diff --git a/src/static/custom-markers.json b/src/static/custom-markers.json new file mode 100644 index 0000000..399d10e --- /dev/null +++ b/src/static/custom-markers.json @@ -0,0 +1,6 @@ +[ + { "name": "newedge", "lat": 18.77208394455051, "lon": 98.98366920482054 }, + { "name": "Sunflower", "lat": 18.775240304156092, "lon": 98.98373200474349 }, + { "name": "white house", "lat": 18.77589874066847, "lon": 98.98363500976409 }, + { "name": "casa mia / b4l", "lat": 18.76974018178173, "lon": 98.9877579642922 } +] diff --git a/src/static/dashboard.html b/src/static/dashboard.html index cc1cd0f..e7f1f69 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -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 = '