# PROMPT DE ENGENHARIA DE SOFTWARE FORENSE
## Gerador da Ferramenta Interativa de Visualização Telemática (ERB / CDR)
**v2.0 — Especificação Completa**
---
> **INSTRUÇÕES DE USO**
>
> Copie e cole **TODO** este prompt em um LLM (Claude, GPT-4, etc).
>
> A ferramenta será entregue como **arquivo HTML único autossuficiente** que funciona offline.
---
## 1. PAPEL E CONTEXTO
Você é um **Engenheiro de Software Sênior** especializado em:
- Sistemas de Informações Geográficas (GIS) com Leaflet.js
- Ferramentas Forenses de Análise Telemática para investigações policiais
- Design tático de interfaces para órgãos de segurança pública
- Processamento e validação de dados telemétricos
**Sua tarefa:** Criar uma aplicação web **interativa em arquivo ÚNICO HTML** (HTML5 + CSS Tailwind + JavaScript vanilla + Leaflet.js + SheetJS) que processe planilhas Excel (.xls, .xlsx) e CSV de extratos de Estação Rádio-Base (ERB) e Call Detail Records (CDR), visualizando:
1. **Histórico cronológico de deslocamento** do alvo (timeline interativa)
2. **Setores angulares de cobertura** (polígonos de 120°) de cada ERB
3. **Azimute central** (linha de orientação da antena)
4. **Metadados forenses** (coordenadas de precisão, timestamps, identificadores)
---
## 2. REQUISITOS FUNCIONAIS
### 2.1 Leitura e Mapeamento Dinâmico de Colunas
A ferramenta deve ler automaticamente planilhas e mapear colunas com **tolerância a variações** de nomenclatura entre operadoras brasileiras (VIVO, TIM, CLARO, OI).
Use este schema para mapeamento:
```json
{
"COLUMN_MAPPING": {
"latitude": [
"Latitude RS", "Latitude RE", "Latitude", "lat", "LAT",
"LATITUDE", "Lat", "latitude"
],
"longitude": [
"Longitude RS", "Longitude RE", "Longitude", "lon", "LON",
"LONGITUDE", "Lon", "longitude"
],
"azimuth": [
"Azimute RS", "Azimute RE", "Azimute", "AZIMUTE",
"Azimuth", "azimuth", "Azi"
],
"terminal": [
"Número A", "MSISDN", "Terminal", "Número",
"TERMINAL", "Alvo", "CPF", "IMSI"
],
"dateStart": [
"Data Início", "Data", "DataInicio",
"Data Partida", "Start Date"
],
"timeStart": [
"Hora Início", "Hora", "HoraInicio",
"Hora Partida", "Start Time"
],
"siteId": [
"SiteID RS", "SiteID", "Site ID", "Site",
"SetorID RS", "SetorID", "ID_ERB"
],
"municipality": [
"Município ERB RS", "Cidade", "Município",
"Municipality", "Local"
],
"address": [
"Endereço ERB RS", "Endereço", "Address",
"Logradouro", "Localização"
]
}
}
```
**Regras de parsing:**
| Campo | Formato Aceito | Tratamento |
|-------|---|---|
| **Latitude/Longitude** | Numérico com vírgula ou ponto | Converter `,` → `.`; validar range: lat ∈ [-90, 90], lon ∈ [-180, 180] |
| **Azimute** | Inteiro 0-360 ou `Ausente` | Se ausente/zero/nulo: usar `0°` (norte); se >360: aplicar `mod 360` |
| **Data** | `YYYYMMDD` ou `DD/MM/YYYY` | Tentar parser nessa ordem; suportar `-`, `/`, `.` como separadores |
| **Hora** | `HHMMSS` ou `HH:MM:SS` | Combinar com data em DateTime ISO; validar range 00:00:00 – 23:59:59 |
| **Colunas ausentes** | Fallback | Usar `0` para coords, `0°` para azimute, `"N/A"` para texto |
---
### 2.2 Lógica Matemática e Geometria Forense
#### A. Setor Angular de 120° ("Fatia de Pizza")
Para cada registro com coordenadas válidas:
1. **Abertura total: 120°** (Azimute − 60° até Azimute + 60°)
2. **Raio configurável dinamicamente** via slider na interface:
- Faixa: 100m a 2.500m
- Padrão: 500m
- Atualização em tempo real ao mover slider
3. **Cálculo geodésico:**
- Usar fórmula de destino com raio da Terra: R = 6.378.137m
- Gerar 9+ vértices do arco (incremento de 15° cada)
- Fechar polígono retornando ao centro
**Pseudocódigo:**
```javascript
function createSectorPolygon(centerLat, centerLon, azimuth, radiusMeters) {
const R = 6378137; // metros
const startAngle = (azimuth - 60) * (Math.PI / 180);
const endAngle = (azimuth + 60) * (Math.PI / 180);
const vertices = [[centerLat, centerLon]]; // incluir centro
for (let angle = startAngle; angle <= endAngle; angle += (15 * Math.PI / 180)) {
const destLat = Math.asin(
Math.sin(centerLat * Math.PI/180) * Math.cos(radiusMeters / R) +
Math.cos(centerLat * Math.PI/180) * Math.sin(radiusMeters / R) * Math.cos(angle)
) * (180 / Math.PI);
const destLon = centerLon * Math.PI/180 + Math.atan2(
Math.sin(angle) * Math.sin(radiusMeters / R) * Math.cos(centerLat * Math.PI/180),
Math.cos(radiusMeters / R) - Math.sin(centerLat * Math.PI/180) * Math.sin(destLat * Math.PI/180)
);
vertices.push([destLat, destLon * (180 / Math.PI)]);
}
vertices.push([centerLat, centerLon]); // fechar polígono
return vertices;
}
```
#### B. Linha de Azimute Central
Desenhe uma linha estendida que:
- Parte do centro da ERB
- Segue na direção exata do azimute
- Estende-se até o raio do setor
- Cor: cyan (#0ea5e9), espessura 2px, opacidade 0.8
#### C. Ordenação Cronológica (OBRIGATÓRIO)
**TODOS** os registros devem ser ordenados cronologicamente **crescente** (mais antigo → mais recente) **antes** de construir a timeline e renderizar o mapa. Se dois registros tiverem o mesmo timestamp:
- Ordenar alfabeticamente por `siteId`
- Se `siteId` também igual, manter ordem original (stable sort)
---
### 2.3 Estrutura de Dados: Mock Dataset Inicial
A aplicação deve **abrir imediatamente funcionando** com dados de exemplo. Use este padrão:
```json
{
"mockDataset": {
"terminal": "11987654321",
"operadora": "VIVO",
"municipios": ["São Paulo", "Guarulhos", "Osasco"],
"records": [
{
"index": 1,
"latitude": -23.550520,
"longitude": -46.633309,
"azimuth": 45,
"datetime": "2024-01-15T14:15:00",
"siteId": "ERB-SP-001",
"municipality": "São Paulo",
"address": "Avenida Paulista, 1000"
},
{
"index": 2,
"latitude": -23.528702,
"longitude": -46.624891,
"azimuth": 135,
"datetime": "2024-01-15T15:02:30",
"siteId": "ERB-SP-002",
"municipality": "São Paulo",
"address": "Rua Augusta, 2500"
},
{
"index": 3,
"latitude": -23.437640,
"longitude": -46.470670,
"azimuth": 270,
"datetime": "2024-01-15T16:45:15",
"siteId": "ERB-GUA-001",
"municipality": "Guarulhos",
"address": "Avenida Monteiro Lobato, 500"
},
{
"index": 4,
"latitude": -23.531090,
"longitude": -46.779050,
"azimuth": 0,
"datetime": "2024-01-15T17:30:00",
"siteId": "ERB-OSA-001",
"municipality": "Osasco",
"address": "Estrada de Ferro, 300"
},
{
"index": 5,
"latitude": -23.550520,
"longitude": -46.633309,
"azimuth": 180,
"datetime": "2024-01-15T18:15:45",
"siteId": "ERB-SP-001",
"municipality": "São Paulo",
"address": "Avenida Paulista, 1000"
},
{
"index": 6,
"latitude": -23.570400,
"longitude": -46.656200,
"azimuth": 90,
"datetime": "2024-01-15T19:00:30",
"siteId": "ERB-SP-003",
"municipality": "São Paulo",
"address": "Rua Oscar Freire, 800"
},
{
"index": 7,
"latitude": -23.437640,
"longitude": -46.470670,
"azimuth": 315,
"datetime": "2024-01-15T20:10:00",
"siteId": "ERB-GUA-001",
"municipality": "Guarulhos",
"address": "Avenida Monteiro Lobato, 500"
},
{
"index": 8,
"latitude": -23.528702,
"longitude": -46.624891,
"azimuth": 225,
"datetime": "2024-01-15T21:30:15",
"siteId": "ERB-SP-002",
"municipality": "São Paulo",
"address": "Rua Augusta, 2500"
}
]
}
}
```
**Características do mock:**
- ✓ 8 registros cronologicamente ordenados
- ✓ Variação de azimutes (45°, 135°, 270°, 0°, 180°, 90°, 315°, 225°)
- ✓ Múltiplos municípios (SP, Guarulhos, Osasco)
- ✓ Repetição intencional de sites (ERB-SP-001 aparece 2x para testar)
- ✓ Timestamps em saltos realistas (45min–1h20min entre eventos)
---
## 3. REQUISITOS VISUAIS
### 3.1 Paleta de Cores (Hex)
```json
{
"colors": {
"bgPrimary": "#0f172a", // Fundo escuro principal (slate-950)
"bgSecondary": "#1e293b", // Fundo secundário (slate-800)
"bgTertiary": "#334155", // Fundo terciário (slate-700)
"textPrimary": "#f1f5f9", // Texto principal (slate-100)
"textSecondary": "#94a3b8", // Texto secundário (slate-400)
"textTertiary": "#64748b", // Texto terciário (slate-500)
"accentCyan": "#0ea5e9", // Azul tático (cyan-500)
"accentPink": "#ec4899", // Rosa/Fúcsia ativa (pink-500)
"sectorInactive": "rgba(225, 29, 72, 0.15)", // rose-600 + alpha 0.15
"sectorActive": "rgba(236, 72, 153, 0.8)", // pink-500 + alpha 0.8
"borderDefault": "#475569", // Borda (slate-600)
"borderHighlight": "#0ea5e9", // Borda destaque (cyan)
"success": "#10b981", // Verde (emerald-500)
"warning": "#f59e0b", // Amarelo (amber-500)
"danger": "#ef4444" // Vermelho (red-500)
}
}
```
### 3.2 Layout Estrutural
**Viewport total: 100vh × 100vw**
Dividida em 3 seções:
```
┌────────────────────────────────────────────────────┐
│ HEADER (h-16, ~80px) │
├──────────────────────┬──────────────────────────────┤
│ │ │
│ MAPA LEAFLET │ TIMELINE & DETALHES │
│ (60% width) │ (40% width) │
│ w-[60%] │ w-[40%] │
│ h-[calc(100vh-80px-70px)] │ h-[calc(100vh-80px-70px)]
│ │ │
│ │ │
│ PLAYER (bottom) │ │
│ h-[70px] │ │
└──────────────────────┴──────────────────────────────┘
```
### 3.3 Componentes da Interface
#### A. HEADER (Altura: 64px)
```html
[#0f172a] border-b border-[#475569]">
POLÍCIA CIVIL — ANÁLISE TELEMÁTICA ERB/CDR
Alvo: {{terminal}}
Eventos: {{totalRecords}}
Setor: 120°
Raio:
500m
Carregar Planilha
Enquadrar Tudo
```
**Badges:**
- **Alvo:** "11987654321" (ou valor do terminal)
- **Eventos:** "8 registros" (total de linhas processadas)
- **Setor:** "120°" (fixo, informativo)
#### B. ÁREA ESQUERDA: Mapa Leaflet (60% width)
```html
[60%] h-[calc(100vh-80px-70px)] relative">
[#1e293b] p-2 rounded">
+
-
ℹ️
```
**Mapa:**
- Tiles: CARTO Dark Matter (`https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png`)
- Zoom inicial: enquadrar todos os pontos (fitBounds)
- Controles padrão: zoom + layers
- Pops-up interativos ao clicar em marcadores/polígonos
#### C. ÁREA DIREITA: Timeline & Detalhes (40% width)
Dividida em 3 sub-seções:
**C1. Search Bar (h-12)**
```html
[#1e293b] p-2 border-b border-[#475569]">
[#334155] text-[#f1f5f9]"
/>
```
**C2. Timeline Cards (flex grow)**
```html
15 jan 2024 - 14:15
45°
ERB-SP-001
São Paulo
```
**C3. Painel de Detalhes (h-32, fixo no rodapé)**
```html
[#0f172a] border-t border-[#475569] p-4">
{{siteId}}
Latitude: {{lat.toFixed(6)}}
Longitude: {{lon.toFixed(6)}}
Azimute: {{azimuth}}°
Endereço: {{address}}
Copiar Coordenadas
```
#### D. PLAYER DE TIMELINE (Altura: 70px, rodapé)
```html
[70px] bg-
[#1e293b]
border-t border-
[#475569]
p-3 flex items-center gap-4">
⏮️ Anterior
▶️ Play
⏭️ Próximo
[#94a3b8] text-center">
Registro 1 / 8
1x
2x
5x
```
---
## 4. INTERATIVIDADE E SINCRONIZAÇÃO
### 4.1 Sincronização Bidirecional
#### Click em Card da Timeline → Mapa
Ao clicar em um card na área direita:
1. **Map Pan/Fly:**
- `map.flyTo([lat, lon], 16)` com duração 300ms
- Easing: quadratic
2. **Highlight do Setor:**
- Remover highlight do setor anterior
- Aplicar cor `rgba(236, 72, 153, 0.8)` ao novo setor
- Desenhar borda grossa (3px) em cyan (#0ea5e9)
- Abrir popup com detalhes
3. **Card Visual:**
- Adicionar classe `.active` com fundo pink/rosa
- Scroll automático: `cardElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })`
#### Click em Polígono/Marcador no Mapa → Timeline
Ao clicar em um setor ou marcador no mapa:
1. **Timeline Scroll:**
- Encontrar card correspondente pelo índice de registro
- `cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' })`
2. **Card Highlight:**
- Adicionar classe `.active`
- Fade-in 200ms
3. **Painel de Detalhes:**
- Atualizar dados do registro selecionado
- Mostrar coordenadas com 6 casas decimais
### 4.2 Timeline Player (Play/Pause/Velocidade)
```javascript
// Pseudocódigo
currentIndex = 0;
isPlaying = false;
playSpeed = 1; // 1x, 2x, 5x
function play() {
isPlaying = true;
const intervals = { 1: 500, 2: 250, 5: 100 }; // ms
const interval = setInterval(() => {
if (currentIndex < records.length - 1) {
currentIndex++;
updateMapAndTimeline(currentIndex);
updateScrubber();
} else {
// Atingiu fim: parar automaticamente (NÃO loopear)
isPlaying = false;
clearInterval(interval);
}
}, intervals[playSpeed]);
}
function pause() {
isPlaying = false;
clearInterval(interval);
}
function updateSpeed(newSpeed) {
playSpeed = newSpeed;
if (isPlaying) {
pause();
play(); // reiniciar com nova velocidade
}
}
```
---
## 5. TRATAMENTO DE ERROS E VALIDAÇÃO
### 5.1 Validação de Dados
| Cenário | Ação |
|---------|------|
| Latitude/Longitude fora de range | Ignorar registro; logar no console: `"[WARN] Registro #5: coordenadas inválidas"` |
| Data malformada | Tentar parsers: `YYYYMMDD` → `DD/MM/YYYY` → `ISO8601`; se falhar, usar timestamp "N/A" |
| Azimute ausente/0/nulo | Usar `0°` (norte) |
| Azimute > 360 | Aplicar `mod 360` |
| Hora inválida (ex: 25:00:00) | Usar `00:00:00` |
| Coluna não encontrada | Usar valor default: `lat/lon = 0`, `azimuth = 0°`, `text = "N/A"` |
| Arquivo vazio | Exibir mensagem: `"❌ Nenhum registro válido encontrado no arquivo"` |
| Arquivo corrompido | Exibir: `"❌ Erro ao ler arquivo. Verifique formato .xlsx ou .csv"` |
### 5.2 Exibição de Erros (Sem `alert()`)
Usar container visual fixo no topo da interface:
```html
[#ef4444] text-[#f1f5f9] px-4 py-2 rounded z-50">
```
**Tipos de mensagens:**
- **Sucesso:** Verde (#10b981), 3 segundos, depois fade out
- **Aviso:** Amarelo (#f59e0b), 5 segundos
- **Erro:** Vermelho (#ef4444), 8 segundos (ou até clicar X)
---
## 6. PERFORMANCE E ESCALABILIDADE
### 6.1 Limite e Otimizações
| Limite | Comportamento |
|--------|---|
| < 500 registros | Renderizar todos os setores normalmente |
| 500–1.000 registros | Avisar: `"⚠️ Grande volume de dados. Aplicação pode ficar lenta; considere filtrar por data."` |
| > 1.000 registros | Rejreitar com mensagem; sugerir split em múltiplos uploads |
### 6.2 Timeline Virtualização
Para > 100 cards na timeline:
- Renderizar apenas cards **visíveis** na viewport
- Manter scroll suave (uso de intersection observer)
- Cache de templates HTML
### 6.3 Busca em Tempo Real
- Debounce: 300ms
- Filtrar por: `terminal`, `siteId`, `municipality`, `data` (substring match case-insensitive)
- Destacar matches em amarelo (#f59e0b)
---
## 7. RESPONSIVIDADE
### 7.1 Breakpoints
| Breakpoint | Layout | Comportamento |
|---|---|---|
| **Desktop** (≥1024px) | 60% mapa \| 40% timeline | Normal |
| **Tablet** (768–1023px) | 50% mapa \| 50% timeline | Ajustar tamanhos proporcionais |
| **Mobile** (<768px) | Stack vertical (100%) | Mapa acima, timeline abaixo com height máxima (40vh) |
### 7.2 Mobile-Specific
- Desabilitar renderização de setores de 120° (trocar por marcadores simples com circulo de raio)
- Ocultar painel lateral, mostrar em tab/modal
- Player em posição fixed bottom
- Busca com teclado numérico otimizado
---
## 8. POPUP DO MAPA
Ao clicar em marcador ou polígono, exibir popup com:
```html
{{siteId}}
Localização: {{municipality}}
Endereço: {{address}}
Azimute: {{azimuth}}°
Latitude: {{latitude.toFixed(6)}}
Longitude: {{longitude.toFixed(6)}}
Data/Hora: {{dateTimeFormatted}}
📋 Copiar Coordenadas
✕
```
**Ação ao copiar:** Copiar para clipboard no formato `LAT, LON` (ex: `-23.550520, -46.633309`); exibir toast "✓ Coordenadas copiadas".
---
## 9. ANIMAÇÃO E TIMING
### 9.1 Transições
| Elemento | Propriedade | Duração | Easing |
|---|---|---|---|
| Mapa flyTo | Pan/zoom | 300ms | quadratic |
| Highlight setor | Opacity + border | 200ms | ease-out |
| Card fade-in | Opacity | 200ms | ease-in |
| Tooltip | Opacity | 100ms | ease-out |
| Error notification | Fade out | 300ms | ease-out |
### 9.2 Player Timing
- **Velocidade 1x:** incrementa índice a cada **500ms**
- **Velocidade 2x:** incrementa índice a cada **250ms**
- **Velocidade 5x:** incrementa índice a cada **100ms**
### 9.3 Comportamento ao Fim
Ao atingir o **último registro:**
- Parar automaticamente (NOT loop)
- Manter último evento destacado
- Resetar botão Play para estado inicial
---
## 10. RESTRIÇÕES TÉCNICAS E ENTREGA
### 10.1 Arquivo Único HTML
**Requisito:** Arquivo `.html` autossuficiente, executável offline.
**Imports via CDN:**
```html
```
### 10.2 Restrições JavaScript
- ❌ **NUNCA** usar `alert()`, `confirm()`, `prompt()`
- ✅ Usar containers visuais para mensagens (notificações)
- ✅ Eventos de erro logar no console (`console.warn`, `console.error`)
- ✅ Código limpo sem placeholders (`...`), omissões ou comentários TODO
### 10.3 Estrutura de Código
```javascript
// Estrutura esperada
const APP = {
config: { /* paleta, limites */ },
data: { records: [], currentIndex: 0 },
ui: { map, timeline, controls },
handlers: { onLoadFile, onClickCard, onPlay },
utils: { parseDate, validateCoords, formatDMS }
};
// Inicializar
document.addEventListener('DOMContentLoaded', () => {
APP.init();
APP.loadMockData();
APP.render();
});
```
---
## 11. CRITÉRIOS DE ACEIÇÃO
A ferramenta será considerada **pronta** quando:
- ✅ Página abre imediatamente com mock dataset visível
- ✅ Mapa Leaflet carrega com tiles CARTO Dark Matter
- ✅ Todos os 8 registros mock renderizam como polígonos + marcadores
- ✅ Setores de 120° renderizam corretamente com geometria geodésica
- ✅ Linha de azimute central desenha-se em cyan (#0ea5e9)
- ✅ Timeline ordena cronologicamente (evento 1 → 8)
- ✅ Clicking card na timeline → mapa flyTo + highlight + popup
- ✅ Clicking polígono no mapa → timeline scrolla + card destaca
- ✅ Slider de raio (100–2500m) atualiza setores em tempo real
- ✅ Player: Play/Pause/Anterior/Próximo funcionam sincronizados
- ✅ Seletor velocidade (1x/2x/5x) altera timing do play
- ✅ Busca filtra cards dinamicamente (debounce 300ms)
- ✅ Botão "Carregar Planilha" dispara file picker
- ✅ Upload .xlsx/.csv valida colunas, mapeia dinamicamente, renderiza novo dataset
- ✅ Tratamento de erros: sem `alert()`, mensagens em container visual
- ✅ Coordenadas exibidas com 6 casas decimais no painel de detalhes
- ✅ Popup mostra todas as informações do registro
- ✅ Botão "Copiar Coordenadas" copia `LAT, LON` para clipboard
- ✅ Botão "Enquadrar Tudo" (fitBounds) centraliza mapa em todos os pontos
- ✅ Paleta de cores segue hex specification (escuro, cyan, pink)
- ✅ Responsividade: desktop (60/40) e mobile (stack vertical)
- ✅ Nenhuma função `alert()`, `confirm()` ou `prompt()`
- ✅ Código limpo, sem placeholders, sem omissões
---
## 12. EXEMPLO DE SAÍDA ESPERADA
Ao carregar a página, o usuário verá:
1. **Header** com logo, título, badges (Terminal: 11987654321, Eventos: 8, Setor: 120°)
2. **Mapa** (60% left) com:
- 8 marcadores numerados
- 8 polígonos de setor 120° em rosa/fúcsia semitransparente
- 8 linhas de azimute central em cyan
- Zoom enquadrando toda a região SP–Guarulhos–Osasco
3. **Timeline** (40% right) com:
- Caixa de busca no topo
- 8 cards ordenados cronologicamente
- Card #1 destacado por padrão
- Painel inferior com detalhes do card selecionado
4. **Player** (bottom) com botões Play/Pause, Anterior/Próximo, scrubber, selector velocidade
Ao clicar **Play**, o mapa anima sequencialmente (500ms entre eventos), percorrendo todos os 8 registros.
---
## 13. NOTAS ADICIONAIS
- **Dados sensíveis:** Esta ferramenta processa dados de investigação policial. Implemente segurança:
- Não persistir dados em localStorage sem criptografia
- Avisar ao usuário: "Dados não são salvos. Feche a aba para limpar."
- **Acessibilidade:** Use `aria-labels` nos botões, contraste de cores conforme WCAG AA
- **Documentação:** Incluir em comentário no HTML a versão do prompt (v2.0) e link para `engenhariadeprompts.ia.br`