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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
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",
|
||||
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__":
|
||||
|
||||
Reference in New Issue
Block a user