diff --git a/software/oamrp_v3.py b/software/oamrp_v3.py
index e2de218..919c94a 100644
--- a/software/oamrp_v3.py
+++ b/software/oamrp_v3.py
@@ -787,6 +787,8 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
missoes_nos = sol["missoes_nos"]
linhas = []
+ horizonte_dt = sol.get("horizonte_dt")
+
cobertas_ids: set[str] = set()
for k in sol["ks"]:
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,
})
+ # 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():
if nid in cobertas_ids:
continue
@@ -1406,6 +1431,7 @@ def main() -> None:
nos, arcos = construir_rede(missoes, aeronaves, catalogo)
sol = resolver(nos, arcos, aeronaves, catalogo)
+ sol["horizonte_dt"] = horizonte_dt
escrever_resultado(sol, args.resultado)
imprimir_relatorio(sol, bases, args.resultado)
diff --git a/software/visualizar_resultado.py b/software/visualizar_resultado.py
index 9b30d75..5b7b716 100644
--- a/software/visualizar_resultado.py
+++ b/software/visualizar_resultado.py
@@ -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"{icao}
{BASES_NOMES.get(icao, '')}",
- popup=folium.Popup(f"{icao}
{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"EVAM {k} — Missão #{r['ordem']}
"
+ 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
"
@@ -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"{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
legenda_html = """
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 = [] + 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"FAB {k} — Inspeção {r['om']}| Aeronave | +Status | +#Missão | +OM | +Origem | +Destino | +Partida (UTC) | +Chegada (UTC) | +Duração (h) | +Prio | +
|---|
Esquadrão Arara · C-105 · OAMRP v3
+