Compare commits
No commits in common. "0becc8a716788c0f99b8f49bf2f1fb0ae1ca5ed1" and "4c9ffb9673e24c504cccd5a5bbaf910ea46476af" have entirely different histories.
0becc8a716
...
4c9ffb9673
2 changed files with 3 additions and 141 deletions
|
|
@ -174,16 +174,7 @@
|
||||||
border: 1px solid var(--line-hi);
|
border: 1px solid var(--line-hi);
|
||||||
box-shadow: 0 0 16px rgba(53,224,255,0.10);
|
box-shadow: 0 0 16px rgba(53,224,255,0.10);
|
||||||
clip-path: polygon(0 6px, 6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px));
|
clip-path: polygon(0 6px, 6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px));
|
||||||
max-width: calc(100% - 300px); /* keep off LAYERS (left) and zoom (right) */
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.chokepoint-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; }
|
|
||||||
.chokepoint-btns .btn { padding: 0.22rem 0.42rem; font-size: 0.66rem; letter-spacing: 0.04em; }
|
|
||||||
.chokepoint-btns .btn.is-on {
|
|
||||||
border-color: var(--cyan); color: var(--cyan);
|
|
||||||
box-shadow: 0 0 8px var(--cyan-glow); background: rgba(53,224,255,0.10);
|
|
||||||
}
|
|
||||||
#chokepoint-select { display: none; }
|
|
||||||
.map-toolbar select, .map-toolbar input[type="date"], .map-toolbar input[type="datetime-local"] {
|
.map-toolbar select, .map-toolbar input[type="date"], .map-toolbar input[type="datetime-local"] {
|
||||||
background: var(--bg-1); color: var(--text); border: 1px solid var(--line);
|
background: var(--bg-1); color: var(--text); border: 1px solid var(--line);
|
||||||
border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.76rem;
|
border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.76rem;
|
||||||
|
|
@ -650,8 +641,6 @@
|
||||||
flex: 1 1 8rem;
|
flex: 1 1 8rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
#chokepoint-label, .chokepoint-btns { display: none; }
|
|
||||||
#chokepoint-select { display: block; }
|
|
||||||
.map-stage {
|
.map-stage {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
@ -760,11 +749,6 @@
|
||||||
<button class="btn" id="map-7d-btn" onclick="mapGoDays(7)" style="display:none">-7d</button>
|
<button class="btn" id="map-7d-btn" onclick="mapGoDays(7)" style="display:none">-7d</button>
|
||||||
<button class="btn" id="map-30d-btn" onclick="mapGoDays(30)" style="display:none">-30d</button>
|
<button class="btn" id="map-30d-btn" onclick="mapGoDays(30)" style="display:none">-30d</button>
|
||||||
<button class="btn" id="map-reset" onclick="mapResetView()">Reset</button>
|
<button class="btn" id="map-reset" onclick="mapResetView()">Reset</button>
|
||||||
<span class="mt-label" id="chokepoint-label">Strait</span>
|
|
||||||
<div class="chokepoint-btns" id="chokepoint-btns" role="group" aria-label="Chokepoint presets"></div>
|
|
||||||
<select id="chokepoint-select" aria-label="Chokepoint preset" onchange="applyChokepoint(this.value)">
|
|
||||||
<option value="">Strait…</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="map-stage" id="map-wrap">
|
<div class="map-stage" id="map-wrap">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
@ -1918,8 +1902,6 @@ let moveDebounce = null;
|
||||||
let overlayAbort = null;
|
let overlayAbort = null;
|
||||||
let lastCell = '';
|
let lastCell = '';
|
||||||
let lastVesselSubBox = ''; // last viewport box sent to the AIS stream
|
let lastVesselSubBox = ''; // last viewport box sent to the AIS stream
|
||||||
let chokepointCatalog = []; // GET /api/map/chokepoints, fetched once
|
|
||||||
let vesselSrcPref = ''; // 'vesselapi' only on Hormuz preset — no extra polls
|
|
||||||
let vesselRefollowTimer = null; // one follow-up fetch after a retune
|
let vesselRefollowTimer = null; // one follow-up fetch after a retune
|
||||||
function bboxCell() {
|
function bboxCell() {
|
||||||
if (!map) return '';
|
if (!map) return '';
|
||||||
|
|
@ -2041,7 +2023,6 @@ async function initMap() {
|
||||||
maxZoom: 12,
|
maxZoom: 12,
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
});
|
});
|
||||||
window.map = map;
|
|
||||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||||
map.attributionControl.setPrefix('');
|
map.attributionControl.setPrefix('');
|
||||||
let activeHls = null;
|
let activeHls = null;
|
||||||
|
|
@ -2150,7 +2131,6 @@ async function initMap() {
|
||||||
vesselsOn = document.getElementById('lp-vessels-on').checked;
|
vesselsOn = document.getElementById('lp-vessels-on').checked;
|
||||||
stormsOn = document.getElementById('lp-storms-on').checked;
|
stormsOn = document.getElementById('lp-storms-on').checked;
|
||||||
connectLiveWs();
|
connectLiveWs();
|
||||||
loadChokepoints();
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (firesOn) loadFires();
|
if (firesOn) loadFires();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -2278,93 +2258,7 @@ function mapGoDays(n) {
|
||||||
document.getElementById('map-date').value = iso;
|
document.getElementById('map-date').value = iso;
|
||||||
mapDateChanged();
|
mapDateChanged();
|
||||||
}
|
}
|
||||||
function mapResetView() {
|
function mapResetView() { if (map) map.setView([25, 10], 2); }
|
||||||
vesselSrcPref = '';
|
|
||||||
document.querySelectorAll('#chokepoint-btns .btn').forEach(b => {
|
|
||||||
b.classList.remove('is-on');
|
|
||||||
b.setAttribute('aria-pressed', 'false');
|
|
||||||
});
|
|
||||||
const sel = document.getElementById('chokepoint-select');
|
|
||||||
if (sel) sel.value = '';
|
|
||||||
if (map) map.setView([25, 10], 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHOKEPOINT_SHORT = {
|
|
||||||
hormuz: 'Hormuz',
|
|
||||||
bab_el_mandeb: 'Bab el-Mandeb',
|
|
||||||
suez: 'Suez',
|
|
||||||
malacca: 'Malacca',
|
|
||||||
taiwan: 'Taiwan',
|
|
||||||
};
|
|
||||||
async function loadChokepoints() {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`${API}/api/map/chokepoints`);
|
|
||||||
const d = await r.json();
|
|
||||||
if (r.ok && Array.isArray(d.chokepoints)) chokepointCatalog = d.chokepoints;
|
|
||||||
} catch (_) { /* catalog is optional until the backend PR lands */ }
|
|
||||||
renderChokepointControls();
|
|
||||||
}
|
|
||||||
function renderChokepointControls() {
|
|
||||||
const btns = document.getElementById('chokepoint-btns');
|
|
||||||
const sel = document.getElementById('chokepoint-select');
|
|
||||||
if (!btns || !sel) return;
|
|
||||||
btns.replaceChildren();
|
|
||||||
sel.querySelectorAll('option:not([value=""])').forEach(o => o.remove());
|
|
||||||
for (const p of chokepointCatalog) {
|
|
||||||
const label = CHOKEPOINT_SHORT[p.id] || p.title || p.id;
|
|
||||||
const b = document.createElement('button');
|
|
||||||
b.type = 'button';
|
|
||||||
b.className = 'btn';
|
|
||||||
b.dataset.id = p.id;
|
|
||||||
b.textContent = label;
|
|
||||||
b.setAttribute('aria-pressed', 'false');
|
|
||||||
b.addEventListener('click', () => applyChokepoint(p.id));
|
|
||||||
btns.appendChild(b);
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = p.id;
|
|
||||||
opt.textContent = label;
|
|
||||||
sel.appendChild(opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function chokepointLeafletBounds(bbox) {
|
|
||||||
// Catalog bbox is minlat,minlon,maxlat,maxlon — Leaflet wants [[lat,lon],[lat,lon]].
|
|
||||||
const [minlat, minlon, maxlat, maxlon] = String(bbox || '').split(',').map(Number);
|
|
||||||
if (![minlat, minlon, maxlat, maxlon].every(Number.isFinite)) return null;
|
|
||||||
return [[minlat, minlon], [maxlat, maxlon]];
|
|
||||||
}
|
|
||||||
function applyChokepoint(id) {
|
|
||||||
if (!id || !map) return;
|
|
||||||
const p = chokepointCatalog.find(x => x.id === id);
|
|
||||||
if (!p) return;
|
|
||||||
document.querySelectorAll('#chokepoint-btns .btn').forEach(b => {
|
|
||||||
const on = b.dataset.id === id;
|
|
||||||
b.classList.toggle('is-on', on);
|
|
||||||
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
||||||
});
|
|
||||||
const sel = document.getElementById('chokepoint-select');
|
|
||||||
if (sel && sel.value !== id) sel.value = id;
|
|
||||||
vesselSrcPref = p.vesselapi ? 'vesselapi' : '';
|
|
||||||
if (Array.isArray(p.center) && p.center.length === 2 && p.zoom) {
|
|
||||||
map.setView(p.center, p.zoom);
|
|
||||||
} else {
|
|
||||||
const bounds = chokepointLeafletBounds(p.bbox);
|
|
||||||
if (bounds) map.fitBounds(bounds, { padding: [24, 24] });
|
|
||||||
}
|
|
||||||
const vChk = document.getElementById('lp-vessels-on');
|
|
||||||
if (vChk) vChk.checked = true;
|
|
||||||
vesselsOn = true;
|
|
||||||
// Hormuz especially: turn SAR on so AIS dots sit on the radar raster. User can untick.
|
|
||||||
if (p.id === 'hormuz' || p.vesselapi) {
|
|
||||||
const sChk = document.getElementById('lp-sentinel-on');
|
|
||||||
if (sChk && !sChk.checked) {
|
|
||||||
sChk.checked = true;
|
|
||||||
sentinelOn = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastCell = '';
|
|
||||||
if (vesselsOn) loadVessels();
|
|
||||||
if (sentinelOn) loadSentinel1();
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentBBox() {
|
function currentBBox() {
|
||||||
const b = map.getBounds();
|
const b = map.getBounds();
|
||||||
|
|
@ -3314,9 +3208,7 @@ async function loadVessels() {
|
||||||
const isWorld = (be - bw) >= 300 || bb.startsWith('-180.0000,-85.0000,180.0000,85.0000');
|
const isWorld = (be - bw) >= 300 || bb.startsWith('-180.0000,-85.0000,180.0000,85.0000');
|
||||||
const subKey = isWorld ? '' : quantizeBBox(bb);
|
const subKey = isWorld ? '' : quantizeBBox(bb);
|
||||||
let retuned = false;
|
let retuned = false;
|
||||||
// AISStream subscribe retunes the US feed. Never fire it for Gulf/strait
|
if (subKey && subKey !== lastVesselSubBox) {
|
||||||
// boxes or the free stream leaves CONUS. VesselAPI is a cached poll.
|
|
||||||
if (subKey && subKey !== lastVesselSubBox && intersectsConus()) {
|
|
||||||
lastVesselSubBox = subKey;
|
lastVesselSubBox = subKey;
|
||||||
retuned = true;
|
retuned = true;
|
||||||
fetch(`${API}/api/vessels/subscribe`, {
|
fetch(`${API}/api/vessels/subscribe`, {
|
||||||
|
|
@ -3326,8 +3218,7 @@ async function loadVessels() {
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const srcQs = vesselSrcPref ? `&src=${encodeURIComponent(vesselSrcPref)}` : '';
|
const r = await overlayFetch(`${API}/api/vessels?bbox=${bb}${dvrQs()}`);
|
||||||
const r = await overlayFetch(`${API}/api/vessels?bbox=${bb}${srcQs}${dvrQs()}`);
|
|
||||||
const pts = await r.json();
|
const pts = await r.json();
|
||||||
if (req !== overlayReq.vessels) return;
|
if (req !== overlayReq.vessels) return;
|
||||||
vesselsGroup = renderPoints(vesselsGroup, Array.isArray(pts) ? pts : [], p => {
|
vesselsGroup = renderPoints(vesselsGroup, Array.isArray(pts) ? pts : [], p => {
|
||||||
|
|
|
||||||
|
|
@ -47,32 +47,3 @@ def test_ws_reconnect_uses_backoff():
|
||||||
def test_check_health_treats_degraded_status():
|
def test_check_health_treats_degraded_status():
|
||||||
fn = HTML.split("async function checkHealth")[1].split("/* ═══════════════ NAV")[0]
|
fn = HTML.split("async function checkHealth")[1].split("/* ═══════════════ NAV")[0]
|
||||||
assert "degraded" in fn.lower() or "d.status" in fn
|
assert "degraded" in fn.lower() or "d.status" in fn
|
||||||
|
|
||||||
|
|
||||||
def test_chokepoint_presets_in_toolbar():
|
|
||||||
assert 'id="chokepoint-btns"' in HTML
|
|
||||||
assert 'id="chokepoint-select"' in HTML
|
|
||||||
assert "loadChokepoints()" in HTML
|
|
||||||
assert "/api/map/chokepoints" in HTML
|
|
||||||
assert "function applyChokepoint" in HTML
|
|
||||||
for name in ("Hormuz", "Bab el-Mandeb", "Suez", "Malacca", "Taiwan"):
|
|
||||||
assert name in HTML
|
|
||||||
|
|
||||||
|
|
||||||
def test_chokepoint_skips_aisstream_subscribe_outside_conus():
|
|
||||||
load = HTML.split("async function loadVessels")[1].split("async function toggleStorms")[0]
|
|
||||||
assert "intersectsConus()" in load
|
|
||||||
assert "api/vessels/subscribe" in load
|
|
||||||
assert "src=${encodeURIComponent(vesselSrcPref)}" in load or "&src=" in load
|
|
||||||
apply = HTML.split("function applyChokepoint")[1].split("function currentBBox")[0]
|
|
||||||
assert "vesselapi" in apply
|
|
||||||
assert "lp-vessels-on" in apply
|
|
||||||
assert "lp-sentinel-on" in apply
|
|
||||||
assert "map.setView" in apply
|
|
||||||
assert "minlat,minlon,maxlat,maxlon" in HTML.split("function chokepointLeafletBounds")[1][:400]
|
|
||||||
|
|
||||||
|
|
||||||
def test_phone_chokepoints_use_select_not_buttons():
|
|
||||||
mobile = HTML.split("@media (max-width: 820px)")[1].split("@media (prefers-reduced-motion")[0]
|
|
||||||
assert "#chokepoint-select { display: block; }" in mobile
|
|
||||||
assert ".chokepoint-btns { display: none; }" in mobile or "#chokepoint-label, .chokepoint-btns { display: none; }" in mobile
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue