Correções ao modelo: - Adiciona conservação de fluxo em nós de inspeção (bug que permitia criação/destruição de unidades de fluxo, inflando cobertura para 50/50 sem atribuição real de rotas) - Remove restrição C6 simplificada que somava horas totais sem respeitar resets de inspeção, tornando 2809/2811 artificialmente ociosos Nomenclatura alinhada ao Al-Thani (2016): - InspecaoParam.lrt_h → f_max (F do artigo: batente máximo legal) - Aeronave.horas_iniciais → f0 (f_k: horas acumuladas no início) - orcamento_h() → lrt_inicial() (LRT = F_max − f0) - horas_iniciais_aleatorias() → gerar_f0_aleatorio() - carregar_horas_iniciais() → carregar_f0() - CLI --horas-iniciais → --f0 Novos arquivos: - software/gerar_ofrag.py: gera tabela de OFRAGs com prioridades 1-5 - software/visualizar_resultado.py: mapa Folium interativo de rotas - db/processed/ofrag.csv: 50 OFRAGs sintéticas (seed 42) - db/processed/mapa_rotas.html: mapa gerado da última rodada Resultado com --sintetico --aleatorio --ofrag: 50/50 missões cumpridas (antes: 30/50 sem ferry, 7/50 com bug de fluxo) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
"""
|
||
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
|
||
|
||
Uso:
|
||
python software/visualizar_resultado.py \
|
||
--resultado db/processed/resultado_oamrp_sintetico.csv \
|
||
--saida db/processed/mapa_rotas.html
|
||
"""
|
||
|
||
import argparse
|
||
import csv
|
||
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"
|
||
|
||
# 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),
|
||
}
|
||
|
||
# Nomes legíveis das bases
|
||
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é",
|
||
}
|
||
|
||
# Paleta de cores por aeronave
|
||
CORES_AERONAVE = {
|
||
"2800": "#1f77b4", # azul
|
||
"2803": "#2ca02c", # verde
|
||
"2809": "#d62728", # vermelho
|
||
"2811": "#ff7f0e", # laranja
|
||
}
|
||
COR_NAO_CUMPRIDA = "#aaaaaa"
|
||
COR_HUB = "#d62728"
|
||
COR_BASE = "#1f77b4"
|
||
|
||
|
||
def _cor_prio(prio: str) -> str:
|
||
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}
|
||
return mapa.get(str(prio), "white")
|
||
|
||
|
||
def carregar_resultado(caminho: Path) -> list[dict]:
|
||
with caminho.open(newline="", encoding="utf-8-sig") as f:
|
||
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 gerar_mapa(resultado: Path, saida: Path) -> None:
|
||
linhas = carregar_resultado(resultado)
|
||
|
||
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["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"]
|
||
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")
|
||
|
||
# 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']}",
|
||
popup=folium.Popup(
|
||
f"<b>EVAM {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>"
|
||
f"Chegada: {r['chegada_utc'][:16].replace('T',' ')}Z<br>"
|
||
f"Duração: {r['dur_h']}h",
|
||
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,
|
||
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"])
|
||
|
||
# Adiciona grupos ao mapa
|
||
for g in grupos.values():
|
||
g.add_to(mapa)
|
||
|
||
adicionar_bases(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);
|
||
font-family:sans-serif;font-size:12px;">
|
||
<b>Esquadrão Arara — OAMRP v3</b><br><br>
|
||
<b>Aeronaves:</b><br>
|
||
"""
|
||
for k, cor in CORES_AERONAVE.items():
|
||
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>'
|
||
)
|
||
legenda_html += """
|
||
<br><b>Prioridade (preenchimento):</b><br>
|
||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||
background:red;margin-right:6px;"></span>Prio 1 (urgente)<br>
|
||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||
background:orange;margin-right:6px;"></span>Prio 2<br>
|
||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||
background:yellow;margin-right:6px;"></span>Prio 3<br>
|
||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||
background:lightblue;margin-right:6px;"></span>Prio 4–5<br>
|
||
<br>
|
||
<span style="border-bottom:2px dashed #aaa;display:inline-block;width:20px;
|
||
margin-right:6px;"></span>Não cumprida
|
||
</div>
|
||
"""
|
||
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)
|
||
|
||
|
||
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 = {}
|
||
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")
|
||
|
||
|
||
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)
|
||
args = parser.parse_args()
|
||
gerar_mapa(resultado=args.resultado, saida=args.saida)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|