diff --git a/templates/index.html b/templates/index.html index 603d93a..69fa606 100644 --- a/templates/index.html +++ b/templates/index.html @@ -648,57 +648,33 @@ MARKER STYLES ===================================================== */ - /* Wrapper that holds both the label and the pin circle */ - .poi-pin-wrapper { - position: relative; - cursor: pointer; - width: 34px; - height: 34px; - } - - /* Permanent name label shown above each pin */ - .pin-label { - position: absolute; - bottom: 38px; - left: 50%; - transform: translateX(-50%); - - background: rgba(255, 255, 255, 0.92); - border: none; - border-radius: 4px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - color: #202124; - font-size: 12px; - font-weight: 500; - padding: 2px 7px; - white-space: nowrap; - cursor: pointer; - } - - /* Custom circular pin that reflects the POI type */ - .poi-pin-inner { - width: 34px; - height: 34px; - + /* + * Simple coloured dot marker. + * Kept intentionally minimal so MapLibre's internal + * transform-based positioning stays stable during + * zoom / pan animations on vector-tile maps. + */ + .poi-dot { + width: 28px; + height: 28px; border-radius: 50%; - + border: 3px solid #fff; + box-shadow: 0 2px 6px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; - - font-size: 17px; - line-height: 1; - - color: white; - - border: 2px solid white; - - box-shadow: - 0 2px 6px rgba(0, 0, 0, 0.35); + font-size: 14px; + cursor: pointer; + /* Prevent any CSS transition from fighting MapLibre's + per-frame transform updates during camera moves. */ + will-change: transform; } + .poi-dot.selected { + box-shadow: 0 0 0 4px rgba(26,115,232,0.35), 0 2px 8px rgba(0,0,0,0.5); + } - /* User location marker (blue dot) */ + /* User location marker (blue pulsing dot) */ .user-loc-marker { cursor: default; } @@ -1020,17 +996,50 @@ ); - let markers = []; + /** All search-result POI markers currently on the map. */ + let poiMarkers = []; + /** The single marker representing the user's current selection (click or search). */ let selectedMarker = null; + /** Popup attached to the selected marker (reverse-geocode result). */ let selectedPopup = null; + /** Blue dot showing the user's GPS position. */ let userLocationMarker = null; let zoomMessageTimer = null; + /* ========================================================= + MARKER FACTORY + ========================================================= */ + + /** + * Build a simple coloured dot element for a POI marker. + * Deliberately kept flat (no nested absolutely-positioned + * children, no SVG, no filters) so that MapLibre's per-frame + * transform updates stay rock-stable during zoom / pan on + * vector-tile maps. + */ + function createDotElement(color, icon) { + const el = document.createElement("div"); + el.className = "poi-dot"; + el.style.background = color; + el.textContent = icon || ""; + el.title = ""; + return el; + } + + + /** + * Plain red dot for map-click selections / fallbacks. + */ + function createDefaultDot() { + return createDotElement("#ea4335", ""); + } + + /* ========================================================= SEARCH ========================================================= */ @@ -1125,168 +1134,77 @@ resultsCard.innerHTML = ""; - - const bounds = []; - + const lngLats = []; data.forEach(place => { - const result = - document.createElement( - "div" - ); - - - result.className = - "result"; + // Skip results with missing/invalid coordinates + if ( + typeof place.lat !== "number" || + typeof place.lon !== "number" || + !isFinite(place.lat) || + !isFinite(place.lon) || + Math.abs(place.lat) > 90 || + Math.abs(place.lon) > 180 + ) + return; + // --- Results list item --- + const result = document.createElement("div"); + result.className = "result"; result.innerHTML = ` - -
- ${getIcon(place)} -
- +
${getIcon(place)}
- -
- ${escapeHtml( - getName(place) - )} -
- -
- ${escapeHtml( - getShortAddress(place) - )} -
- -
- ${escapeHtml( - formatType( - getType(place) - ) - )} -
- +
${escapeHtml(getName(place))}
+
${escapeHtml(getShortAddress(place))}
+
${escapeHtml(formatType(getType(place)))}
`; + result.addEventListener("click", () => { + selectPlace(place); + resultsCard.style.display = "none"; + }); - result.addEventListener( - "click", - () => { + resultsCard.appendChild(result); - selectPlace( - place - ); - - resultsCard.style.display = - "none"; - } + // --- Map marker (simple dot, centered on coordinate) --- + const dotEl = createDotElement( + getPoiColor(place), + getIcon(place) ); + dotEl.title = getName(place); + const marker = new maplibregl.Marker({ + element: dotEl, + anchor: "center" + }).setLngLat([place.lon, place.lat]).addTo(map); - resultsCard.appendChild( - result - ); - - - // Build the marker element with label + pin - const el = - document.createElement( - "div" - ); - - el.className = - "poi-pin-wrapper"; - - el.innerHTML = ` -
- ${escapeHtml( - getName(place) - )} -
-
- ${getIcon(place)} -
- `; - - - const marker = - new maplibregl.Marker({ - element: el, - anchor: "center" - }) - .setLngLat([ - place.lon, - place.lat - ]) - .addTo(map); - - - marker.getElement().addEventListener( - "click", - () => { - - selectPlace( - place - ); - - resultsCard.style.display = - "none"; - } - ); - - - markers.push(marker); - - - bounds.push([ - place.lat, - place.lon - ]); + // Stop propagation so clicking a POI pin does NOT + // also trigger the map's reverse-geocode handler. + marker.getElement().addEventListener("click", e => { + e.stopPropagation(); + highlightPoiMarker(marker); + selectPlace(place); + resultsCard.style.display = "none"; + }); + poiMarkers.push(marker); + lngLats.push([place.lon, place.lat]); }); - - if (bounds.length === 1) { - - const zoom = getZoomForPlace(data[0]); - + // Fly / fit to results + if (lngLats.length === 1) { map.flyTo({ - center: [ - bounds[0][1], - bounds[0][0] - ], - zoom: zoom, + center: lngLats[0], + zoom: getZoomForPlace(data[0]), duration: 700 }); - - } - else { - - const lons = - bounds.map(b => b[1]); - - const lats = - bounds.map(b => b[0]); - - map.fitBounds([ - [ - Math.min(...lons), - Math.min(...lats) - ], - [ - Math.max(...lons), - Math.max(...lats) - ] - ], { - padding: 80 - }); + } else { + const sw = [Math.min(...lngLats.map(p => p[0])), Math.min(...lngLats.map(p => p[1]))]; + const ne = [Math.max(...lngLats.map(p => p[0])), Math.max(...lngLats.map(p => p[1]))]; + map.fitBounds([sw, ne], { padding: 80 }); } } @@ -1430,9 +1348,7 @@ let url = "/search?q=" + - encodeURIComponent( - query + ", " - ); + encodeURIComponent(query); if (viewbox) { @@ -1515,40 +1431,46 @@ } + /** + * Highlight a POI marker by adding the "selected" class. + * Called when the user clicks on an existing search-result pin. + */ + function highlightPoiMarker(marker) { + // Remove highlight from all other POI markers + poiMarkers.forEach(m => { + m.getElement().classList.remove("selected"); + }); + marker.getElement().classList.add("selected"); + } + + function selectPlace(place) { - placeCard.style.display = - "block"; - - resultsCard.style.display = - "none"; - + placeCard.style.display = "block"; + resultsCard.style.display = "none"; clearSelection(); + // Coloured dot for known POIs, red default otherwise + const isKnownPoi = place.kind === "poi" || place.kind === "address"; + const dotEl = isKnownPoi + ? createDotElement(getPoiColor(place), getIcon(place)) + : createDefaultDot(); + dotEl.title = getName(place); - selectedMarker = - new maplibregl.Marker() - .setLngLat([ - place.lon, - place.lat - ]) - .addTo(map); - + selectedMarker = new maplibregl.Marker({ + element: dotEl, + anchor: "center" + }).setLngLat([place.lon, place.lat]).addTo(map); const zoom = getZoomForPlace(place); - map.flyTo({ - center: [ - place.lon, - place.lat - ], + center: [place.lon, place.lat], zoom: zoom, duration: 700 }); - renderPlace(place); } @@ -1625,14 +1547,10 @@ clearSelection(); - - selectedMarker = - new maplibregl.Marker() - .setLngLat([ - lon, - lat - ]) - .addTo(map); + selectedMarker = new maplibregl.Marker({ + element: createDefaultDot(), + anchor: "center" + }).setLngLat([lon, lat]).addTo(map); try { @@ -2529,14 +2447,8 @@ function clearMarkers() { - markers.forEach( - marker => - marker.remove() - ); - - - markers = []; - + poiMarkers.forEach(marker => marker.remove()); + poiMarkers = []; clearSelection(); }