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

@@ -787,6 +787,8 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
missoes_nos = sol["missoes_nos"] missoes_nos = sol["missoes_nos"]
linhas = [] linhas = []
horizonte_dt = sol.get("horizonte_dt")
cobertas_ids: set[str] = set() cobertas_ids: set[str] = set()
for k in sol["ks"]: for k in sol["ks"]:
missoes_rota = [n for n in rota_da_aeronave(sol, k) if n.tipo == "missao"] missoes_rota = [n for n in rota_da_aeronave(sol, k) if n.tipo == "missao"]
@@ -810,6 +812,29 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
"linhas_origem": m.linhas_origem, "linhas_origem": m.linhas_origem,
}) })
# Inspeções executadas por esta aeronave
if horizonte_dt is not None:
for seq, s_val, insp in inspecoes_da_aeronave(sol, k):
from datetime import timedelta
ini_dt = horizonte_dt + timedelta(hours=s_val)
fim_dt = horizonte_dt + timedelta(hours=s_val + insp.dt_h)
linhas.append({
"status": "inspecao",
"aeronave": k,
"ordem": "",
"id": f"INSP_{k}_{seq}",
"om": insp.sigla,
"orig": "SBMN",
"dest": "SBMN",
"partida_utc": ini_dt.isoformat().replace("+00:00", "Z"),
"chegada_utc": fim_dt.isoformat().replace("+00:00", "Z"),
"dur_h": f"{insp.dt_h:.2f}",
"prioridade": "",
"periodo": "",
"aeronave_real_2025": "",
"linhas_origem": "",
})
for nid, n in missoes_nos.items(): for nid, n in missoes_nos.items():
if nid in cobertas_ids: if nid in cobertas_ids:
continue continue
@@ -1406,6 +1431,7 @@ def main() -> None:
nos, arcos = construir_rede(missoes, aeronaves, catalogo) nos, arcos = construir_rede(missoes, aeronaves, catalogo)
sol = resolver(nos, arcos, aeronaves, catalogo) sol = resolver(nos, arcos, aeronaves, catalogo)
sol["horizonte_dt"] = horizonte_dt
escrever_resultado(sol, args.resultado) escrever_resultado(sol, args.resultado)
imprimir_relatorio(sol, bases, args.resultado) imprimir_relatorio(sol, bases, args.resultado)

View File

