Adiciona visualizacao combinada (Gantt + mapa + tabela) e exporta inspecoes no CSV (v0.8)

oamrp_v3.py: exporta linhas status=inspecao no CSV resultado com datas UTC reais
visualizar_resultado.py: refatorado para gerar um unico HTML (planejamento.html)
com cabecalho, Gantt Plotly (divisoes diarias, FAB labels), mapa Folium em iframe
e tabela de voos com filtros por aeronave/status/prioridade/base e busca livre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cesa-V
2026-06-16 01:48:36 -03:00
parent 4d8554df38
commit 3756f961a8
2 changed files with 513 additions and 96 deletions

View File

@@ -1,29 +1,29 @@
"""
Visualização interativa do resultado OAMRP — Esquadrão Arara C-105.
Gera um mapa HTML (Folium) com:
- Marcadores para cada base operacional
- Rotas por aeronave (cor distinta por EVAM)
- Missões não cumpridas em cinza tracejado
- Popup com detalhes de cada missão
Gera um único HTML com título "Planejamento de Diagonal de Manutenção" contendo:
- Gantt (Plotly): calendário aeronave × tempo com voos e inspeções
- Mapa (Folium): rotas por aeronave, missões não cumpridas, bases
Uso:
python software/visualizar_resultado.py \
--resultado db/processed/resultado_oamrp_sintetico.csv \
--saida db/processed/mapa_rotas.html
--saida db/processed/planejamento.html
"""
import argparse
import csv
import io
from datetime import datetime, timedelta, timezone
from html import escape
from pathlib import Path
import folium
from folium import plugins
BASE_DIR = Path(__file__).resolve().parents[1]
RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv"
MAPA_PADRAO = BASE_DIR / "db" / "processed" / "mapa_rotas.html"
SAIDA_PADRAO = BASE_DIR / "db" / "processed" / "planejamento.html"
# Coordenadas das bases (lat, lon)
BASES_COORDS: dict[str, tuple[float, float]] = {
@@ -39,7 +39,6 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMY": (-5.81, -61.28),
}
# Nomes legíveis das bases
BASES_NOMES = {
"SBMN": "Manaus / Ponta Pelada (Hub)",
"SBBE": "Belém",
@@ -53,21 +52,39 @@ BASES_NOMES = {
"SBMY": "Manicoré",
}
# Paleta de cores por aeronave
CORES_AERONAVE = {
"2800": "#1f77b4", # azul
"2803": "#2ca02c", # verde
"2809": "#d62728", # vermelho
"2811": "#ff7f0e", # laranja
"2800": "#1f77b4",
"2803": "#2ca02c",
"2809": "#d62728",
"2811": "#ff7f0e",
}
COR_NAO_CUMPRIDA = "#aaaaaa"
COR_HUB = "#d62728"
COR_BASE = "#1f77b4"
_GANTT_CORES = {
"cumprida": {
"1": "#d62728",
"2": "#ff7f0e",
"3": "#2ca02c",
"4": "#1f77b4",
"5": "#9467bd",
"": "#1f77b4",
},
"inspecao": "#8c564b",
"nao_cumprida": "#aaaaaa",
}
def _cor_prio(prio: str) -> str:
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}
return mapa.get(str(prio), "white")
return {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}.get(str(prio), "white")
def _cor_gantt(linha: dict) -> str:
status = linha["status"]
if status == "inspecao":
return _GANTT_CORES["inspecao"]
if status == "nao_cumprida":
return _GANTT_CORES["nao_cumprida"]
return _GANTT_CORES["cumprida"].get(linha.get("prioridade", ""), "#1f77b4")
def carregar_resultado(caminho: Path) -> list[dict]:
@@ -75,57 +92,49 @@ def carregar_resultado(caminho: Path) -> list[dict]:
return list(csv.DictReader(f))
def adicionar_bases(mapa: folium.Map) -> None:
for icao, (lat, lon) in BASES_COORDS.items():
cor = COR_HUB if icao == "SBMN" else COR_BASE
icone = folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa")
folium.Marker(
location=[lat, lon],
tooltip=f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}",
popup=folium.Popup(f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", max_width=200),
icon=icone,
).add_to(mapa)
def _resumir_resultado(linhas: list[dict]) -> None:
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
total_missoes = sum(1 for r in linhas if r["status"] in ("cumprida", "nao_cumprida"))
print(f" Missões: {cumpridas}/{total_missoes} cumpridas")
aeronaves: dict[str, int] = {}
for r in linhas:
if r["aeronave"] and r["status"] == "cumprida":
aeronaves.setdefault(r["aeronave"], 0)
aeronaves[r["aeronave"]] += 1
for k, c in sorted(aeronaves.items()):
print(f" FAB {k}: {c} missões")
def gerar_mapa(resultado: Path, saida: Path) -> None:
linhas = carregar_resultado(resultado)
# ---------------------------------------------------------------------------
# Mapa Folium → string HTML
# ---------------------------------------------------------------------------
mapa = folium.Map(
location=[-3.5, -62.0],
zoom_start=5,
tiles="CartoDB positron",
)
def _construir_mapa(linhas: list[dict]) -> str:
mapa = folium.Map(location=[-3.5, -62.0], zoom_start=5, tiles="CartoDB positron")
# Grupos por aeronave + grupo para não cumpridas
aeronaves_vistas = sorted({r["aeronave"] for r in linhas if r["aeronave"]})
grupos: dict[str, folium.FeatureGroup] = {}
for k in aeronaves_vistas:
grupos[k] = folium.FeatureGroup(name=f"EVAM {k}", show=True)
grupos[k] = folium.FeatureGroup(name=f"FAB {k}", show=True)
grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True)
cumpridas = [r for r in linhas if r["status"] == "cumprida"]
nao_cumpridas = [r for r in linhas if r["status"] != "cumprida"]
# Rotas cumpridas
for r in cumpridas:
orig = r["orig"]
dest = r["dest"]
for r in linhas:
if r["status"] != "cumprida":
continue
orig, dest = r["orig"], r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
k = r["aeronave"]
cor = CORES_AERONAVE.get(k, "#333333")
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
# Linha de rota
folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=cor,
weight=2.5,
opacity=0.8,
tooltip=f"EVAM {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
color=cor, weight=2.5, opacity=0.8,
tooltip=f"FAB {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
popup=folium.Popup(
f"<b>EVAM {k}</b> — Missão #{r['ordem']}<br>"
f"<b>FAB {k}</b> — Missão #{r['ordem']}<br>"
f"{orig}{dest}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z<br>"
@@ -134,43 +143,38 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
max_width=250,
),
).add_to(grupos[k])
# Marcador de origem (pequeno círculo)
folium.CircleMarker(
location=[lat_o, lon_o],
radius=4,
color=cor,
fill=True,
fill_color=_cor_prio(r["prioridade"]),
fill_opacity=0.9,
location=[lat_o, lon_o], radius=4, color=cor,
fill=True, fill_color=_cor_prio(r["prioridade"]), fill_opacity=0.9,
tooltip=f"{orig} | prio {r['prioridade']}",
).add_to(grupos[k])
# Missões não cumpridas (linha tracejada cinza)
for r in nao_cumpridas:
orig = r["orig"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=COR_NAO_CUMPRIDA,
weight=1.5,
opacity=0.5,
dash_array="6 4",
tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
).add_to(grupos["nao_cumprida"])
for r in linhas:
if r["status"] == "nao_cumprida":
orig, dest = r["orig"], r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=COR_NAO_CUMPRIDA, weight=1.5, opacity=0.5, dash_array="6 4",
tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
).add_to(grupos["nao_cumprida"])
# Adiciona grupos ao mapa
for g in grupos.values():
g.add_to(mapa)
adicionar_bases(mapa)
for icao, (lat, lon) in BASES_COORDS.items():
folium.Marker(
location=[lat, lon],
tooltip=f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}",
popup=folium.Popup(f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", max_width=200),
icon=folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa"),
).add_to(mapa)
folium.LayerControl(collapsed=False).add_to(mapa)
# Legenda HTML
legenda_html = """
<div style="position:fixed;bottom:30px;left:30px;z-index:1000;background:white;
padding:12px 16px;border-radius:8px;box-shadow:2px 2px 6px rgba(0,0,0,0.3);
@@ -182,7 +186,7 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
legenda_html += (
f' <span style="display:inline-block;width:16px;height:4px;'
f'background:{cor};margin-right:6px;vertical-align:middle;"></span>'
f'EVAM {k}<br>'
f'FAB {k}<br>'
)
legenda_html += """
<br><b>Prioridade (preenchimento):</b><br>
@@ -201,31 +205,418 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
"""
mapa.get_root().html.add_child(folium.Element(legenda_html))
saida.parent.mkdir(parents=True, exist_ok=True)
mapa.save(str(saida))
print(f" Mapa salvo: {saida}")
_resumir_resultado(linhas)
return mapa.get_root().render()
def _resumir_resultado(linhas: list[dict]) -> None:
total = len(linhas)
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
print(f" Missões: {cumpridas}/{total} cumpridas")
aeronaves = {}
# ---------------------------------------------------------------------------
# Gantt Plotly → string HTML (só o div, sem <html>)
# ---------------------------------------------------------------------------
def _construir_gantt(linhas: list[dict]) -> str:
try:
import plotly.graph_objects as go
except ImportError:
return "<p><em>plotly não instalado — instale com: pip install plotly</em></p>"
aeronaves_ord = sorted(
{r["aeronave"] for r in linhas if r["aeronave"]},
key=lambda k: int(k) if k.isdigit() else 0,
)
y_pos = {k: i for i, k in enumerate(aeronaves_ord)}
fig = go.Figure()
for i, k in enumerate(aeronaves_ord):
fig.add_hrect(
y0=i - 0.45, y1=i + 0.45,
fillcolor="#f0f0f0" if i % 2 == 0 else "white",
layer="below", line_width=0,
)
shapes = []
hover_traces = []
for r in linhas:
if r["aeronave"]:
aeronaves.setdefault(r["aeronave"], 0)
aeronaves[r["aeronave"]] += 1
for k, c in sorted(aeronaves.items()):
print(f" EVAM {k}: {c} missões")
k = r["aeronave"]
if not k or k not in y_pos:
continue
try:
ini = r["partida_utc"].replace("Z", "+00:00")
fim = r["chegada_utc"].replace("Z", "+00:00")
except Exception:
continue
yi = y_pos[k]
cor = _cor_gantt(r)
status = r["status"]
shapes.append(dict(
type="rect", xref="x", yref="y",
x0=ini, x1=fim,
y0=yi - 0.4, y1=yi + 0.4,
fillcolor=cor,
line=dict(color="white", width=0.5),
opacity=1.0 if status == "inspecao" else 0.85,
layer="above",
))
if status == "inspecao":
txt = (
f"<b>FAB {k}</b> — Inspeção {r['om']}<br>"
f"Início: {ini[:16].replace('T',' ')}Z<br>"
f"Fim: {fim[:16].replace('T',' ')}Z<br>"
f"Duração: {r['dur_h']}h"
)
else:
txt = (
f"<b>FAB {k}</b> — Missão #{r['ordem']}<br>"
f"{r['orig']}{r['dest']}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {ini[:16].replace('T',' ')}Z<br>"
f"Chegada: {fim[:16].replace('T',' ')}Z<br>"
f"Duração: {r['dur_h']}h"
)
hover_traces.append(go.Scatter(
x=[ini, fim], y=[yi, yi], mode="markers",
marker=dict(size=0.1, color=cor, opacity=0),
hovertext=[txt, txt], hoverinfo="text", showlegend=False,
))
# Divisões diárias
datas_utc = [
r[campo].replace("Z", "+00:00")
for r in linhas
for campo in ("partida_utc", "chegada_utc")
if r.get(campo)
]
if datas_utc:
t_min = datetime.fromisoformat(min(datas_utc))
t_max = datetime.fromisoformat(max(datas_utc))
dia = datetime(t_min.year, t_min.month, t_min.day, tzinfo=timezone.utc)
while dia <= t_max + timedelta(days=1):
shapes.append(dict(
type="line", xref="x", yref="paper",
x0=dia.isoformat(), x1=dia.isoformat(),
y0=0, y1=1,
line=dict(color="#bbbbbb", width=1, dash="dot"),
layer="above",
))
dia += timedelta(days=1)
# Legenda
for nome, cor in [
("Prio 1", _GANTT_CORES["cumprida"]["1"]),
("Prio 2", _GANTT_CORES["cumprida"]["2"]),
("Prio 3", _GANTT_CORES["cumprida"]["3"]),
("Prio 4", _GANTT_CORES["cumprida"]["4"]),
("Prio 5", _GANTT_CORES["cumprida"]["5"]),
("Inspeção", _GANTT_CORES["inspecao"]),
]:
fig.add_trace(go.Scatter(
x=[None], y=[None], mode="markers",
marker=dict(size=12, color=cor, symbol="square"),
name=nome, showlegend=True,
))
for t in hover_traces:
fig.add_trace(t)
fig.update_layout(
shapes=shapes,
xaxis=dict(
title="Data / Hora (UTC)", type="date",
tickformat="%d/%m", dtick=86400000,
showgrid=True, gridcolor="#dddddd",
),
yaxis=dict(
title="Aeronave",
tickvals=list(range(len(aeronaves_ord))),
ticktext=[f"FAB {k}" for k in aeronaves_ord],
showgrid=False,
),
height=max(300, 120 + 80 * len(aeronaves_ord)),
plot_bgcolor="white",
paper_bgcolor="white",
legend=dict(title="Legenda", orientation="v", x=1.01, y=1, xanchor="left"),
margin=dict(l=100, r=160, t=10, b=60),
hovermode="closest",
)
return fig.to_html(full_html=False, include_plotlyjs="cdn")
# ---------------------------------------------------------------------------
# Tabela de voos com filtros
# ---------------------------------------------------------------------------
def _construir_tabela(linhas: list[dict]) -> str:
import json
# Monta lista de registros para a tabela (missões + inspeções, sem nao_cumprida sem aeronave)
registros = []
for r in linhas:
status = r["status"]
k = r["aeronave"]
if status == "nao_cumprida" and not k:
# mantém como linha sem aeronave
k = ""
registros.append({
"status": status,
"aeronave": f"FAB {k}" if k and k != "" else "",
"ordem": r.get("ordem", ""),
"om": r.get("om", ""),
"orig": r.get("orig", ""),
"dest": r.get("dest", ""),
"partida": r.get("partida_utc", "")[:16].replace("T", " ") + "Z" if r.get("partida_utc") else "",
"chegada": r.get("chegada_utc", "")[:16].replace("T", " ") + "Z" if r.get("chegada_utc") else "",
"dur_h": r.get("dur_h", ""),
"prioridade": r.get("prioridade", ""),
})
dados_json = json.dumps(registros, ensure_ascii=False)
opcoes_aeronave = sorted({r["aeronave"] for r in registros if r["aeronave"] != ""})
opcoes_status = ["cumprida", "nao_cumprida", "inspecao"]
opcoes_prio = sorted({r["prioridade"] for r in registros if r["prioridade"]})
opcoes_base = sorted({b for r in registros for b in (r["orig"], r["dest"]) if b})
def opts(valores: list[str], label: str) -> str:
html = f'<option value="">— {label} —</option>'
for v in valores:
html += f'<option value="{escape(v)}">{escape(v)}</option>'
return html
return f"""
<style>
.filtros {{
display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; align-items: flex-end;
}}
.filtros label {{ font-size: 0.78rem; color: #555; display: flex; flex-direction: column; gap: 3px; }}
.filtros select, .filtros input {{
border: 1px solid #ccc; border-radius: 4px; padding: 5px 8px;
font-size: 0.85rem; min-width: 130px; background: white;
}}
.filtros button {{
padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer;
font-size: 0.85rem; background: #1a3a5c; color: white;
align-self: flex-end;
}}
.filtros button:hover {{ background: #254f80; }}
#contador {{ font-size: 0.82rem; color: #666; margin-bottom: 10px; }}
#tabela-voos {{ width: 100%; border-collapse: collapse; font-size: 0.85rem; }}
#tabela-voos thead th {{
background: #1a3a5c; color: white; padding: 8px 10px;
text-align: left; white-space: nowrap; cursor: pointer; user-select: none;
}}
#tabela-voos thead th:hover {{ background: #254f80; }}
#tabela-voos thead th .sort-icon {{ margin-left: 4px; opacity: 0.5; }}
#tabela-voos tbody tr:nth-child(even) {{ background: #f7f8fa; }}
#tabela-voos tbody tr:hover {{ background: #e8f0fb; }}
#tabela-voos td {{ padding: 7px 10px; border-bottom: 1px solid #eee; white-space: nowrap; }}
.badge {{
display: inline-block; padding: 2px 8px; border-radius: 10px;
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
}}
.badge-cumprida {{ background: #d4edda; color: #155724; }}
.badge-nao_cumprida {{ background: #f8d7da; color: #721c24; }}
.badge-inspecao {{ background: #e2d9f3; color: #4a235a; }}
.prio-1 {{ color: #d62728; font-weight: 700; }}
.prio-2 {{ color: #ff7f0e; font-weight: 700; }}
.prio-3 {{ color: #2ca02c; font-weight: 700; }}
</style>
<div class="filtros">
<label>Aeronave
<select id="f-aeronave">{opts(opcoes_aeronave, "Todas")}</select>
</label>
<label>Status
<select id="f-status">{opts(opcoes_status, "Todos")}</select>
</label>
<label>Prioridade
<select id="f-prio">{opts(opcoes_prio, "Todas")}</select>
</label>
<label>Base (orig/dest)
<select id="f-base">{opts(opcoes_base, "Todas")}</select>
</label>
<label>Busca (OM, rota…)
<input id="f-texto" type="text" placeholder="ex: SBMN, EVAM…">
</label>
<button onclick="limparFiltros()">Limpar</button>
</div>
<div id="contador"></div>
<table id="tabela-voos">
<thead>
<tr>
<th onclick="ordenar(0)">Aeronave <span class="sort-icon">↕</span></th>
<th onclick="ordenar(1)">Status <span class="sort-icon">↕</span></th>
<th onclick="ordenar(2)">#Missão <span class="sort-icon">↕</span></th>
<th onclick="ordenar(3)">OM <span class="sort-icon">↕</span></th>
<th onclick="ordenar(4)">Origem <span class="sort-icon">↕</span></th>
<th onclick="ordenar(5)">Destino <span class="sort-icon">↕</span></th>
<th onclick="ordenar(6)">Partida (UTC) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(7)">Chegada (UTC) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(8)">Duração (h) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(9)">Prio <span class="sort-icon">↕</span></th>
</tr>
</thead>
<tbody id="corpo-tabela"></tbody>
</table>
<script>
const DADOS = {dados_json};
let _colOrdem = -1, _colAsc = true;
function badgeStatus(s) {{
const label = {{cumprida:'Cumprida', nao_cumprida:'Não cumprida', inspecao:'Inspeção'}}[s] || s;
return `<span class="badge badge-${{s}}">${{label}}</span>`;
}}
function prio(p) {{
if (!p) return '';
const cls = ['1','2','3'].includes(p) ? `prio-${{p}}` : '';
return `<span class="${{cls}}">${{p}}</span>`;
}}
function linhasFiltradas() {{
const fa = document.getElementById('f-aeronave').value;
const fs = document.getElementById('f-status').value;
const fp = document.getElementById('f-prio').value;
const fb = document.getElementById('f-base').value;
const ft = document.getElementById('f-texto').value.toLowerCase();
return DADOS.filter(r => {{
if (fa && r.aeronave !== fa) return false;
if (fs && r.status !== fs) return false;
if (fp && r.prioridade !== fp) return false;
if (fb && r.orig !== fb && r.dest !== fb) return false;
if (ft && !JSON.stringify(r).toLowerCase().includes(ft)) return false;
return true;
}});
}}
function renderizar() {{
let rows = linhasFiltradas();
if (_colOrdem >= 0) {{
const chaves = ['aeronave','status','ordem','om','orig','dest','partida','chegada','dur_h','prioridade'];
const c = chaves[_colOrdem];
rows = [...rows].sort((a,b) => {{
const va = a[c] ?? '', vb = b[c] ?? '';
return _colAsc ? String(va).localeCompare(String(vb), 'pt', {{numeric:true}})
: String(vb).localeCompare(String(va), 'pt', {{numeric:true}});
}});
}}
document.getElementById('contador').textContent =
`${{rows.length}} registro${{rows.length !== 1 ? 's' : ''}} exibido${{rows.length !== 1 ? 's' : ''}} de ${{DADOS.length}}`;
document.getElementById('corpo-tabela').innerHTML = rows.map(r => `
<tr>
<td>${{r.aeronave}}</td>
<td>${{badgeStatus(r.status)}}</td>
<td>${{r.ordem}}</td>
<td>${{r.om}}</td>
<td>${{r.orig}}</td>
<td>${{r.dest}}</td>
<td>${{r.partida}}</td>
<td>${{r.chegada}}</td>
<td>${{r.dur_h}}</td>
<td>${{prio(r.prioridade)}}</td>
</tr>`).join('');
}}
function ordenar(col) {{
if (_colOrdem === col) _colAsc = !_colAsc;
else {{ _colOrdem = col; _colAsc = true; }}
renderizar();
}}
function limparFiltros() {{
['f-aeronave','f-status','f-prio','f-base'].forEach(id => document.getElementById(id).value = '');
document.getElementById('f-texto').value = '';
renderizar();
}}
['f-aeronave','f-status','f-prio','f-base'].forEach(id =>
document.getElementById(id).addEventListener('change', renderizar));
document.getElementById('f-texto').addEventListener('input', renderizar);
renderizar();
</script>"""
# ---------------------------------------------------------------------------
# HTML combinado
# ---------------------------------------------------------------------------
def gerar_planejamento(resultado: Path, saida: Path) -> None:
linhas = carregar_resultado(resultado)
gantt_div = _construir_gantt(linhas)
mapa_html = _construir_mapa(linhas)
tabela_div = _construir_tabela(linhas)
html = f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Planejamento de Diagonal de Manutenção</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #222; }}
header {{
background: #1a3a5c; color: white;
padding: 18px 32px;
border-bottom: 4px solid #c8a400;
}}
header h1 {{ font-size: 1.5rem; font-weight: 700; letter-spacing: 0.03em; }}
header p {{ font-size: 0.85rem; opacity: 0.75; margin-top: 4px; }}
.container {{ padding: 24px 32px; max-width: 1600px; margin: 0 auto; }}
.section {{
background: white; border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
margin-bottom: 28px; overflow: hidden;
}}
.section-title {{
background: #f7f8fa; border-bottom: 1px solid #e0e0e0;
padding: 12px 20px; font-size: 0.95rem; font-weight: 600;
color: #1a3a5c; text-transform: uppercase; letter-spacing: 0.05em;
}}
.section-body {{ padding: 16px; }}
iframe.mapa {{
width: 100%; height: 560px; border: none; border-radius: 4px; display: block;
}}
</style>
</head>
<body>
<header>
<h1>Planejamento de Diagonal de Manutenção</h1>
<p>Esquadrão Arara · C-105 · OAMRP v3</p>
</header>
<div class="container">
<div class="section">
<div class="section-title">Calendário de Emprego das Aeronaves</div>
<div class="section-body">{gantt_div}</div>
</div>
<div class="section">
<div class="section-title">Rotas Operacionais</div>
<iframe class="mapa" srcdoc="{escape(mapa_html)}"></iframe>
</div>
<div class="section">
<div class="section-title">Planejamento de Voos</div>
<div class="section-body">{tabela_div}</div>
</div>
</div>
</body>
</html>"""
saida.parent.mkdir(parents=True, exist_ok=True)
saida.write_text(html, encoding="utf-8")
print(f" Planejamento salvo: {saida}")
_resumir_resultado(linhas)
def main() -> None:
parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105")
parser.add_argument("--resultado", type=Path, default=RESULTADO_PADRAO)
parser.add_argument("--saida", type=Path, default=MAPA_PADRAO)
parser.add_argument("--saida", type=Path, default=SAIDA_PADRAO)
args = parser.parse_args()
gerar_mapa(resultado=args.resultado, saida=args.saida)
gerar_planejamento(resultado=args.resultado, saida=args.saida)
if __name__ == "__main__":