""" Visualização interativa do resultado OAMRP — Esquadrão Arara C-105. 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/planejamento.html """ import argparse import csv import io from datetime import datetime, timedelta, timezone from html import escape from pathlib import Path import folium BASE_DIR = Path(__file__).resolve().parents[1] RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv" SAIDA_PADRAO = BASE_DIR / "db" / "processed" / "planejamento.html" # Coordenadas das bases (lat, lon) BASES_COORDS: dict[str, tuple[float, float]] = { "SBMN": (-3.15, -59.99), "SBBE": (-1.38, -48.48), "SBBV": ( 2.84, -60.69), "SBSN": (-2.42, -54.79), "SBPV": (-8.71, -63.90), "SBTS": (-4.25, -69.94), "SBTT": (-3.38, -64.72), "SBUA": (-0.15, -67.05), "SWBC": (-0.98, -62.92), "SBMY": (-5.81, -61.28), } BASES_NOMES = { "SBMN": "Manaus / Ponta Pelada (Hub)", "SBBE": "Belém", "SBBV": "Boa Vista", "SBSN": "Santarém", "SBPV": "Porto Velho", "SBTS": "Tabatinga", "SBTT": "Tefé", "SBUA": "São Gabriel da Cachoeira", "SWBC": "Barcelos", "SBMY": "Manicoré", } CORES_AERONAVE = { "2800": "#1f77b4", "2803": "#2ca02c", "2809": "#d62728", "2811": "#ff7f0e", } COR_NAO_CUMPRIDA = "#aaaaaa" _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: 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]: with caminho.open(newline="", encoding="utf-8-sig") as f: return list(csv.DictReader(f)) 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") # --------------------------------------------------------------------------- # Mapa Folium → string HTML # --------------------------------------------------------------------------- def _construir_mapa(linhas: list[dict]) -> str: mapa = folium.Map(location=[-3.5, -62.0], zoom_start=5, tiles="CartoDB positron") 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"FAB {k}", show=True) grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True) 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 k = r["aeronave"] cor = CORES_AERONAVE.get(k, "#333333") 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, weight=2.5, opacity=0.8, tooltip=f"FAB {k} | {orig}→{dest} | OM {r['om']} | prio {r['prioridade']}", popup=folium.Popup( f"FAB {k} — Missão #{r['ordem']}
" f"{orig} → {dest}
" f"OM: {r['om']} | Prio: {r['prioridade']}
" f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z
" f"Chegada: {r['chegada_utc'][:16].replace('T',' ')}Z
" f"Duração: {r['dur_h']}h", max_width=250, ), ).add_to(grupos[k]) folium.CircleMarker( 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]) 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"]) for g in grupos.values(): g.add_to(mapa) for icao, (lat, lon) in BASES_COORDS.items(): folium.Marker( location=[lat, lon], tooltip=f"{icao}
{BASES_NOMES.get(icao, '')}", popup=folium.Popup(f"{icao}
{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 = """
Esquadrão Arara — OAMRP v3

Aeronaves:
""" for k, cor in CORES_AERONAVE.items(): legenda_html += ( f' ' f'FAB {k}
' ) legenda_html += """
Prioridade (preenchimento):
Prio 1 (urgente)
Prio 2
Prio 3
Prio 4–5

Não cumprida
""" mapa.get_root().html.add_child(folium.Element(legenda_html)) return mapa.get_root().render() # --------------------------------------------------------------------------- # Gantt Plotly → string HTML (só o div, sem ) # --------------------------------------------------------------------------- def _construir_gantt(linhas: list[dict]) -> str: try: import plotly.graph_objects as go except ImportError: return "

plotly não instalado — instale com: pip install plotly

" 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 = [] bar_traces = [] for r in linhas: 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 ini_dt = datetime.fromisoformat(ini) fim_dt = datetime.fromisoformat(fim) if (fim_dt - ini_dt).total_seconds() < 2 * 3600: fim_dt = ini_dt + timedelta(hours=2) yi = y_pos[k] cor = _cor_gantt(r) status = r["status"] if status == "inspecao": txt = ( f"FAB {k} — Inspeção {r['om']}
" f"Início: {ini[:16].replace('T',' ')}Z
" f"Fim: {fim[:16].replace('T',' ')}Z
" f"Duração: {r['dur_h']}h" ) else: txt = ( f"FAB {k} — Missão #{r['ordem']}
" f"{r['orig']} → {r['dest']}
" f"OM: {r['om']} | Prio: {r['prioridade']}
" f"Partida: {ini[:16].replace('T',' ')}Z
" f"Chegada: {fim[:16].replace('T',' ')}Z
" f"Duração: {r['dur_h']}h" ) # Barra como linha horizontal grossa — renderiza garantido no eixo de datas bar_traces.append(go.Scatter( x=[ini_dt, fim_dt], y=[yi, yi], mode="lines", line=dict(color=cor, width=64), 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 bar_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, range=[-0.5, len(aeronaves_ord) - 0.5], ), 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'' for v in valores: html += f'' return html return f"""
Aeronave Status #Missão OM Origem Destino Partida (UTC) Chegada (UTC) Duração (h) Prio
""" # --------------------------------------------------------------------------- # 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""" Planejamento de Diagonal de Manutenção

Planejamento de Diagonal de Manutenção

Esquadrão Arara · C-105 · OAMRP v3

Calendário de Emprego das Aeronaves
{gantt_div}
Rotas Operacionais
Planejamento de Voos
{tabela_div}
""" 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=SAIDA_PADRAO) args = parser.parse_args() gerar_planejamento(resultado=args.resultado, saida=args.saida) if __name__ == "__main__": main()