@@ -1,29 +1,29 @@
""" """
Visualização interativa do resultado OAMRP — Esquadrão Arara C-105. Visualização interativa do resultado OAMRP — Esquadrão Arara C-105.
Gera um mapa HTML (Folium) com: Gera um único HTML com título "Planejamento de Diagonal de Manutenção" contendo:
- Marcadores para cada base operacional - Gantt (Plotly): calendário aeronave × tempo com voos e inspeções
- Rotas por aeronave (cor distinta por EVAM) - Mapa (Folium): rotas por aeronave, missões não cumpridas, bases
- Missões não cumpridas em cinza tracejado
- Popup com detalhes de cada missão
Uso: Uso:
python software/visualizar_resultado.py \ python software/visualizar_resultado.py \
--resultado db/processed/resultado_oamrp_sintetico.csv \ --resultado db/processed/resultado_oamrp_sintetico.csv \
--saida db/processed/mapa_rotas.html --saida db/processed/planejamento.html
""" """
import argparse import argparse
import csv import csv
import io
from datetime import datetime, timedelta, timezone
from html import escape
from pathlib import Path from pathlib import Path
import folium import folium
from folium import plugins
BASE_DIR = Path(__file__).resolve().parents[1] BASE_DIR = Path(__file__).resolve().parents[1]
RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv" 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) # Coordenadas das bases (lat, lon)
BASES_COORDS: dict[str, tuple[float, float]] = { BASES_COORDS: dict[str, tuple[float, float]] = {
@@ -39,7 +39,6 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMY": (-5.81, -61.28), "SBMY": (-5.81, -61.28),
} }
# Nomes legíveis das bases
BASES_NOMES = { BASES_NOMES = {
"SBMN": "Manaus / Ponta Pelada (Hub)", "SBMN": "Manaus / Ponta Pelada (Hub)",
"SBBE": "Belém", "SBBE": "Belém",
@@ -53,21 +52,39 @@ BASES_NOMES = {
"SBMY": "Manicoré", "SBMY": "Manicoré",
} }
# Paleta de cores por aeronave
CORES_AERONAVE = { CORES_AERONAVE = {
"2800": "#1f77b4", # azul "2800": "#1f77b4",
"2803": "#2ca02c", # verde "2803": "#2ca02c",
"2809": "#d62728", # vermelho "2809": "#d62728",
"2811": "#ff7f0e", # laranja "2811": "#ff7f0e",
} }
COR_NAO_CUMPRIDA = "#aaaaaa" 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: def _cor_prio(prio: str) -> str:
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"} return {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}.get(str(prio), "white")
return mapa.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]: def carregar_resultado(caminho: Path) -> list[dict]:
@@ -75,57 +92,49 @@ def carregar_resultado(caminho: Path) -> list[dict]:
return list(csv.DictReader(f)) return list(csv.DictReader(f))
def adicionar_bases(mapa: folium.Map) -> None: def _resumir_resultado(linhas: list[dict]) -> None:
for icao, (lat, lon) in BASES_COORDS.items(): cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
cor = COR_HUB if icao == "SBMN" else COR_BASE total_missoes = sum(1 for r in linhas if r["status"] in ("cumprida", "nao_cumprida"))
icone = folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa") print(f" Missões: {cumpridas}/{total_missoes} cumpridas")
folium.Marker( aeronaves: dict[str, int] = {}
location=[lat, lon], for r in linhas:
tooltip=f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", if r["aeronave"] and r["status"] == "cumprida":
popup=folium.Popup(f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", max_width=200), aeronaves.setdefault(r["aeronave"], 0)
icon=icone, aeronaves[r["aeronave"]] += 1
).add_to(mapa) 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( def _construir_mapa(linhas: list[dict]) -> str:
location=[-3.5, -62.0], mapa = folium.Map(location=[-3.5, -62.0], zoom_start=5, tiles="CartoDB positron")
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"]}) aeronaves_vistas = sorted({r["aeronave"] for r in linhas if r["aeronave"]})
grupos: dict[str, folium.FeatureGroup] = {} grupos: dict[str, folium.FeatureGroup] = {}
for k in aeronaves_vistas: 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) grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True)
cumpridas = [r for r in linhas if r["status"] == "cumprida"] for r in linhas:
nao_cumpridas = [r for r in linhas if r["status"] != "cumprida"] if r["status"] != "cumprida":
continue
# Rotas cumpridas orig, dest = r["orig"], r["dest"]
for r in cumpridas:
orig = r["orig"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS: if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
k = r["aeronave"] k = r["aeronave"]
cor = CORES_AERONAVE.get(k, "#333333") 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( folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]], locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=cor, color=cor, weight=2.5, opacity=0.8,
weight=2.5, tooltip=f"FAB {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
opacity=0.8,
tooltip=f"EVAM {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
popup=folium.Popup( 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"{orig}{dest}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>" f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z<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, max_width=250,
), ),
).add_to(grupos[k]) ).add_to(grupos[k])
# Marcador de origem (pequeno círculo)
folium.CircleMarker( folium.CircleMarker(
location=[lat_o, lon_o], location=[lat_o, lon_o], radius=4, color=cor,
radius=4, fill=True, fill_color=_cor_prio(r["prioridade"]), fill_opacity=0.9,
color=cor,
fill=True,
fill_color=_cor_prio(r["prioridade"]),
fill_opacity=0.9,
tooltip=f"{orig} | prio {r['prioridade']}", tooltip=f"{orig} | prio {r['prioridade']}",
).add_to(grupos[k]) ).add_to(grupos[k])
# Missões não cumpridas (linha tracejada cinza) for r in linhas:
for r in nao_cumpridas: if r["status"] == "nao_cumprida":
orig = r["orig"] orig, dest = r["orig"], r["dest"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS: if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue continue
lat_o, lon_o = BASES_COORDS[orig] lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest] lat_d, lon_d = BASES_COORDS[dest]
folium.PolyLine( folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]], locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=COR_NAO_CUMPRIDA, color=COR_NAO_CUMPRIDA, weight=1.5, opacity=0.5, dash_array="6 4",
weight=1.5,
opacity=0.5,
dash_array="6 4",
tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}", tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
).add_to(grupos["nao_cumprida"]) ).add_to(grupos["nao_cumprida"])
# Adiciona grupos ao mapa
for g in grupos.values(): for g in grupos.values():
g.add_to(mapa) 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) folium.LayerControl(collapsed=False).add_to(mapa)
# Legenda HTML
legenda_html = """ legenda_html = """
<div style="position:fixed;bottom:30px;left:30px;z-index:1000;background:white; <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); 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 += ( legenda_html += (
f' <span style="display:inline-block;width:16px;height:4px;' f' <span style="display:inline-block;width:16px;height:4px;'
f'background:{cor};margin-right:6px;vertical-align:middle;"></span>' f'background:{cor};margin-right:6px;vertical-align:middle;"></span>'
f'EVAM {k}<br>' f'FAB {k}<br>'
) )
legenda_html += """ legenda_html += """
<br><b>Prioridade (preenchimento):</b><br> <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)) mapa.get_root().html.add_child(folium.Element(legenda_html))
saida.parent.mkdir(parents=True, exist_ok=True) return mapa.get_root().render()
mapa.save(str(saida))
print(f" Mapa salvo: {saida}")
_resumir_resultado(linhas)
def _resumir_resultado(linhas: list[dict]) -> None: # ---------------------------------------------------------------------------
total = len(linhas) # Gantt Plotly → string HTML (só o div, sem <html>)
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida") # ---------------------------------------------------------------------------
print(f" Missões: {cumpridas}/{total} cumpridas")
aeronaves = {} 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: for r in linhas:
if r["aeronave"]: k = r["aeronave"]
aeronaves.setdefault(r["aeronave"], 0) if not k or k not in y_pos:
aeronaves[r["aeronave"]] += 1 continue
for k, c in sorted(aeronaves.items()): try:
print(f" EVAM {k}: {c} missões") 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: def main() -> None:
parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105") parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105")
parser.add_argument("--resultado", type=Path, default=RESULTADO_PADRAO) 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() args = parser.parse_args()
gerar_mapa(resultado=args.resultado, saida=args.saida) gerar_planejamento(resultado=args.resultado, saida=args.saida)
if __name__ == "__main__": if __name__ == "__main__":