perf: faster map boot — zoom-gate heavy layers, static basemap, defer HUD, lazy HLS

World view no longer fetches cameras/NWS/WFIGS. Default Blue Marble so init
does not wait on GIBS times. News/summary/events load with their views.
hls.min.js loads only on the first HLS camera popup.
This commit is contained in:
Sirius DevOps 2026-08-27 21:12:58 -04:00
parent dd7a09aadd
commit 07638288a9

View file

@ -1026,10 +1026,22 @@
<script src="/static/vendor/leaflet/leaflet.js"></script> <script src="/static/vendor/leaflet/leaflet.js"></script>
<script src="/static/vendor/leaflet/leaflet.markercluster.js"></script> <script src="/static/vendor/leaflet/leaflet.markercluster.js"></script>
<script src="/static/vendor/leaflet/leaflet.heat.js"></script> <script src="/static/vendor/leaflet/leaflet.heat.js"></script>
<script src="/static/vendor/hls/hls.min.js"></script>
<script> <script>
const API = ''; const API = '';
function loadHls() {
if (window.Hls) return Promise.resolve();
if (window._hlsLoading) return window._hlsLoading;
window._hlsLoading = new Promise((res, rej) => {
const s = document.createElement('script');
s.src = '/static/vendor/hls/hls.min.js';
s.onload = res;
s.onerror = rej;
document.head.appendChild(s);
});
return window._hlsLoading;
}
/* ═══════════════ BOOT SPLASH ═══════════════ */ /* ═══════════════ BOOT SPLASH ═══════════════ */
(function () { (function () {
const boot = document.getElementById('boot'); const boot = document.getElementById('boot');
@ -1136,7 +1148,8 @@ function showView(name) {
} }
} }
if (name === 'news') loadNews(false); if (name === 'news') loadNews(false);
if (name === 'settings') renderSysInfo(); if (name === 'events') { loadSummary(); loadEvents(); }
if (name === 'settings') { loadSummary(); renderSysInfo(); }
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@ -1651,7 +1664,10 @@ async function initMap() {
const sel = document.getElementById('map-layer'); const sel = document.getElementById('map-layer');
sel.innerHTML = mapLayers.map(l => sel.innerHTML = mapLayers.map(l =>
`<option value="${l.id}">${l.title}</option>`).join(''); `<option value="${l.id}">${l.title}</option>`).join('');
const preferred = mapLayers.find(l => l.has_time) || mapLayers[0]; const preferred =
mapLayers.find(l => l.id === 'BlueMarble_ShadedRelief_Bathymetry')
|| mapLayers.find(l => !l.has_time)
|| mapLayers[0];
if (preferred) sel.value = preferred.id; if (preferred) sel.value = preferred.id;
map = L.map('map', { map = L.map('map', {
center: [25, 10], zoom: 2, center: [25, 10], zoom: 2,
@ -1676,20 +1692,24 @@ async function initMap() {
img.alt = 'camera preview'; img.alt = 'camera preview';
v.replaceWith(img); v.replaceWith(img);
}; };
if (window.Hls && Hls.isSupported()) { const startHls = () => {
activeHls = new Hls({ enableWorker: true, lowLatencyMode: true }); if (window.Hls && Hls.isSupported()) {
activeHls.loadSource(v.dataset.hls); activeHls = new Hls({ enableWorker: true, lowLatencyMode: true });
activeHls.attachMedia(v); activeHls.loadSource(v.dataset.hls);
v.play && v.play().catch(() => {}); activeHls.attachMedia(v);
activeHls.on(Hls.Events.ERROR, (_, data) => { v.play && v.play().catch(() => {});
if (data && data.fatal) fallback(); activeHls.on(Hls.Events.ERROR, (_, data) => {
}); if (data && data.fatal) fallback();
} else if (v.canPlayType && v.canPlayType('application/vnd.apple.mpegurl')) { });
v.src = v.dataset.hls; } else if (v.canPlayType && v.canPlayType('application/vnd.apple.mpegurl')) {
v.addEventListener('error', fallback, { once: true }); v.src = v.dataset.hls;
} else { v.addEventListener('error', fallback, { once: true });
fallback(); } else {
} fallback();
}
};
if (window.Hls) startHls();
else loadHls().then(startHls).catch(fallback);
}); });
map.on('popupclose', () => { map.on('popupclose', () => {
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; } if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
@ -1719,7 +1739,8 @@ async function initMap() {
refreshLiveOverlays(); refreshLiveOverlays();
}, 300); }, 300);
}); });
await mapLayerChanged(); // Static Blue Marble has no time-domain fetch — don't block overlays on GIBS.
mapLayerChanged();
// Overlays default ON (checkboxes in the layer panel + saved settings) // Overlays default ON (checkboxes in the layer panel + saved settings)
applyMapSettings(readMapPrefs(), false); applyMapSettings(readMapPrefs(), false);
firesOn = document.getElementById('lp-fires-on').checked; firesOn = document.getElementById('lp-fires-on').checked;
@ -2036,6 +2057,13 @@ function camThumb(c) {
} }
async function loadCams() { async function loadCams() {
if (!map) return; if (!map) return;
if (tooZoomedOut()) {
camsGroup = dropLayer(camsGroup);
markZoom('lp-cams-count');
hudCamsCount = null;
syncHud();
return;
}
const req = ++camReq; const req = ++camReq;
try { try {
const r = await fetch(`${API}/api/cameras?bbox=${currentBBox()}&limit=5000`); const r = await fetch(`${API}/api/cameras?bbox=${currentBBox()}&limit=5000`);
@ -2175,6 +2203,14 @@ function dropLayer(ref) {
if (ref && map && map.hasLayer(ref)) map.removeLayer(ref); if (ref && map && map.hasLayer(ref)) map.removeLayer(ref);
return null; return null;
} }
/* Polygon/camera layers hitch at world scale. Same idea as aircraft's zoom<=3
skip: do not fetch or render these until the user is zoomed in. */
const HEAVY_MIN_ZOOM = 4;
function tooZoomedOut() { return !map || map.getZoom() <= HEAVY_MIN_ZOOM; }
function markZoom(id) {
const el = document.getElementById(id);
if (el) el.textContent = 'zoom';
}
function intersectsConus() { function intersectsConus() {
if (!map) return false; if (!map) return false;
const b = map.getBounds(); const b = map.getBounds();
@ -2338,6 +2374,11 @@ async function toggleWxAlerts() {
} }
async function loadWxAlerts() { async function loadWxAlerts() {
if (!map) return; if (!map) return;
if (tooZoomedOut()) {
wxAlertsGroup = dropLayer(wxAlertsGroup);
markZoom('lp-alerts-count');
return;
}
const req = ++overlayReq.alerts; const req = ++overlayReq.alerts;
try { try {
const r = await fetch(`${API}/api/weather-alerts?bbox=${currentBBox()}`); const r = await fetch(`${API}/api/weather-alerts?bbox=${currentBBox()}`);
@ -2369,6 +2410,11 @@ async function togglePerimeters() {
} }
async function loadPerimeters() { async function loadPerimeters() {
if (!map) return; if (!map) return;
if (tooZoomedOut()) {
perimGroup = dropLayer(perimGroup);
markZoom('lp-perim-count');
return;
}
const req = ++overlayReq.perim; const req = ++overlayReq.perim;
try { try {
const r = await fetch(`${API}/api/fire-perimeters?bbox=${currentBBox()}`); const r = await fetch(`${API}/api/fire-perimeters?bbox=${currentBBox()}`);
@ -2402,6 +2448,11 @@ async function toggleIncidents() {
} }
async function loadIncidents() { async function loadIncidents() {
if (!map) return; if (!map) return;
if (tooZoomedOut()) {
incidentsGroup = dropLayer(incidentsGroup);
markZoom('lp-incidents-count');
return;
}
const req = ++overlayReq.incidents; const req = ++overlayReq.incidents;
try { try {
const r = await fetch(`${API}/api/fire-incidents?bbox=${currentBBox()}`); const r = await fetch(`${API}/api/fire-incidents?bbox=${currentBBox()}`);
@ -2511,9 +2562,9 @@ async function loadStorms() {
initNav(); initNav();
initSettings(); initSettings();
initMarketTicker(); initMarketTicker();
loadSummary(); loadEvents(); loadNews(true); checkHealth(); checkHealth();
setInterval(() => { loadSummary(); loadEvents(); checkHealth(); }, 30000);
initMap(); initMap();
setInterval(checkHealth, 30000);
</script> </script>
</body> </body>
</html> </html>