sync: 2026-08-21 16:58:10
This commit is contained in:
@@ -648,57 +648,33 @@
|
|||||||
MARKER STYLES
|
MARKER STYLES
|
||||||
===================================================== */
|
===================================================== */
|
||||||
|
|
||||||
/* Wrapper that holds both the label and the pin circle */
|
/*
|
||||||
.poi-pin-wrapper {
|
* Simple coloured dot marker.
|
||||||
position: relative;
|
* Kept intentionally minimal so MapLibre's internal
|
||||||
cursor: pointer;
|
* transform-based positioning stays stable during
|
||||||
width: 34px;
|
* zoom / pan animations on vector-tile maps.
|
||||||
height: 34px;
|
*/
|
||||||
}
|
.poi-dot {
|
||||||
|
width: 28px;
|
||||||
/* Permanent name label shown above each pin */
|
height: 28px;
|
||||||
.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;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
font-size: 17px;
|
cursor: pointer;
|
||||||
line-height: 1;
|
/* Prevent any CSS transition from fighting MapLibre's
|
||||||
|
per-frame transform updates during camera moves. */
|
||||||
color: white;
|
will-change: transform;
|
||||||
|
|
||||||
border: 2px solid white;
|
|
||||||
|
|
||||||
box-shadow:
|
|
||||||
0 2px 6px rgba(0, 0, 0, 0.35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.user-loc-marker {
|
||||||
cursor: default;
|
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;
|
let selectedMarker = null;
|
||||||
|
|
||||||
|
/** Popup attached to the selected marker (reverse-geocode result). */
|
||||||
let selectedPopup = null;
|
let selectedPopup = null;
|
||||||
|
|
||||||
|
/** Blue dot showing the user's GPS position. */
|
||||||
let userLocationMarker = null;
|
let userLocationMarker = null;
|
||||||
|
|
||||||
let zoomMessageTimer = 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
|
SEARCH
|
||||||
========================================================= */
|
========================================================= */
|
||||||
@@ -1125,168 +1134,77 @@
|
|||||||
|
|
||||||
resultsCard.innerHTML = "";
|
resultsCard.innerHTML = "";
|
||||||
|
|
||||||
|
const lngLats = [];
|
||||||
const bounds = [];
|
|
||||||
|
|
||||||
|
|
||||||
data.forEach(place => {
|
data.forEach(place => {
|
||||||
|
|
||||||
const result =
|
// Skip results with missing/invalid coordinates
|
||||||
document.createElement(
|
if (
|
||||||
"div"
|
typeof place.lat !== "number" ||
|
||||||
);
|
typeof place.lon !== "number" ||
|
||||||
|
!isFinite(place.lat) ||
|
||||||
|
!isFinite(place.lon) ||
|
||||||
result.className =
|
Math.abs(place.lat) > 90 ||
|
||||||
"result";
|
Math.abs(place.lon) > 180
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// --- Results list item ---
|
||||||
|
const result = document.createElement("div");
|
||||||
|
result.className = "result";
|
||||||
|
|
||||||
result.innerHTML = `
|
result.innerHTML = `
|
||||||
|
<div class="result-icon">${getIcon(place)}</div>
|
||||||
<div class="result-icon">
|
|
||||||
${getIcon(place)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-text">
|
<div class="result-text">
|
||||||
|
<div class="result-name">${escapeHtml(getName(place))}</div>
|
||||||
<div class="result-name">
|
<div class="result-address">${escapeHtml(getShortAddress(place))}</div>
|
||||||
${escapeHtml(
|
<div class="result-type">${escapeHtml(formatType(getType(place)))}</div>
|
||||||
getName(place)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-address">
|
|
||||||
${escapeHtml(
|
|
||||||
getShortAddress(place)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-type">
|
|
||||||
${escapeHtml(
|
|
||||||
formatType(
|
|
||||||
getType(place)
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
result.addEventListener("click", () => {
|
||||||
|
selectPlace(place);
|
||||||
|
resultsCard.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
result.addEventListener(
|
resultsCard.appendChild(result);
|
||||||
"click",
|
|
||||||
() => {
|
|
||||||
|
|
||||||
selectPlace(
|
// --- Map marker (simple dot, centered on coordinate) ---
|
||||||
place
|
const dotEl = createDotElement(
|
||||||
);
|
getPoiColor(place),
|
||||||
|
getIcon(place)
|
||||||
resultsCard.style.display =
|
|
||||||
"none";
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
dotEl.title = getName(place);
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({
|
||||||
|
element: dotEl,
|
||||||
|
anchor: "center"
|
||||||
|
}).setLngLat([place.lon, place.lat]).addTo(map);
|
||||||
|
|
||||||
resultsCard.appendChild(
|
// Stop propagation so clicking a POI pin does NOT
|
||||||
result
|
// also trigger the map's reverse-geocode handler.
|
||||||
);
|
marker.getElement().addEventListener("click", e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
highlightPoiMarker(marker);
|
||||||
// Build the marker element with label + pin
|
selectPlace(place);
|
||||||
const el =
|
resultsCard.style.display = "none";
|
||||||
document.createElement(
|
});
|
||||||
"div"
|
|
||||||
);
|
|
||||||
|
|
||||||
el.className =
|
|
||||||
"poi-pin-wrapper";
|
|
||||||
|
|
||||||
el.innerHTML = `
|
|
||||||
<div class="pin-label">
|
|
||||||
${escapeHtml(
|
|
||||||
getName(place)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="poi-pin-inner"
|
|
||||||
style="background:${getPoiColor(place)};"
|
|
||||||
>
|
|
||||||
${getIcon(place)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
poiMarkers.push(marker);
|
||||||
|
lngLats.push([place.lon, place.lat]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fly / fit to results
|
||||||
if (bounds.length === 1) {
|
if (lngLats.length === 1) {
|
||||||
|
|
||||||
const zoom = getZoomForPlace(data[0]);
|
|
||||||
|
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [
|
center: lngLats[0],
|
||||||
bounds[0][1],
|
zoom: getZoomForPlace(data[0]),
|
||||||
bounds[0][0]
|
|
||||||
],
|
|
||||||
zoom: zoom,
|
|
||||||
duration: 700
|
duration: 700
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
}
|
const sw = [Math.min(...lngLats.map(p => p[0])), Math.min(...lngLats.map(p => p[1]))];
|
||||||
else {
|
const ne = [Math.max(...lngLats.map(p => p[0])), Math.max(...lngLats.map(p => p[1]))];
|
||||||
|
map.fitBounds([sw, ne], { padding: 80 });
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1430,9 +1348,7 @@
|
|||||||
|
|
||||||
let url =
|
let url =
|
||||||
"/search?q=" +
|
"/search?q=" +
|
||||||
encodeURIComponent(
|
encodeURIComponent(query);
|
||||||
query + ", "
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
if (viewbox) {
|
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) {
|
function selectPlace(place) {
|
||||||
|
|
||||||
placeCard.style.display =
|
placeCard.style.display = "block";
|
||||||
"block";
|
resultsCard.style.display = "none";
|
||||||
|
|
||||||
resultsCard.style.display =
|
|
||||||
"none";
|
|
||||||
|
|
||||||
|
|
||||||
clearSelection();
|
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 =
|
selectedMarker = new maplibregl.Marker({
|
||||||
new maplibregl.Marker()
|
element: dotEl,
|
||||||
.setLngLat([
|
anchor: "center"
|
||||||
place.lon,
|
}).setLngLat([place.lon, place.lat]).addTo(map);
|
||||||
place.lat
|
|
||||||
])
|
|
||||||
.addTo(map);
|
|
||||||
|
|
||||||
|
|
||||||
const zoom = getZoomForPlace(place);
|
const zoom = getZoomForPlace(place);
|
||||||
|
|
||||||
|
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [
|
center: [place.lon, place.lat],
|
||||||
place.lon,
|
|
||||||
place.lat
|
|
||||||
],
|
|
||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
duration: 700
|
duration: 700
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
renderPlace(place);
|
renderPlace(place);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1625,14 +1547,10 @@
|
|||||||
|
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
|
selectedMarker = new maplibregl.Marker({
|
||||||
selectedMarker =
|
element: createDefaultDot(),
|
||||||
new maplibregl.Marker()
|
anchor: "center"
|
||||||
.setLngLat([
|
}).setLngLat([lon, lat]).addTo(map);
|
||||||
lon,
|
|
||||||
lat
|
|
||||||
])
|
|
||||||
.addTo(map);
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2529,14 +2447,8 @@
|
|||||||
|
|
||||||
function clearMarkers() {
|
function clearMarkers() {
|
||||||
|
|
||||||
markers.forEach(
|
poiMarkers.forEach(marker => marker.remove());
|
||||||
marker =>
|
poiMarkers = [];
|
||||||
marker.remove()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
markers = [];
|
|
||||||
|
|
||||||
|
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user