sync: 2026-08-21 15:04:46
This commit is contained in:
@@ -634,6 +634,89 @@
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Permanent name label shown above each pin */
|
||||
.pin-label {
|
||||
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;
|
||||
}
|
||||
|
||||
.pin-label::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Custom circular pin that reflects the POI type */
|
||||
.poi-pin {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.poi-pin-inner {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/* User location marker (blue dot) */
|
||||
.user-loc-marker {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.user-loc-dot {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background: #1a73e8;
|
||||
|
||||
border: 3px solid white;
|
||||
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(26, 115, 232, 0.35),
|
||||
0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.user-loc-dot::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: -10px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background:
|
||||
radial-gradient(
|
||||
circle,
|
||||
rgba(26, 115, 232, 0.25) 0%,
|
||||
rgba(26, 115, 232, 0) 70%
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================
|
||||
MOBILE
|
||||
@@ -931,6 +1014,8 @@
|
||||
|
||||
let selectedMarker = null;
|
||||
|
||||
let userLocationMarker = null;
|
||||
|
||||
let zoomMessageTimer = null;
|
||||
|
||||
|
||||
@@ -977,55 +1062,55 @@
|
||||
].join(",");
|
||||
|
||||
|
||||
try {
|
||||
let data =
|
||||
await performSearch(
|
||||
query,
|
||||
viewbox
|
||||
);
|
||||
|
||||
const response =
|
||||
await fetch(
|
||||
"/search?q=" +
|
||||
encodeURIComponent(
|
||||
query + ", "
|
||||
) +
|
||||
"&viewbox=" +
|
||||
encodeURIComponent(viewbox)
|
||||
|
||||
/*
|
||||
* If nothing was found in the visible area,
|
||||
* fall back to searching the entire world.
|
||||
*/
|
||||
if (!data.length) {
|
||||
|
||||
data =
|
||||
await performSearch(
|
||||
query,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const data =
|
||||
await response.json();
|
||||
if (
|
||||
data === null
|
||||
) {
|
||||
|
||||
|
||||
if (
|
||||
!response.ok ||
|
||||
data.error
|
||||
) {
|
||||
|
||||
resultsCard.innerHTML = `
|
||||
<div class="result">
|
||||
<div class="result-text">
|
||||
${escapeHtml(
|
||||
data.error ||
|
||||
"Search failed"
|
||||
)}
|
||||
</div>
|
||||
resultsCard.innerHTML = `
|
||||
<div class="result">
|
||||
<div class="result-text">
|
||||
Search failed
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!data.length) {
|
||||
if (!data.length) {
|
||||
|
||||
resultsCard.innerHTML = `
|
||||
<div class="result">
|
||||
<div class="result-text">
|
||||
No results found
|
||||
</div>
|
||||
resultsCard.innerHTML = `
|
||||
<div class="result">
|
||||
<div class="result-text">
|
||||
No results found
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
resultsCard.innerHTML = "";
|
||||
@@ -1101,11 +1186,19 @@
|
||||
L.marker([
|
||||
place.lat,
|
||||
place.lon
|
||||
]).addTo(map);
|
||||
], {
|
||||
icon: createPoiIcon(place)
|
||||
}).addTo(map);
|
||||
|
||||
|
||||
marker.bindTooltip(
|
||||
getName(place)
|
||||
escapeHtml(getName(place)),
|
||||
{
|
||||
permanent: true,
|
||||
direction: "top",
|
||||
offset: [0, -20],
|
||||
className: "pin-label"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1136,9 +1229,11 @@
|
||||
|
||||
if (bounds.length === 1) {
|
||||
|
||||
const zoom = getZoomForPlace(data[0]);
|
||||
|
||||
map.flyTo(
|
||||
bounds[0],
|
||||
17,
|
||||
zoom,
|
||||
{
|
||||
duration: 0.7
|
||||
}
|
||||
@@ -1158,19 +1253,53 @@
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
resultsCard.innerHTML = `
|
||||
<div class="result">
|
||||
<div class="result-text">
|
||||
Search failed
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
/* =========================================================
|
||||
PERFORM SEARCH
|
||||
========================================================= */
|
||||
|
||||
async function performSearch(
|
||||
query,
|
||||
viewbox
|
||||
) {
|
||||
|
||||
let url =
|
||||
"/search?q=" +
|
||||
encodeURIComponent(
|
||||
query + ", "
|
||||
);
|
||||
|
||||
|
||||
if (viewbox) {
|
||||
|
||||
url +=
|
||||
"&viewbox=" +
|
||||
encodeURIComponent(
|
||||
viewbox
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const response =
|
||||
await fetch(url);
|
||||
|
||||
|
||||
const data =
|
||||
await response.json();
|
||||
|
||||
|
||||
if (
|
||||
!response.ok ||
|
||||
data.error
|
||||
) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1178,6 +1307,51 @@
|
||||
SELECT PLACE
|
||||
========================================================= */
|
||||
|
||||
/*
|
||||
* Determine an appropriate zoom level based on the
|
||||
* kind and type of the selected place.
|
||||
*/
|
||||
function getZoomForPlace(place) {
|
||||
|
||||
const kind = place.kind || "other";
|
||||
const type = (place.type || "").toLowerCase();
|
||||
|
||||
// POIs: zoom in close to see the building/spot.
|
||||
if (kind === "poi") {
|
||||
return 17;
|
||||
}
|
||||
|
||||
// Addresses: street-level view.
|
||||
if (kind === "address") {
|
||||
return 18;
|
||||
}
|
||||
|
||||
// Areas: vary by administrative level.
|
||||
if (kind === "area") {
|
||||
if (type === "country" || type === "state" || type === "region") {
|
||||
return 6;
|
||||
}
|
||||
if (type === "county" || type === "municipality") {
|
||||
return 9;
|
||||
}
|
||||
if (type === "city" || type === "town") {
|
||||
return 13;
|
||||
}
|
||||
if (type === "village" || type === "hamlet") {
|
||||
return 14;
|
||||
}
|
||||
if (type === "suburb" || type === "neighbourhood" || type === "quarter") {
|
||||
return 15;
|
||||
}
|
||||
// Water bodies, natural features, etc.
|
||||
return 12;
|
||||
}
|
||||
|
||||
// Default fallback.
|
||||
return 15;
|
||||
}
|
||||
|
||||
|
||||
function selectPlace(place) {
|
||||
|
||||
placeCard.style.display =
|
||||
@@ -1202,12 +1376,15 @@
|
||||
]).addTo(map);
|
||||
|
||||
|
||||
const zoom = getZoomForPlace(place);
|
||||
|
||||
|
||||
map.flyTo(
|
||||
[
|
||||
place.lat,
|
||||
place.lon
|
||||
],
|
||||
17,
|
||||
zoom,
|
||||
{
|
||||
duration: 0.7
|
||||
}
|
||||
@@ -2006,51 +2183,125 @@
|
||||
LOCATION BUTTON
|
||||
========================================================= */
|
||||
|
||||
function showUserLocation(lat, lon) {
|
||||
|
||||
const coords = [lat, lon];
|
||||
|
||||
|
||||
if (!userLocationMarker) {
|
||||
|
||||
userLocationMarker =
|
||||
L.marker(coords, {
|
||||
icon: createUserLocationIcon(),
|
||||
zIndexOffset: 1000
|
||||
}).addTo(map);
|
||||
|
||||
} else {
|
||||
|
||||
userLocationMarker.setLatLng(coords);
|
||||
}
|
||||
|
||||
|
||||
map.flyTo(
|
||||
coords,
|
||||
16,
|
||||
{
|
||||
duration: 0.8
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function locateMe() {
|
||||
|
||||
if (
|
||||
!navigator.geolocation
|
||||
) {
|
||||
|
||||
alert(
|
||||
"Geolocation is not supported."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
navigator.geolocation
|
||||
.getCurrentPosition(
|
||||
position => {
|
||||
|
||||
showUserLocation(
|
||||
position.coords.latitude,
|
||||
position.coords.longitude
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
error => {
|
||||
|
||||
console.warn(
|
||||
"Geolocation failed:",
|
||||
error
|
||||
);
|
||||
|
||||
alert(
|
||||
"Could not determine your location."
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
document
|
||||
.getElementById(
|
||||
"location-button"
|
||||
)
|
||||
.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
|
||||
if (
|
||||
!navigator.geolocation
|
||||
) {
|
||||
|
||||
alert(
|
||||
"Geolocation is not supported."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
navigator.geolocation
|
||||
.getCurrentPosition(
|
||||
position => {
|
||||
|
||||
map.flyTo(
|
||||
[
|
||||
position.coords.latitude,
|
||||
position.coords.longitude
|
||||
],
|
||||
16
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
() => {
|
||||
|
||||
alert(
|
||||
"Could not determine your location."
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
locateMe
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* On launch, focus the map around the viewer's device
|
||||
* location and drop a pin there. If the location cannot
|
||||
* be determined, fall back to Amsterdam.
|
||||
*/
|
||||
window.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
|
||||
if (
|
||||
!navigator.geolocation
|
||||
) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
navigator.geolocation
|
||||
.getCurrentPosition(
|
||||
position => {
|
||||
|
||||
showUserLocation(
|
||||
position.coords.latitude,
|
||||
position.coords.longitude
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
() => {
|
||||
|
||||
/*
|
||||
* Location unavailable — keep the
|
||||
* default Amsterdam view.
|
||||
*/
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/* =========================================================
|
||||
ZOOM MESSAGE
|
||||
========================================================= */
|
||||
@@ -2346,6 +2597,96 @@
|
||||
}
|
||||
|
||||
|
||||
function getPoiColor(place) {
|
||||
|
||||
const type =
|
||||
getType(place)
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
if (
|
||||
type.includes("restaurant") ||
|
||||
type.includes("cafe") ||
|
||||
type.includes("food")
|
||||
)
|
||||
return "#e8710a";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("hotel") ||
|
||||
type.includes("hostel")
|
||||
)
|
||||
return "#7b1fa2";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("museum")
|
||||
)
|
||||
return "#5c6bc0";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("shop") ||
|
||||
type.includes("supermarket")
|
||||
)
|
||||
return "#43a047";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("station") ||
|
||||
type.includes("transport")
|
||||
)
|
||||
return "#0288d1";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("pharmacy")
|
||||
)
|
||||
return "#e53935";
|
||||
|
||||
|
||||
if (
|
||||
type.includes("park")
|
||||
)
|
||||
return "#2e7d32";
|
||||
|
||||
|
||||
return "#1a73e8";
|
||||
}
|
||||
|
||||
|
||||
function createPoiIcon(place) {
|
||||
|
||||
const html = `
|
||||
<div
|
||||
class="poi-pin-inner"
|
||||
style="background:${getPoiColor(place)};"
|
||||
>
|
||||
${getIcon(place)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
return L.divIcon({
|
||||
className: "poi-pin",
|
||||
html: html,
|
||||
iconSize: [34, 34],
|
||||
iconAnchor: [17, 17]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function createUserLocationIcon() {
|
||||
|
||||
return L.divIcon({
|
||||
className: "user-loc-marker",
|
||||
html: `<div class="user-loc-dot"></div>`,
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function safeUrl(url) {
|
||||
|
||||
if (!url)
|
||||
|
||||
Reference in New Issue
Block a user