Compare commits
2 commits
4c9ffb9673
...
0becc8a716
| Author | SHA1 | Date | |
|---|---|---|---|
| 0becc8a716 | |||
|
|
ac03442998 |
2 changed files with 141 additions and 3 deletions
|
|
@ -174,7 +174,16 @@
|
|||
border: 1px solid var(--line-hi);
|
||||
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));
|
||||
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"] {
|
||||
background: var(--bg-1); color: var(--text); border: 1px solid var(--line);
|
||||
border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.76rem;
|
||||
|
|
@ -641,6 +650,8 @@
|
|||
flex: 1 1 8rem;
|
||||
min-width: 0;
|
||||
}
|
||||
#chokepoint-label, .chokepoint-btns { display: none; }
|
||||
#chokepoint-select { display: block; }
|
||||
.map-stage {
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
|
|
@ -749,6 +760,11 @@
|
|||
<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-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 class="map-stage" id="map-wrap">
|
||||
<div id="map"></div>
|
||||
|
|
@ -1902,6 +1918,8 @@ let moveDebounce = null;
|
|||
let overlayAbort = null;
|
||||
let lastCell = '';
|
||||
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
|
||||
function bboxCell() {
|
||||
if (!map) return '';
|
||||
|
|
@ -2023,6 +2041,7 @@ async function initMap() {
|
|||
maxZoom: 12,
|
||||
attributionControl: true,
|
||||
});
|
||||
window.map = map;
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
map.attributionControl.setPrefix('');
|
||||
let activeHls = null;
|
||||
|
|
@ -2131,6 +2150,7 @@ async function initMap() {
|
|||
vesselsOn = document.getElementById('lp-vessels-on').checked;
|
||||
stormsOn = document.getElementById('lp-storms-on').checked;
|
||||
connectLiveWs();
|
||||
loadChokepoints();
|
||||
requestAnimationFrame(() => {
|
||||
if (firesOn) loadFires();
|
||||
setTimeout(() => {
|
||||
|
|
@ -2258,7 +2278,93 @@ function mapGoDays(n) {
|
|||
document.getElementById('map-date').value = iso;
|
||||
mapDateChanged();
|
||||
}
|
||||
function mapResetView() { if (map) map.setView([25, 10], 2); }
|
||||
function mapResetView() {
|
||||
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() {
|
||||
const b = map.getBounds();
|
||||
|
|
@ -3208,7 +3314,9 @@ async function loadVessels() {
|
|||
const isWorld = (be - bw) >= 300 || bb.startsWith('-180.0000,-85.0000,180.0000,85.0000');
|
||||
const subKey = isWorld ? '' : quantizeBBox(bb);
|
||||
let retuned = false;
|
||||
if (subKey && subKey !== lastVesselSubBox) {
|
||||
// AISStream subscribe retunes the US feed. Never fire it for Gulf/strait
|
||||
// boxes or the free stream leaves CONUS. VesselAPI is a cached poll.
|
||||
if (subKey && subKey !== lastVesselSubBox && intersectsConus()) {
|
||||
lastVesselSubBox = subKey;
|
||||
retuned = true;
|
||||
fetch(`${API}/api/vessels/subscribe`, {
|
||||
|
|
@ -3218,7 +3326,8 @@ async function loadVessels() {
|
|||
}).catch(() => {});
|
||||
}
|
||||
try {
|
||||
const r = await overlayFetch(`${API}/api/vessels?bbox=${bb}${dvrQs()}`);
|
||||
const srcQs = vesselSrcPref ? `&src=${encodeURIComponent(vesselSrcPref)}` : '';
|
||||
const r = await overlayFetch(`${API}/api/vessels?bbox=${bb}${srcQs}${dvrQs()}`);
|
||||
const pts = await r.json();
|
||||
if (req !== overlayReq.vessels) return;
|
||||
vesselsGroup = renderPoints(vesselsGroup, Array.isArray(pts) ? pts : [], p => {
|
||||
|
|
|
|||
|
|
@ -47,3 +47,32 @@ def test_ws_reconnect_uses_backoff():
|
|||
def test_check_health_treats_degraded_status():
|
||||
fn = HTML.split("async function checkHealth")[1].split("/* ═══════════════ NAV")[0]
|
||||
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