feat: custom map markers with per-location flood-zone risk
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
src/static/custom-markers.json holds user points of interest
({name, lat, lon, optional note}); the dashboard renders them as pin
markers whose popups show which inundation zone the point sits in, its
P.1 trigger level, and the live 24 h exceedance probability (ray-cast
point-in-polygon against the zone geojson; smallest matching zone wins).
Seeded with the owner's four properties.
This commit is contained in:
@@ -123,6 +123,10 @@
|
||||
@keyframes riverMove { to { stroke-dashoffset: -40; } }
|
||||
@media (prefers-reduced-motion: reduce) { .flow-line, .flow-marker::before { animation: none; } }
|
||||
.line-swatch { width: 24px; height: 4px; border-radius: 2px; flex: none; }
|
||||
.poi-marker {
|
||||
width: 26px; height: 26px; display: grid; place-items: center; font-size: 19px;
|
||||
filter: drop-shadow(0 3px 4px rgba(5,43,58,.45));
|
||||
}
|
||||
.forecast-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 10px; margin-top: 14px; }
|
||||
.forecast-station { border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; }
|
||||
.forecast-station strong { font-size: .8rem; }
|
||||
@@ -586,6 +590,7 @@
|
||||
renderMap(stations, readings, riverNetwork);
|
||||
renderList(stations, readings);
|
||||
renderThaiWaterSensors(thaiWaterSensors, new Set(stations.map((station) => station.station_code)));
|
||||
loadCustomMarkers();
|
||||
renderSummary(stations, readings);
|
||||
$('loading').style.display = 'none';
|
||||
loadForecasts(); // non-blocking; the card stays hidden until models are deployed
|
||||
@@ -675,6 +680,68 @@
|
||||
if (zoneLayer) zoneLayer.setStyle(zoneStyle);
|
||||
}
|
||||
|
||||
function pointInRing(lat, lon, ring) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const xi = ring[i][0], yi = ring[i][1];
|
||||
const xj = ring[j][0], yj = ring[j][1];
|
||||
if ((yi > lat) !== (yj > lat) && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
function zoneOfPoint(lat, lon) {
|
||||
if (!zoneData) return null;
|
||||
let best = null;
|
||||
zoneData.features.forEach((feature) => {
|
||||
const geom = feature.geometry;
|
||||
const polygons = geom.type === 'Polygon' ? [geom.coordinates] : geom.type === 'MultiPolygon' ? geom.coordinates : [];
|
||||
const inside = polygons.some((rings) => rings.length && pointInRing(lat, lon, rings[0]));
|
||||
if (inside && (!best || feature.properties.zone < best.properties.zone)) best = feature;
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
function poiPopupHtml(poi) {
|
||||
const zone = zoneOfPoint(poi.lat, poi.lon);
|
||||
let riskHtml = '<div class="popup-th">Outside the mapped flood zones</div>';
|
||||
if (zone) {
|
||||
const st = zoneStage(zone.properties.zone);
|
||||
riskHtml = `<div class="popup-grid">
|
||||
<div class="popup-metric"><span>Flood zone</span><strong>${zone.properties.zone} · P.1 ≥ ${Number(zone.properties.level).toFixed(2)} m</strong></div>
|
||||
<div class="popup-metric"><span>Chance within 24 h</span><strong>${st ? Math.round(st.p_exceed * 100) + '%' : '—'}</strong></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="popup"><div class="popup-code">Marker</div><h3>${escapeHtml(poi.name)}</h3>${poi.note ? `<div class="popup-th">${escapeHtml(poi.note)}</div>` : ''}${riskHtml}</div>`;
|
||||
}
|
||||
|
||||
async function loadCustomMarkers() {
|
||||
try {
|
||||
const response = await fetch('/static/custom-markers.json');
|
||||
if (!response.ok) return;
|
||||
const pois = await response.json();
|
||||
if (!Array.isArray(pois)) return;
|
||||
if (!zoneData) {
|
||||
try {
|
||||
const zonesResponse = await fetch('/static/flood-zones-p1.geojson');
|
||||
if (zonesResponse.ok) zoneData = await zonesResponse.json();
|
||||
} catch (error) { /* zones optional for markers */ }
|
||||
}
|
||||
pois.forEach((poi) => {
|
||||
if (!Number.isFinite(poi.lat) || !Number.isFinite(poi.lon)) return;
|
||||
const icon = L.divIcon({
|
||||
className: 'marker-wrap',
|
||||
html: '<div class="poi-marker">📍</div>',
|
||||
iconSize: [26, 26], iconAnchor: [13, 24], popupAnchor: [0, -22]
|
||||
});
|
||||
const marker = L.marker([poi.lat, poi.lon], { icon, title: poi.name })
|
||||
.bindPopup(() => poiPopupHtml(poi))
|
||||
.addTo(state.map);
|
||||
state.layers.push(marker);
|
||||
});
|
||||
} catch (error) { /* markers are optional */ }
|
||||
}
|
||||
|
||||
function stageColor(p) {
|
||||
if (p >= .5) return '#cc4b37';
|
||||
if (p >= .2) return '#d99018';
|
||||
|
||||
Reference in New Issue
Block a user