sync: 2026-08-21 15:04:46
This commit is contained in:
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# LibreMaps
|
||||||
|
|
||||||
|
A lightweight, self-hosted map viewer built with **Flask** and **Leaflet**, powered entirely by free open data from **OpenStreetMap** (via the Nominatim geocoding API) and **MapTiler** street tiles.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive map** — pan/zoom over MapTiler "Streets" tiles, centered on Amsterdam by default.
|
||||||
|
- **Search** — type a place or POI to search. Results are first restricted to the area currently visible on the map; if nothing is found locally, it automatically falls back to searching the entire world.
|
||||||
|
- **Category shortcuts** — quick chips for common places (Supermarkt, Café, Kroeg, Camping, Park, Restaurant, Vegan, Hotel, Parkeerplek).
|
||||||
|
- **Typed pins** — each result is drawn as a colored circular badge whose emoji and color reflect the POI type (e.g. 🍴 orange for food, 🛏 purple for hotels, 🚆 blue for transport, 💊 red for pharmacies, 🌳 green for parks). A small permanent label shows the location name above each pin.
|
||||||
|
- **Place details card** — click a result or a pin to see the name, type, address, opening hours, phone, website, email, and extra tags (cuisine, brand, wheelchair access, payment methods, etc.).
|
||||||
|
- **Actions** — get directions (Google Maps), web search (Qwant), call, visit website, or email directly from the detail card.
|
||||||
|
- **Reverse geocoding** — click anywhere on the map (when zoomed in enough) to look up what's at that spot.
|
||||||
|
- **"My location"** button using browser geolocation.
|
||||||
|
- **Responsive layout** — adapts to mobile screens.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-----------|-------------------------------------|
|
||||||
|
| Backend | Python 3, Flask |
|
||||||
|
| Frontend | Vanilla JS, Leaflet 1.9.4 |
|
||||||
|
| Geocoding | OpenStreetMap Nominatim |
|
||||||
|
| Tiles | MapTiler Streets v4 |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
openmaps/
|
||||||
|
├── app.py # Flask app: /, /search, /geocode routes
|
||||||
|
├── templates/
|
||||||
|
│ └── index.html # Single-page UI (map + all client logic)
|
||||||
|
├── sync.py # Sync helper script
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── Dockerfile # Container image definition
|
||||||
|
├── docker-compose.yml # Local container orchestration
|
||||||
|
├── .dockerignore
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Backend (`app.py`)
|
||||||
|
|
||||||
|
Three endpoints proxy requests to Nominatim while applying a simple rate limit (minimum 1 second between calls) and a descriptive `User-Agent`:
|
||||||
|
|
||||||
|
- `GET /` — serves the single-page UI.
|
||||||
|
- `GET /search?q=...&viewbox=left,top,right,bottom` — forwards a forward-geocoding query. When a valid viewbox is supplied, results are bounded to that region (`bounded=1`). Returns normalized JSON (coordinates, address, extratags, namedetails, importance, bounding box).
|
||||||
|
- `GET /geocode?lat=...&lon=...` — reverse-geocodes a coordinate into a place record.
|
||||||
|
|
||||||
|
### Frontend (`templates/index.html`)
|
||||||
|
|
||||||
|
- Initializes a Leaflet map with MapTiler tiles.
|
||||||
|
- Builds a viewbox from the current map bounds and sends it with each search so results stay local.
|
||||||
|
- Renders results in a scrollable card and drops typed pins on the map.
|
||||||
|
- Falls back to a worldwide search when the local search returns nothing.
|
||||||
|
- Shows a detail card with contact info, opening hours, and action buttons.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- (Optional) Docker / Docker Compose
|
||||||
|
|
||||||
|
### Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open <http://localhost:5000>.
|
||||||
|
|
||||||
|
### Run with Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The MapTiler tile layer uses an embedded API key in `templates/index.html`; replace it with your own key for production use.
|
||||||
|
- Nominatim has usage policies — keep the built-in rate limiting in place and identify your application via the `User-Agent`.
|
||||||
67
app.py
67
app.py
@@ -17,6 +17,70 @@ last_request_time = 0
|
|||||||
MIN_REQUEST_INTERVAL = 1.0
|
MIN_REQUEST_INTERVAL = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def classify_place(item):
|
||||||
|
"""
|
||||||
|
Classify a Nominatim result into one of:
|
||||||
|
- "area" : countries, states, cities, towns, villages, suburbs,
|
||||||
|
neighbourhoods, water bodies, natural features, etc.
|
||||||
|
- "poi" : shops, restaurants, hotels, buildings, attractions, ...
|
||||||
|
- "address" : street / house-number level results
|
||||||
|
- "other" : anything else
|
||||||
|
|
||||||
|
The client uses this to pick an appropriate zoom level when the
|
||||||
|
user selects a result.
|
||||||
|
"""
|
||||||
|
category = (item.get("category") or "").lower()
|
||||||
|
place_type = (item.get("type") or "").lower()
|
||||||
|
address = item.get("address") or {}
|
||||||
|
namedetails = item.get("namedetails") or {}
|
||||||
|
|
||||||
|
# Administrative boundaries are always areas.
|
||||||
|
if place_type.endswith("_boundary"):
|
||||||
|
return "area"
|
||||||
|
|
||||||
|
# Place categories (city, town, village, suburb, ...) are areas.
|
||||||
|
if category == "place":
|
||||||
|
return "area"
|
||||||
|
|
||||||
|
# Boundaries are areas.
|
||||||
|
if category == "boundary":
|
||||||
|
return "area"
|
||||||
|
|
||||||
|
# Natural & water features are treated as areas (they span regions).
|
||||||
|
if category in ("natural", "water", "waterway"):
|
||||||
|
return "area"
|
||||||
|
|
||||||
|
# A real street/house-number result is an address.
|
||||||
|
if (
|
||||||
|
address.get("house_number")
|
||||||
|
or address.get("road")
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
namedetails.get("name")
|
||||||
|
or namedetails.get("addr:housenumber")
|
||||||
|
or address.get("house_number")
|
||||||
|
):
|
||||||
|
return "address"
|
||||||
|
|
||||||
|
# Known POI categories.
|
||||||
|
poi_categories = {
|
||||||
|
"amenity",
|
||||||
|
"shop",
|
||||||
|
"tourism",
|
||||||
|
"leisure",
|
||||||
|
"office",
|
||||||
|
"craft",
|
||||||
|
"building",
|
||||||
|
"historic",
|
||||||
|
"industrial",
|
||||||
|
"public_transport",
|
||||||
|
}
|
||||||
|
if category in poi_categories:
|
||||||
|
return "poi"
|
||||||
|
|
||||||
|
return "other"
|
||||||
|
|
||||||
|
|
||||||
def nominatim_request(url, params):
|
def nominatim_request(url, params):
|
||||||
"""
|
"""
|
||||||
Make a request to Nominatim while respecting a small
|
Make a request to Nominatim while respecting a small
|
||||||
@@ -124,6 +188,9 @@ def search():
|
|||||||
"category":
|
"category":
|
||||||
item.get("category"),
|
item.get("category"),
|
||||||
|
|
||||||
|
"kind":
|
||||||
|
classify_place(item),
|
||||||
|
|
||||||
"address":
|
"address":
|
||||||
item.get("address", {}),
|
item.get("address", {}),
|
||||||
|
|
||||||
|
|||||||
@@ -634,6 +634,89 @@
|
|||||||
font-size: 10px;
|
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
|
MOBILE
|
||||||
@@ -931,6 +1014,8 @@
|
|||||||
|
|
||||||
let selectedMarker = null;
|
let selectedMarker = null;
|
||||||
|
|
||||||
|
let userLocationMarker = null;
|
||||||
|
|
||||||
let zoomMessageTimer = null;
|
let zoomMessageTimer = null;
|
||||||
|
|
||||||
|
|
||||||
@@ -977,35 +1062,35 @@
|
|||||||
].join(",");
|
].join(",");
|
||||||
|
|
||||||
|
|
||||||
try {
|
let data =
|
||||||
|
await performSearch(
|
||||||
const response =
|
query,
|
||||||
await fetch(
|
viewbox
|
||||||
"/search?q=" +
|
|
||||||
encodeURIComponent(
|
|
||||||
query + ", "
|
|
||||||
) +
|
|
||||||
"&viewbox=" +
|
|
||||||
encodeURIComponent(viewbox)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const data =
|
/*
|
||||||
await response.json();
|
* If nothing was found in the visible area,
|
||||||
|
* fall back to searching the entire world.
|
||||||
|
*/
|
||||||
|
if (!data.length) {
|
||||||
|
|
||||||
|
data =
|
||||||
|
await performSearch(
|
||||||
|
query,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!response.ok ||
|
data === null
|
||||||
data.error
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
resultsCard.innerHTML = `
|
resultsCard.innerHTML = `
|
||||||
<div class="result">
|
<div class="result">
|
||||||
<div class="result-text">
|
<div class="result-text">
|
||||||
${escapeHtml(
|
Search failed
|
||||||
data.error ||
|
|
||||||
"Search failed"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1101,11 +1186,19 @@
|
|||||||
L.marker([
|
L.marker([
|
||||||
place.lat,
|
place.lat,
|
||||||
place.lon
|
place.lon
|
||||||
]).addTo(map);
|
], {
|
||||||
|
icon: createPoiIcon(place)
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
|
||||||
marker.bindTooltip(
|
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) {
|
if (bounds.length === 1) {
|
||||||
|
|
||||||
|
const zoom = getZoomForPlace(data[0]);
|
||||||
|
|
||||||
map.flyTo(
|
map.flyTo(
|
||||||
bounds[0],
|
bounds[0],
|
||||||
17,
|
zoom,
|
||||||
{
|
{
|
||||||
duration: 0.7
|
duration: 0.7
|
||||||
}
|
}
|
||||||
@@ -1159,18 +1254,52 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
resultsCard.innerHTML = `
|
/* =========================================================
|
||||||
<div class="result">
|
PERFORM SEARCH
|
||||||
<div class="result-text">
|
========================================================= */
|
||||||
Search failed
|
|
||||||
</div>
|
async function performSearch(
|
||||||
</div>
|
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
|
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) {
|
function selectPlace(place) {
|
||||||
|
|
||||||
placeCard.style.display =
|
placeCard.style.display =
|
||||||
@@ -1202,12 +1376,15 @@
|
|||||||
]).addTo(map);
|
]).addTo(map);
|
||||||
|
|
||||||
|
|
||||||
|
const zoom = getZoomForPlace(place);
|
||||||
|
|
||||||
|
|
||||||
map.flyTo(
|
map.flyTo(
|
||||||
[
|
[
|
||||||
place.lat,
|
place.lat,
|
||||||
place.lon
|
place.lon
|
||||||
],
|
],
|
||||||
17,
|
zoom,
|
||||||
{
|
{
|
||||||
duration: 0.7
|
duration: 0.7
|
||||||
}
|
}
|
||||||
@@ -2006,13 +2183,36 @@
|
|||||||
LOCATION BUTTON
|
LOCATION BUTTON
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
document
|
function showUserLocation(lat, lon) {
|
||||||
.getElementById(
|
|
||||||
"location-button"
|
const coords = [lat, lon];
|
||||||
)
|
|
||||||
.addEventListener(
|
|
||||||
"click",
|
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 (
|
if (
|
||||||
!navigator.geolocation
|
!navigator.geolocation
|
||||||
@@ -2030,21 +2230,72 @@
|
|||||||
.getCurrentPosition(
|
.getCurrentPosition(
|
||||||
position => {
|
position => {
|
||||||
|
|
||||||
map.flyTo(
|
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",
|
||||||
|
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.latitude,
|
||||||
position.coords.longitude
|
position.coords.longitude
|
||||||
],
|
|
||||||
16
|
|
||||||
);
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
alert(
|
/*
|
||||||
"Could not determine your location."
|
* Location unavailable — keep the
|
||||||
);
|
* default Amsterdam view.
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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) {
|
function safeUrl(url) {
|
||||||
|
|
||||||
if (!url)
|
if (!url)
|
||||||
|
|||||||
Reference in New Issue
Block a user