- visualizar_resultado: barras Gantt via go.Scatter lines (renderizacao garantida), yaxis range fixo para FAB 2800 visivel, largura minima 2h - oamrp_v3: parser --escala para Escala de Voo Modelo 1 (CSV real), coordenadas de 26 bases, time-limit default 5min, imports timedelta/date Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
626 lines
22 KiB
Python
626 lines
22 KiB
Python
"""
|
||
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"<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>"
|
||
f"Chegada: {r['chegada_utc'][:16].replace('T',' ')}Z<br>"
|
||
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"<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 = """
|
||
<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'FAB {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))
|
||
|
||
return mapa.get_root().render()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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 = []
|
||
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"<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"
|
||
)
|
||
|
||
# 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'<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=SAIDA_PADRAO)
|
||
args = parser.parse_args()
|
||
gerar_planejamento(resultado=args.resultado, saida=args.saida)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|