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 = ''; + if (zone) { + const st = zoneStage(zone.properties.zone); + riskHtml = ``; + } + return ``; + } + + 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: '
๐Ÿ“
', + 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';