Quazim0t0's picture
Upload 34 files
41e0c9e verified
Raw
History Blame Contribute Delete
6.41 kB
"""Builds a self-contained, transparent 3D globe (Globe.gl / Three.js) as an
iframe srcdoc, with flight points and animated estimated-path arcs."""
from __future__ import annotations
import base64
import json
_TEMPLATE = r"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style>
html, body { margin:0; height:100%; background:#02060a; overflow:hidden;
font-family: 'Courier New', monospace; }
#globeViz { width:100%; height:100%; }
.scene-tooltip { color:#7CFFB2; text-shadow:0 0 6px #00ff88; font-size:12px; }
#hud {
position:absolute; top:10px; left:12px; color:#39FF14;
text-shadow:0 0 8px #39FF14; font-size:12px; letter-spacing:1px;
pointer-events:none; line-height:1.5;
}
#legend {
position:absolute; bottom:10px; right:12px; color:#7CFFB2;
text-shadow:0 0 6px #00ff88; font-size:11px; text-align:right; line-height:1.6;
}
/* green cube marker = a flight's current position */
.fcube {
width:11px; height:11px; background:#39FF14;
border:1px solid #d6ffd6; cursor:pointer;
box-shadow:0 0 9px #39FF14, 0 0 3px #39FF14,
inset -2px -2px 0 rgba(0,70,0,0.65),
inset 2px 2px 0 rgba(210,255,210,0.55);
}
/* grey cube marker = the destination airport */
.fcube.dest {
background:#9aa0a8; border-color:#e2e6ea;
box-shadow:0 0 9px #aab0b8, 0 0 3px #cfd4da,
inset -2px -2px 0 rgba(40,45,50,0.7),
inset 2px 2px 0 rgba(230,234,238,0.6);
}
</style>
<script src="https://unpkg.com/globe.gl@2.32.0/dist/globe.gl.min.js"></script>
</head>
<body>
<div id="globeViz"></div>
<div id="hud">
&gt;&gt; FLIGHTDECK // LIVE TELEMETRY<br/>
TARGETS: <span id="count">0</span><br/>
<span id="ts"></span>
</div>
<div id="legend">
<span style="color:#39FF14">▣</span> flight &nbsp;
<span style="color:#cfd4da">▣</span> destination<br/>
╌╌ path to destination
</div>
<script>
const DATA = /*__DATA__*/;
const world = Globe()(document.getElementById('globeViz'))
.backgroundColor('rgba(0,0,0,0)')
.globeImageUrl('https://unpkg.com/three-globe/example/img/earth-night.jpg')
.showGraticules(true)
.showAtmosphere(true)
.atmosphereColor('#19ff8a')
.atmosphereAltitude(0.3)
.htmlElementsData(DATA.points)
.htmlLat('lat').htmlLng('lng')
.htmlAltitude('alt')
.htmlElement(d => {
const el = document.createElement('div');
el.className = 'fcube' + (d.kind === 'dest' ? ' dest' : '');
el.title = d.label;
return el;
})
.arcsData(DATA.arcs)
.arcStartLat('sLat').arcStartLng('sLng')
.arcEndLat('eLat').arcEndLng('eLng')
.arcColor('color')
.arcStroke(0.4)
.arcDashLength(0.4)
.arcDashGap(0.2)
.arcDashInitialGap(() => Math.random())
.arcDashAnimateTime(2600)
.arcAltitudeAutoScale(0.4)
.arcLabel('label');
// Slightly transparent, neon-tinted globe (kept visible even if the texture
// is slow: a dark-teal base + emissive glow + graticule grid define the sphere).
const mat = world.globeMaterial();
mat.transparent = true;
mat.opacity = 0.9;
if (mat.color) mat.color.set('#0a1f17');
if (mat.emissive) { mat.emissive.set('#0a3a24'); mat.emissiveIntensity = 0.55; }
// User can spin/drag/zoom freely; idle auto-rotation when not interacting.
const ctrl = world.controls();
ctrl.autoRotate = true;
ctrl.autoRotateSpeed = 0.35;
ctrl.enableZoom = true;
ctrl.enablePan = false;
// As soon as the user grabs/drags (or zooms) the globe, stop the idle spin.
ctrl.addEventListener('start', () => { ctrl.autoRotate = false; });
document.getElementById('count').textContent = DATA.points.length;
document.getElementById('ts').textContent = DATA.stamp || '';
// Search fly-in: start wide, then TURN + MAGNIFY onto the target location.
if (DATA.focus) {
ctrl.autoRotate = false; // pause idle spin during the fly-in
world.pointOfView({lat: 5, lng: DATA.focus.lng, altitude: 3.2}, 0); // wide
setTimeout(() => {
world.pointOfView(
{lat: DATA.focus.lat, lng: DATA.focus.lng, altitude: 1.25}, 2600); // zoom in
}, 250);
setTimeout(() => { ctrl.autoRotate = true; }, 3200); // resume gentle spin
}
addEventListener('resize', () => {
world.width(innerWidth); world.height(innerHeight);
});
world.width(innerWidth); world.height(innerHeight);
</script>
</body>
</html>"""
def build_globe_html(flights, stamp="", focus=None) -> str:
"""flights: list of dicts from app.normalize_flight(). Returns an iframe."""
points, arcs = [], []
dest_seen = {} # dedup destination cubes by rounded coordinate
for f in flights:
lat, lon = f.get("lat"), f.get("lon")
if lat is None or lon is None:
continue
alt = f.get("alt")
# Plain-text tooltip (htmlElement uses the title attribute).
label = (
f"{f.get('callsign') or f.get('flight') or '??'} "
f"{f.get('orig') or '?'} -> {f.get('dest') or '?'} | "
f"alt {alt or '?'} ft · {f.get('gspeed') or '?'} kt · "
f"ETA {f.get('eta_human') or '—'}"
)
points.append({
"lat": lat, "lng": lon, "kind": "flight",
"alt": 0.015, # sit just above the surface (a marker, not a pole)
"label": label,
})
ep = f.get("est_path")
if ep:
arcs.append({
"sLat": lat, "sLng": lon,
"eLat": ep[0], "eLng": ep[1],
"color": ["#39FF14", "#aaffcc"],
"label": f"{f.get('callsign') or '??'}{f.get('dest') or '?'} · ETA {f.get('eta_human') or '—'}",
})
# Grey cube marking the destination airport (deduplicated).
key = (round(ep[0], 3), round(ep[1], 3))
if key not in dest_seen:
dest_seen[key] = True
points.append({
"lat": ep[0], "lng": ep[1], "kind": "dest",
"alt": 0.015,
"label": f"DEST {f.get('dest') or '?'}",
})
data = {"points": points, "arcs": arcs, "stamp": stamp, "focus": focus}
html = _TEMPLATE.replace("/*__DATA__*/", json.dumps(data))
b64 = base64.b64encode(html.encode("utf-8")).decode("ascii")
return (
f'<iframe src="data:text/html;base64,{b64}" '
'style="width:100%;height:640px;border:0;border-radius:10px;'
'box-shadow:0 0 24px #0aff9d55, inset 0 0 40px #00000080;"></iframe>'
)