Merge pull request 'feat(map): aircraft popup enrichment and emergency/MIL styling' (#35) from osint-dashboard/t_1d9c5839-osint-map-aircraft-popup-enrichment-emer into master
All checks were successful
build-and-deploy / build-push-deploy (push) Successful in 18s
All checks were successful
build-and-deploy / build-push-deploy (push) Successful in 18s
Reviewed-on: #35
This commit is contained in:
commit
f072a83ca8
2 changed files with 149 additions and 19 deletions
|
|
@ -236,6 +236,16 @@
|
||||||
filter: drop-shadow(0 0 4px currentColor) drop-shadow(0 1px 2px rgba(0,0,0,0.8));
|
filter: drop-shadow(0 0 4px currentColor) drop-shadow(0 1px 2px rgba(0,0,0,0.8));
|
||||||
}
|
}
|
||||||
.hdg-glyph svg { width: 100%; height: 100%; display: block; }
|
.hdg-glyph svg { width: 100%; height: 100%; display: block; }
|
||||||
|
.hdg-emerg {
|
||||||
|
animation: emerg-pulse 1.15s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes emerg-pulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 4px #ff5d5d); }
|
||||||
|
50% { filter: drop-shadow(0 0 12px #ff5d5d) drop-shadow(0 0 18px #ff5d5d); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.hdg-emerg, .role-badge.emergency { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Camera marker clusters (neon green, matches the dots) ──── */
|
/* ── Camera marker clusters (neon green, matches the dots) ──── */
|
||||||
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large {
|
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large {
|
||||||
|
|
@ -334,6 +344,7 @@
|
||||||
.lp-dot.news { background: var(--magenta); box-shadow: 0 0 7px var(--magenta); border-radius: 1px; transform: rotate(45deg); }
|
.lp-dot.news { background: var(--magenta); box-shadow: 0 0 7px var(--magenta); border-radius: 1px; transform: rotate(45deg); }
|
||||||
.lp-dot.weather { background: var(--amber); box-shadow: 0 0 7px var(--amber); }
|
.lp-dot.weather { background: var(--amber); box-shadow: 0 0 7px var(--amber); }
|
||||||
.lp-dot.flights { background: #7dd3fc; box-shadow: 0 0 7px #7dd3fc; }
|
.lp-dot.flights { background: #7dd3fc; box-shadow: 0 0 7px #7dd3fc; }
|
||||||
|
.lp-dot.mil { background: #f472b6; box-shadow: 0 0 7px #f472b6; }
|
||||||
.lp-dot.vessels { background: #2dd4bf; box-shadow: 0 0 7px #2dd4bf; }
|
.lp-dot.vessels { background: #2dd4bf; box-shadow: 0 0 7px #2dd4bf; }
|
||||||
.lp-dot.radar { background: #38bdf8; box-shadow: 0 0 7px #38bdf8; }
|
.lp-dot.radar { background: #38bdf8; box-shadow: 0 0 7px #38bdf8; }
|
||||||
.lp-dot.thermal { background: #f97316; box-shadow: 0 0 7px #f97316; }
|
.lp-dot.thermal { background: #f97316; box-shadow: 0 0 7px #f97316; }
|
||||||
|
|
@ -393,6 +404,10 @@
|
||||||
.role-badge.civilian { color: #7dd3fc; border: 1px solid #38bdf8; background: rgba(56,189,248,0.1); }
|
.role-badge.civilian { color: #7dd3fc; border: 1px solid #38bdf8; background: rgba(56,189,248,0.1); }
|
||||||
.role-badge.government { color: #facc15; border: 1px solid #facc15; background: rgba(250,204,21,0.12); }
|
.role-badge.government { color: #facc15; border: 1px solid #facc15; background: rgba(250,204,21,0.12); }
|
||||||
.role-badge.firefighter { color: #fb923c; border: 1px solid #fb923c; background: rgba(251,146,60,0.12); }
|
.role-badge.firefighter { color: #fb923c; border: 1px solid #fb923c; background: rgba(251,146,60,0.12); }
|
||||||
|
.role-badge.emergency {
|
||||||
|
color: #ff5d5d; border: 1px solid #ff5d5d; background: rgba(255,93,93,0.16);
|
||||||
|
animation: emerg-pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
.blip-pop { min-width: 200px; max-width: 260px; }
|
.blip-pop { min-width: 200px; max-width: 260px; }
|
||||||
.blip-pop .blip-src { font-family: 'Share Tech Mono', monospace; font-size: 0.62rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.08em; }
|
.blip-pop .blip-src { font-family: 'Share Tech Mono', monospace; font-size: 0.62rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.08em; }
|
||||||
.blip-pop .blip-time { font-size: 0.7rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; margin: 0.2rem 0 0.3rem; }
|
.blip-pop .blip-time { font-size: 0.7rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; margin: 0.2rem 0 0.3rem; }
|
||||||
|
|
@ -893,7 +908,11 @@
|
||||||
<label class="lp-name"><input type="checkbox" id="lp-ac-on" checked onchange="toggleAircraft()"> <span class="lp-dot flights"></span> Aircraft</label>
|
<label class="lp-name"><input type="checkbox" id="lp-ac-on" checked onchange="toggleAircraft()"> <span class="lp-dot flights"></span> Aircraft</label>
|
||||||
<span class="lp-count" id="lp-ac-count">0</span>
|
<span class="lp-count" id="lp-ac-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lp-note">Magenta = military · orange = firefighting · else altitude. Click a plane for type / squawk / operator.</div>
|
<div class="lp-row" id="lp-ac-mil-row" hidden>
|
||||||
|
<label class="lp-name"><input type="checkbox" id="lp-ac-mil-on" onchange="toggleAircraftMil()"> <span class="lp-dot mil"></span> MIL</label>
|
||||||
|
<span class="lp-count" id="lp-ac-mil-count"></span>
|
||||||
|
</div>
|
||||||
|
<div class="lp-note">Magenta = military · orange = firefighting · red = emergency (7700/7600/7500) · else altitude. Click a plane for type / squawk / operator.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lp-layer">
|
<div class="lp-layer">
|
||||||
<div class="lp-row">
|
<div class="lp-row">
|
||||||
|
|
@ -1919,7 +1938,7 @@ let sentinelItemId = null, sentinelBounds = null;
|
||||||
let wxAlertsGroup = null, wxAlertsOn = true;
|
let wxAlertsGroup = null, wxAlertsOn = true;
|
||||||
let perimGroup = null, perimOn = true;
|
let perimGroup = null, perimOn = true;
|
||||||
let incidentsGroup = null, incidentsOn = false;
|
let incidentsGroup = null, incidentsOn = false;
|
||||||
let acGroup = null, acOn = true;
|
let acGroup = null, acOn = true, acMilOn = false, acMilSupported = false;
|
||||||
let trainsGroup = null, trainsOn = true;
|
let trainsGroup = null, trainsOn = true;
|
||||||
let vesselsGroup = null, vesselsOn = false;
|
let vesselsGroup = null, vesselsOn = false;
|
||||||
let stormsGroup = null, stormsOn = true;
|
let stormsGroup = null, stormsOn = true;
|
||||||
|
|
@ -1950,6 +1969,14 @@ function sendLiveViewport() {
|
||||||
if (!liveWs || liveWs.readyState !== 1 || !map) return;
|
if (!liveWs || liveWs.readyState !== 1 || !map) return;
|
||||||
liveWs.send(JSON.stringify({ type: 'viewport', bbox: currentBBox() }));
|
liveWs.send(JSON.stringify({ type: 'viewport', bbox: currentBBox() }));
|
||||||
}
|
}
|
||||||
|
function dropLivePoint(group, id) {
|
||||||
|
if (!group || !group._osintById || id == null) return;
|
||||||
|
const key = String(id);
|
||||||
|
const m = group._osintById.get(key);
|
||||||
|
if (!m) return;
|
||||||
|
group.removeLayer(m);
|
||||||
|
group._osintById.delete(key);
|
||||||
|
}
|
||||||
function upsertLivePoint(group, p, colorFn, feed) {
|
function upsertLivePoint(group, p, colorFn, feed) {
|
||||||
if (!map || !p || p.id == null || p.lat == null || p.lon == null) return group;
|
if (!map || !p || p.id == null || p.lat == null || p.lon == null) return group;
|
||||||
if (!group || !map.hasLayer(group) || !group._osintById) {
|
if (!group || !map.hasLayer(group) || !group._osintById) {
|
||||||
|
|
@ -1960,7 +1987,7 @@ function upsertLivePoint(group, p, colorFn, feed) {
|
||||||
let m = group._osintById.get(id);
|
let m = group._osintById.get(id);
|
||||||
if (m) {
|
if (m) {
|
||||||
m.setLatLng([p.lat, p.lon]);
|
m.setLatLng([p.lat, p.lon]);
|
||||||
if (feed) m.setIcon(feedIcon(feed, col, p.heading));
|
if (feed) m.setIcon(feedIcon(feed, col, p.heading, feedPulse(feed, p)));
|
||||||
else if (m.setStyle) m.setStyle({ color: col, fillColor: col });
|
else if (m.setStyle) m.setStyle({ color: col, fillColor: col });
|
||||||
} else {
|
} else {
|
||||||
m = makePointMarker(p, colorFn, feed, pointCanvas());
|
m = makePointMarker(p, colorFn, feed, pointCanvas());
|
||||||
|
|
@ -1976,24 +2003,48 @@ function applyLiveMarker(kind, p) {
|
||||||
return sog > 0.5 ? '#2dd4bf' : '#64748b';
|
return sog > 0.5 ? '#2dd4bf' : '#64748b';
|
||||||
}, 'vessel');
|
}, 'vessel');
|
||||||
} else if (kind === 'adsb' && acOn && map && map.getZoom() > 3 && !dvrTs) {
|
} else if (kind === 'adsb' && acOn && map && map.getZoom() > 3 && !dvrTs) {
|
||||||
|
if (!acVisible(p)) {
|
||||||
|
dropLivePoint(acGroup, p.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
acGroup = upsertLivePoint(acGroup, p, q => acColor(q), 'ac');
|
acGroup = upsertLivePoint(acGroup, p, q => acColor(q), 'ac');
|
||||||
} else if (kind === 'geofence_alert') {
|
} else if (kind === 'geofence_alert') {
|
||||||
showGeofenceToast(p);
|
showGeofenceToast(p);
|
||||||
} else if (kind === 'fire_aircraft') {
|
} else if (kind === 'fire_aircraft') {
|
||||||
firefighterHex.add(String(p.aircraft_hex || ''));
|
firefighterHex.add(String(p.aircraft_hex || ''));
|
||||||
if (acOn && p.aircraft_lat != null) {
|
if (acOn && p.aircraft_lat != null) {
|
||||||
acGroup = upsertLivePoint(acGroup, {
|
const row = {
|
||||||
id: p.aircraft_hex, lat: p.aircraft_lat, lon: p.aircraft_lon,
|
id: p.aircraft_hex, lat: p.aircraft_lat, lon: p.aircraft_lon,
|
||||||
label: p.label || p.aircraft_hex, extra: { type: p.aircraft_type, firefighter: true, hex: p.aircraft_hex, src: 'adsb.lol' },
|
label: p.label || p.aircraft_hex, extra: { type: p.aircraft_type, firefighter: true, hex: p.aircraft_hex, src: 'adsb.lol' },
|
||||||
}, q => acColor(q), 'ac');
|
};
|
||||||
|
if (!acVisible(row)) dropLivePoint(acGroup, row.id);
|
||||||
|
else acGroup = upsertLivePoint(acGroup, row, q => acColor(q), 'ac');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const EMERG_SQUAWK = new Set(['7700', '7600', '7500']);
|
||||||
|
function acIsEmergency(p) {
|
||||||
|
const extra = (p && p.extra) || {};
|
||||||
|
const em = String(extra.emergency || '').toLowerCase();
|
||||||
|
if (em && em !== 'none') return true;
|
||||||
|
const sq = String(extra.squawk || '').replace(/\s/g, '');
|
||||||
|
return EMERG_SQUAWK.has(sq);
|
||||||
|
}
|
||||||
|
function acVisible(p) {
|
||||||
|
if (!acMilOn) return true;
|
||||||
|
return ((p && p.extra) || {}).role === 'military';
|
||||||
|
}
|
||||||
|
function noteMilSupport(pts) {
|
||||||
|
if (acMilSupported) return;
|
||||||
|
if (!Array.isArray(pts) || !pts.some(q => q && q.extra && q.extra.role)) return;
|
||||||
|
acMilSupported = true;
|
||||||
|
const row = document.getElementById('lp-ac-mil-row');
|
||||||
|
if (row) row.hidden = false;
|
||||||
|
}
|
||||||
function acColor(p) {
|
function acColor(p) {
|
||||||
const extra = p.extra || {};
|
const extra = p.extra || {};
|
||||||
const t = String(extra.type || '').toUpperCase();
|
const t = String(extra.type || '').toUpperCase();
|
||||||
const em = String(extra.emergency || '').toLowerCase();
|
if (acIsEmergency(p)) return '#ff5d5d';
|
||||||
if (em && em !== 'none') return '#ff5d5d';
|
|
||||||
if (extra.firefighter || firefighterHex.has(String(p.id)) || FF_ICAO.has(t)) return '#fb923c';
|
if (extra.firefighter || firefighterHex.has(String(p.id)) || FF_ICAO.has(t)) return '#fb923c';
|
||||||
if (extra.role === 'military') return '#f472b6';
|
if (extra.role === 'military') return '#f472b6';
|
||||||
return altColor(extra.alt_baro);
|
return altColor(extra.alt_baro);
|
||||||
|
|
@ -2822,9 +2873,13 @@ function pointPopup(p) {
|
||||||
const extra = p.extra || {};
|
const extra = p.extra || {};
|
||||||
const src = extra.src || '';
|
const src = extra.src || '';
|
||||||
const role = extra.firefighter ? 'firefighter' : extra.role;
|
const role = extra.firefighter ? 'firefighter' : extra.role;
|
||||||
const badge = role
|
let badge = '';
|
||||||
? ` <span class="role-badge ${esc(role)}">${esc(String(role).toUpperCase())}</span>`
|
if (src === 'adsb.lol' && acIsEmergency(p)) {
|
||||||
: '';
|
badge += ' <span class="role-badge emergency">EMERGENCY</span>';
|
||||||
|
}
|
||||||
|
if (role) {
|
||||||
|
badge += ` <span class="role-badge ${esc(role)}">${esc(String(role).toUpperCase())}</span>`;
|
||||||
|
}
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const add = (k, v) => {
|
const add = (k, v) => {
|
||||||
const val = _popVal(v);
|
const val = _popVal(v);
|
||||||
|
|
@ -2832,10 +2887,12 @@ function pointPopup(p) {
|
||||||
rows.push(`<tr><td class="k">${esc(k)}</td><td>${esc(val)}</td></tr>`);
|
rows.push(`<tr><td class="k">${esc(k)}</td><td>${esc(val)}</td></tr>`);
|
||||||
};
|
};
|
||||||
if (src === 'adsb.lol') {
|
if (src === 'adsb.lol') {
|
||||||
|
add('callsign', p.label);
|
||||||
|
add('hex', extra.hex);
|
||||||
|
add('registration', extra.reg);
|
||||||
add('type', extra.type);
|
add('type', extra.type);
|
||||||
add('aircraft', extra.desc);
|
add('aircraft', extra.desc);
|
||||||
add('operator', extra.ownOp);
|
add('operator', extra.ownOp);
|
||||||
add('reg', extra.reg);
|
|
||||||
const alt = extra.alt_baro;
|
const alt = extra.alt_baro;
|
||||||
add('alt', alt != null ? `${alt} ft` : null);
|
add('alt', alt != null ? `${alt} ft` : null);
|
||||||
add('vs', extra.vs != null ? `${extra.vs} fpm` : null);
|
add('vs', extra.vs != null ? `${extra.vs} fpm` : null);
|
||||||
|
|
@ -2844,7 +2901,6 @@ function pointPopup(p) {
|
||||||
add('squawk', extra.squawk);
|
add('squawk', extra.squawk);
|
||||||
add('emergency', extra.emergency);
|
add('emergency', extra.emergency);
|
||||||
add('class', extra.emitter);
|
add('class', extra.emitter);
|
||||||
add('hex', extra.hex);
|
|
||||||
} else if (src === 'aisstream') {
|
} else if (src === 'aisstream') {
|
||||||
add('kind', extra.kind);
|
add('kind', extra.kind);
|
||||||
add('flag', extra.country);
|
add('flag', extra.country);
|
||||||
|
|
@ -2943,20 +2999,24 @@ function sanitizeColor(c, fallback) {
|
||||||
if (/^rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(,\s*[\d.]+\s*)?\)$/.test(s)) return s;
|
if (/^rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(,\s*[\d.]+\s*)?\)$/.test(s)) return s;
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
function feedIcon(feed, color, heading) {
|
function feedPulse(feed, p) {
|
||||||
|
return feed === 'ac' && acIsEmergency(p);
|
||||||
|
}
|
||||||
|
function feedIcon(feed, color, heading, pulse) {
|
||||||
// Normalize heading to [0,360) integer so the cache stays bounded.
|
// Normalize heading to [0,360) integer so the cache stays bounded.
|
||||||
// Missing/empty/NaN heading -> -1 sentinel -> glyph rendered upright.
|
// Missing/empty/NaN heading -> -1 sentinel -> glyph rendered upright.
|
||||||
const hnum = Number(heading);
|
const hnum = Number(heading);
|
||||||
const h = (heading === null || heading === '' || heading === undefined || !Number.isFinite(hnum))
|
const h = (heading === null || heading === '' || heading === undefined || !Number.isFinite(hnum))
|
||||||
? -1
|
? -1
|
||||||
: (Math.round(hnum % 360) + 360) % 360;
|
: (Math.round(hnum % 360) + 360) % 360;
|
||||||
const key = `${feed}|${color}|${h}`;
|
const key = `${feed}|${color}|${h}|${pulse ? 1 : 0}`;
|
||||||
let ic = feedIconCache.get(key);
|
let ic = feedIconCache.get(key);
|
||||||
if (!ic) {
|
if (!ic) {
|
||||||
const rot = h >= 0 ? `transform:rotate(${h}deg);` : '';
|
const rot = h >= 0 ? `transform:rotate(${h}deg);` : '';
|
||||||
|
const pulseCls = pulse ? ' hdg-emerg' : '';
|
||||||
ic = L.divIcon({
|
ic = L.divIcon({
|
||||||
className: '',
|
className: '',
|
||||||
html: `<span class="hdg-marker"><span class="hdg-glyph hdg-${feed}" style="color:${color};${rot}">${FEED_GLYPHS[feed]}</span></span>`,
|
html: `<span class="hdg-marker${pulseCls}"><span class="hdg-glyph hdg-${feed}" style="color:${color};${rot}">${FEED_GLYPHS[feed]}</span></span>`,
|
||||||
iconSize: [26, 26], iconAnchor: [13, 13],
|
iconSize: [26, 26], iconAnchor: [13, 13],
|
||||||
});
|
});
|
||||||
feedIconCache.set(key, ic);
|
feedIconCache.set(key, ic);
|
||||||
|
|
@ -2969,7 +3029,7 @@ function makePointMarker(p, colorFn, feed, renderer) {
|
||||||
let m;
|
let m;
|
||||||
if (feed) {
|
if (feed) {
|
||||||
const heading = Number(p.heading);
|
const heading = Number(p.heading);
|
||||||
const icon = feedIcon(feed, col, Number.isNaN(heading) ? null : heading);
|
const icon = feedIcon(feed, col, Number.isNaN(heading) ? null : heading, feedPulse(feed, p));
|
||||||
m = L.marker([p.lat, p.lon], { icon });
|
m = L.marker([p.lat, p.lon], { icon });
|
||||||
} else {
|
} else {
|
||||||
m = L.circleMarker([p.lat, p.lon], {
|
m = L.circleMarker([p.lat, p.lon], {
|
||||||
|
|
@ -3016,7 +3076,7 @@ function renderPoints(existing, points, colorFn, cluster, feed) {
|
||||||
if (m) {
|
if (m) {
|
||||||
m.setLatLng([p.lat, p.lon]);
|
m.setLatLng([p.lat, p.lon]);
|
||||||
m._osintP = p;
|
m._osintP = p;
|
||||||
if (feed) m.setIcon(feedIcon(feed, col, p.heading));
|
if (feed) m.setIcon(feedIcon(feed, col, p.heading, feedPulse(feed, p)));
|
||||||
else if (m.setStyle) m.setStyle({ color: col, fillColor: col });
|
else if (m.setStyle) m.setStyle({ color: col, fillColor: col });
|
||||||
} else {
|
} else {
|
||||||
const nm = makePointMarker(p, colorFn, feed, renderer);
|
const nm = makePointMarker(p, colorFn, feed, renderer);
|
||||||
|
|
@ -3286,6 +3346,10 @@ async function toggleAircraft() {
|
||||||
if (acOn) await loadAircraft();
|
if (acOn) await loadAircraft();
|
||||||
else acGroup = dropLayer(acGroup);
|
else acGroup = dropLayer(acGroup);
|
||||||
}
|
}
|
||||||
|
function toggleAircraftMil() {
|
||||||
|
acMilOn = document.getElementById('lp-ac-mil-on').checked;
|
||||||
|
if (acOn) loadAircraft();
|
||||||
|
}
|
||||||
async function loadAircraft() {
|
async function loadAircraft() {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
if (map.getZoom() <= 3) {
|
if (map.getZoom() <= 3) {
|
||||||
|
|
@ -3297,8 +3361,17 @@ async function loadAircraft() {
|
||||||
const r = await overlayFetch(`${API}/api/aircraft?bbox=${currentBBox()}${dvrQs()}`);
|
const r = await overlayFetch(`${API}/api/aircraft?bbox=${currentBBox()}${dvrQs()}`);
|
||||||
const pts = await r.json();
|
const pts = await r.json();
|
||||||
if (req !== overlayReq.ac) return;
|
if (req !== overlayReq.ac) return;
|
||||||
acGroup = renderPoints(acGroup, Array.isArray(pts) ? pts : [], p => acColor(p), true, 'ac');
|
const all = Array.isArray(pts) ? pts : [];
|
||||||
document.getElementById('lp-ac-count').textContent = (pts.length || 0).toLocaleString();
|
noteMilSupport(all);
|
||||||
|
const shown = acMilOn ? all.filter(acVisible) : all;
|
||||||
|
acGroup = renderPoints(acGroup, shown, p => acColor(p), true, 'ac');
|
||||||
|
document.getElementById('lp-ac-count').textContent = shown.length.toLocaleString();
|
||||||
|
const milEl = document.getElementById('lp-ac-mil-count');
|
||||||
|
if (milEl) {
|
||||||
|
milEl.textContent = acMilOn
|
||||||
|
? shown.length.toLocaleString()
|
||||||
|
: String(all.filter(q => ((q.extra || {}).role === 'military')).length);
|
||||||
|
}
|
||||||
addExtraAttrib('<a href="https://www.adsb.lol/docs/open-data/api">ADSB.lol</a> ODbL');
|
addExtraAttrib('<a href="https://www.adsb.lol/docs/open-data/api">ADSB.lol</a> ODbL');
|
||||||
addExtraAttrib('<a href="https://www.planespotters.net/photo/api">Photo © planespotters.net</a>');
|
addExtraAttrib('<a href="https://www.planespotters.net/photo/api">Photo © planespotters.net</a>');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
57
tests/test_aircraft_popup_frontend.py
Normal file
57
tests/test_aircraft_popup_frontend.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Aircraft popup enrichment + emergency/MIL layer contract (static HTML)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
HTML = (ROOT / "app/static/index.html").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def _fn(name: str, nxt: str) -> str:
|
||||||
|
return HTML.split(f"function {name}", 1)[1].split(f"function {nxt}", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_popup_has_required_adsb_fields_and_photo():
|
||||||
|
js = _fn("pointPopup", "loadPlanePhoto")
|
||||||
|
for field in ("callsign", "hex", "registration", "type", "alt", "gs", "squawk"):
|
||||||
|
assert f"add('{field}'" in js
|
||||||
|
assert "class=\"ps-photo\"" in js or "class='ps-photo'" in js
|
||||||
|
assert "wikipedia" not in js.lower()
|
||||||
|
assert "ceo" not in js.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_emergency_badge_and_squawk_codes():
|
||||||
|
assert "role-badge emergency" in HTML
|
||||||
|
assert "hdg-emerg" in HTML
|
||||||
|
assert "EMERG_SQUAWK" in HTML
|
||||||
|
assert "['7700', '7600', '7500']" in HTML
|
||||||
|
emerg = HTML.split("function acIsEmergency", 1)[1].split("function acVisible", 1)[0]
|
||||||
|
assert "EMERG_SQUAWK.has(sq)" in emerg
|
||||||
|
color = HTML.split("function acColor", 1)[1].split("function connectLiveWs", 1)[0]
|
||||||
|
assert "acIsEmergency(p)" in color
|
||||||
|
assert "#ff5d5d" in color
|
||||||
|
|
||||||
|
|
||||||
|
def test_mil_toggle_hidden_until_role_flag_and_never_hits_adsb_lol():
|
||||||
|
assert 'id="lp-ac-mil-row"' in HTML
|
||||||
|
assert 'id="lp-ac-mil-on"' in HTML
|
||||||
|
row = HTML.split('id="lp-ac-mil-row"', 1)[1].split(">", 1)[0]
|
||||||
|
assert "hidden" in row
|
||||||
|
on = HTML.split('id="lp-ac-mil-on"', 1)[1].split(">", 1)[0]
|
||||||
|
assert "checked" not in on
|
||||||
|
load = HTML.split("async function loadAircraft", 1)[1].split("async function toggleTrains", 1)[0]
|
||||||
|
assert "/api/aircraft?bbox=" in load
|
||||||
|
assert "api.adsb.lol" not in load
|
||||||
|
assert "noteMilSupport" in load
|
||||||
|
assert "acMilOn" in load
|
||||||
|
note = HTML.split("function noteMilSupport", 1)[1].split("function acColor", 1)[0]
|
||||||
|
assert "extra.role" in note
|
||||||
|
assert "lp-ac-mil-row" in note
|
||||||
|
assert "hidden = false" in note
|
||||||
|
|
||||||
|
|
||||||
|
def test_planespotters_lazy_photo_still_wired():
|
||||||
|
assert "function loadPlanePhoto" in HTML
|
||||||
|
assert "/api/aircraft/photo?" in HTML
|
||||||
|
assert "map.on('popupopen', (e) => { loadPlanePhoto(e.popup); });" in HTML
|
||||||
Loading…
Add table
Reference in a new issue