237 lines
6.6 KiB
HTML
237 lines
6.6 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Pupmap by BoopLabs – v2025-10-03_2300</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<link rel="stylesheet" href="style.css">
|
||
<link href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel="stylesheet"/>
|
||
<script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
|
||
<script src="https://unpkg.com/pmtiles/dist/pmtiles.js"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<input id="searchBox" placeholder="Search place..." />
|
||
<div id="map"></div>
|
||
|
||
<script>
|
||
const protocol = new pmtiles.Protocol();
|
||
maplibregl.addProtocol("pmtiles", protocol.tile);
|
||
|
||
const map = new maplibregl.Map({
|
||
container: 'map',
|
||
style: 'style.json',
|
||
center: [10, 60],
|
||
zoom: 4
|
||
});
|
||
|
||
map.on('load', () => {
|
||
// Roads
|
||
map.addSource('roads', {
|
||
type: 'geojson',
|
||
data: 'data/roads_europe_clean.geojson'
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'roads-minor',
|
||
type: 'line',
|
||
source: 'roads',
|
||
filter: ['==', ['get', 'type'], 'minor'],
|
||
paint: {
|
||
'line-color': '#cccccc',
|
||
'line-width': 1,
|
||
'line-opacity': 0.5
|
||
}
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'roads-major',
|
||
type: 'line',
|
||
source: 'roads',
|
||
filter: ['==', ['get', 'type'], 'major'],
|
||
paint: {
|
||
'line-color': '#555555',
|
||
'line-width': 2,
|
||
'line-opacity': 0.7
|
||
}
|
||
});
|
||
|
||
// Cities
|
||
map.addSource('cities', {
|
||
type: 'geojson',
|
||
data: 'data/world_cities_europe_clean.geojson'
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'city-points-small',
|
||
type: 'circle',
|
||
source: 'cities',
|
||
filter: ['<', ['get','POP_MAX'], 100000],
|
||
paint: {
|
||
'circle-radius': 3,
|
||
'circle-color': '#999999',
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': '#666666'
|
||
}
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'city-points-medium',
|
||
type: 'circle',
|
||
source: 'cities',
|
||
filter: ['all', ['>=', ['get','POP_MAX'], 100000], ['<', ['get','POP_MAX'], 500000]],
|
||
paint: {
|
||
'circle-radius': 5,
|
||
'circle-color': '#777777',
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': '#555555'
|
||
}
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'city-points-large',
|
||
type: 'circle',
|
||
source: 'cities',
|
||
filter: ['>=', ['get','POP_MAX'], 500000],
|
||
paint: {
|
||
'circle-radius': 7,
|
||
'circle-color': '#444444',
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': '#222222'
|
||
}
|
||
});
|
||
|
||
// City labels, zoom + population based
|
||
map.addLayer({
|
||
id: 'city-labels',
|
||
type: 'symbol',
|
||
source: 'cities',
|
||
layout: {
|
||
'text-field': ['get', 'NAME'],
|
||
'text-font': ['Noto Sans Regular'],
|
||
'text-size': [
|
||
'interpolate', ['linear'], ['zoom'],
|
||
4, 10,
|
||
6, 12,
|
||
10, 16
|
||
],
|
||
'text-offset': [0, 1.2],
|
||
'text-anchor': 'top'
|
||
},
|
||
paint: {
|
||
'text-color': '#222222',
|
||
'text-halo-color': '#ffffff',
|
||
'text-halo-width': 1
|
||
},
|
||
filter: [
|
||
'>',
|
||
['get','POP_MAX'],
|
||
['interpolate', ['linear'], ['zoom'],
|
||
4, 2000000, // at zoom 4, only megacities
|
||
6, 1000000, // at zoom 6, large cities
|
||
8, 500000, // at zoom 8, medium cities
|
||
10, 100000 // at zoom 10+, small cities
|
||
]
|
||
]
|
||
});
|
||
|
||
// City popups
|
||
map.on('click', ['city-points-small','city-points-medium','city-points-large'], (e) => {
|
||
const coordinates = e.features[0].geometry.coordinates.slice();
|
||
const cityName = e.features[0].properties.NAME || "Unknown City";
|
||
const population = e.features[0].properties.POP_MAX || "n/a";
|
||
new maplibregl.Popup()
|
||
.setLngLat(coordinates)
|
||
.setHTML(`<strong>${cityName}</strong><br/>Population: ${population}`)
|
||
.addTo(map);
|
||
});
|
||
|
||
// Cursor for city dots
|
||
['city-points-small','city-points-medium','city-points-large'].forEach(layer => {
|
||
map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; });
|
||
map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; });
|
||
});
|
||
|
||
// POIs
|
||
map.addSource('pois', {
|
||
type: 'geojson',
|
||
data: 'data/pois.geojson',
|
||
cluster: true,
|
||
clusterMaxZoom: 14,
|
||
clusterRadius: 50
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'clusters',
|
||
type: 'circle',
|
||
source: 'pois',
|
||
filter: ['has', 'point_count'],
|
||
paint: {
|
||
'circle-color': '#FF0000',
|
||
'circle-radius': ['step', ['get','point_count'], 15, 10, 20, 50, 25],
|
||
'circle-opacity': 0.7,
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': '#fff'
|
||
}
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'unclustered-point',
|
||
type: 'circle',
|
||
source: 'pois',
|
||
filter: ['!', ['has','point_count']],
|
||
paint: {
|
||
'circle-color': '#FF0000',
|
||
'circle-radius': 8,
|
||
'circle-opacity': 0.7,
|
||
'circle-stroke-width': 1,
|
||
'circle-stroke-color': '#fff'
|
||
}
|
||
});
|
||
|
||
map.on('click', 'unclustered-point', (e) => {
|
||
const coordinates = e.features[0].geometry.coordinates.slice();
|
||
const popupContent = e.features[0].properties.popup || e.features[0].properties.names.join(', ');
|
||
new maplibregl.Popup()
|
||
.setLngLat(coordinates)
|
||
.setHTML(`<strong>${popupContent}</strong>`)
|
||
.addTo(map);
|
||
});
|
||
|
||
map.on('click', 'clusters', (e) => {
|
||
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
|
||
const clusterId = features[0].properties.cluster_id;
|
||
map.getSource('pois').getClusterExpansionZoom(clusterId, (err, zoom) => {
|
||
if (err) return;
|
||
map.easeTo({ center: features[0].geometry.coordinates, zoom: zoom });
|
||
});
|
||
});
|
||
|
||
['clusters', 'unclustered-point'].forEach(layer => {
|
||
map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; });
|
||
map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; });
|
||
});
|
||
|
||
});
|
||
|
||
// Nominatim search
|
||
document.getElementById('searchBox').addEventListener('keypress', async function(e){
|
||
if(e.key==='Enter'){
|
||
const query = e.target.value;
|
||
const url = `http://yourhost:7070/search?q=${encodeURIComponent(query)}&format=json`;
|
||
try{
|
||
const res = await fetch(url);
|
||
const results = await res.json();
|
||
if(results.length>0){
|
||
const loc = results[0];
|
||
map.flyTo({ center: [loc.lon, loc.lat], zoom: 12 });
|
||
}else{ alert("No results found"); }
|
||
}catch(err){ alert("Error connecting to search API"); console.error(err);}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
|