Geo‑Daten dynamisch mit Leaflet.js laden
Nach meinem ersten Blogpost zum Leaflet-Hono-Projekt gibt es nun ein Update mit einigen Änderungen und neuen Features.
Hier ein Überblick über die wichtigsten Änderungen:
Statt die Layer-Reihenfolge mit bringToFront() zu steuern, haben ich nun Leaflet-Panes benutzt.
So kann ich die Z-Index-Reihenfolge der Layer exakt bestimmen:
- Länder liegen im Pane
countriesPane(zIndex 400) - POIs im Pane
poiPane(zIndex 500) - Hauptstädte/Capitals im Pane
capitalsPane(zIndex 600)
Das sorgt für eine saubere und vorhersehbare Darstellung – unabhängig von Interaktionen oder Nachladen von Daten. Vorallem aber wird bei einem Hover-Over über Länder die Darstellung der POIs nicht mehr beeinflusst.
Neue map.js Datei
Hier die neue map.js Datei, die die Ländergrenzen, POIs und Hauptstädte verwaltet:
1const map = L.map('map').setView([52.0705, 4.707], 6);
2L.tileLayer('', {}).addTo(map);
3var capitalsIcon = L.divIcon({ className: 'capitals-icon' });
4
5// Create custom panes for z-order control
6map.createPane('countriesPane');
7map.getPane('countriesPane').style.zIndex = 400;
8
9map.createPane('poiPane');
10map.getPane('poiPane').style.zIndex = 500;
11
12map.createPane('capitalsPane');
13map.getPane('capitalsPane').style.zIndex = 600;
14
15// Highlight-Funktionen
16function highlightFeature(e) {
17 const layer = e.target;
18 layer.setStyle({
19 weight: 2,
20 color: '#ff0000',
21 fillOpacity: 0.7,
22 });
23 layer.bringToFront();
24}
25
26function resetHighlight(e) {
27 geojson.resetStyle(e.target);
28 overlayGroup.bringToFront();
29}
30
31// Layer groups for better control
32const baseGroup = L.layerGroup().addTo(map);
33const overlayGroup = L.layerGroup().addTo(map);
34const capitalsLayer = L.layerGroup().addTo(map);
35
36// Country boundaries with outlines and optional fill
37let geojson;
38
39// Call layers in correct order
40function initLayers() {
41 addCountryLayer(addPOILayer);
42}
43
44function addCountryLayer(callback) {
45 fetch('/data/vm_medium_countries.geo.json')
46 .then((res) => res.json())
47 .then((data) => {
48 if (baseGroup) baseGroup.clearLayers();
49 geojson = L.geoJSON(data, {
50 pane: 'countriesPane',
51 style: (feature) => ({
52 color: feature.properties.color || '#333',
53 weight: 1,
54 opacity: 1,
55 fillColor: feature.properties.fillColor || '#eee',
56 fillOpacity: 1,
57 }),
58 onEachFeature: (feature, layer) => {
59 layer.bindPopup(feature.properties.name);
60 layer.on({
61 mouseover: highlightFeature,
62 mouseout: resetHighlight,
63 });
64 },
65 }).addTo(baseGroup);
66 if (callback) callback();
67 })
68 .catch(console.error);
69}
70
71function addPOILayer() {
72 fetch('/data/poi.geo.json')
73 .then((response) => response.json())
74 .then((data) => {
75 if (overlayGroup) overlayGroup.clearLayers();
76 L.geoJSON(data, {
77 pane: 'poiPane',
78 style: function (feature) {
79 if (!feature.properties) return {};
80 return {
81 color: feature.properties.stroke || '#3388ff',
82 weight: feature.properties['stroke-width'] || 3,
83 opacity: feature.properties['stroke-opacity'] || 1,
84 fillColor: feature.properties.fill || '#3388ff',
85 fillOpacity: feature.properties['fill-opacity'] || 0.2,
86 };
87 },
88 onEachFeature: (feature, layer) => {
89 if (feature.properties && feature.properties.name) {
90 layer.bindPopup(feature.properties.name);
91 }
92 },
93 }).addTo(overlayGroup);
94 });
95}
96
97// Capitals logic
98function clearCapitals() {
99 capitalsLayer.clearLayers();
100}
101
102document.getElementById('capitals-btn').onclick = async function () {
103 clearCapitals();
104 const res = await fetch('/api/capitals');
105 const capitals = await res.json();
106 capitals.forEach((capital) => {
107 const marker = L.marker([capital.lat, capital.lng], { icon: capitalsIcon, pane: 'capitalsPane' }).bindPopup(capital.name);
108 capitalsLayer.addLayer(marker);
109 });
110 capitalsLayer.bringToFront();
111};
112
113document.getElementById('remove-capitals-btn').onclick = function () {
114 clearCapitals();
115};
116
117initLayers();Die Layer für Länder, POIs und Hauptstädte werden jetzt in eigenen LayerGroups verwaltet. Das macht das Nachladen, Entfernen und Aktualisieren von Daten viel einfacher und übersichtlicher.
Neue HTML-Struktur
Hier die neue index.html Datei, die die Karte und die Buttons enthält:
1<!DOCTYPE html>
2<html lang="de">
3 <head>
4 <meta charset="UTF-8" />
5 <title>Minimal Leaflet Karte</title>
6 <link rel="stylesheet" href="styles.css" />
7 <link rel="stylesheet" href="/public/leaflet/leaflet.css" crossorigin="" />
8 </head>
9 <body>
10 <header>
11 <h1>Leaflet Demo Map</h1>
12 <p class="subtitle">A simple demonstration of a Leaflet map</p>
13 <button id="capitals-btn">Load Capitals via API</button>
14 <button id="remove-capitals-btn">Remove Capitals</button>
15 </header>
16
17 <div id="map"></div>
18 <script src="/public/leaflet/leaflet.js" crossorigin=""></script>
19 <script src="map.js"></script>
20 </body>
21</html>Neue Styles
Dazu passend die neuen Styles in der styles.css Datei, für die Karte und die Buttons:
1/* Header ersetzen */
2header {
3 background-color: #f8f9fa;
4 padding: 1em;
5 text-align: center;
6 border-bottom: 1px solid #ccc;
7}
8
9/* und in der Map die Werte für 'margin' neu setzen */
10#map {
11 margin: 1rem 1rem;
12}
13
14button {
15 padding: 0.5em 1em;
16 margin: 0.5em;
17 border: none;
18 border-radius: 4px;
19 background-color: #007bff;
20 color: white;
21 cursor: pointer;
22}
23
24.capitals-icon {
25 background-color: #007bff;
26 border-radius: 50%;
27 width: 24px;
28 height: 24px;
29 display: flex;
30 align-items: center;
31 justify-content: center;
32}Neue index.ts Datei
Jetzt noch die index.ts Datei, die die API für die Hauptstädte bereitstellt:
1import { serve } from '@hono/node-server';
2import { Hono } from 'hono';
3import { serveStatic } from '@hono/node-server/serve-static';
4
5const app = new Hono();
6
7// Serve static files from 'public'
8app.use('/public/*', serveStatic({ root: './' }));
9
10// Serve static files from 'data'
11app.use('/data/*', serveStatic({ root: './' }));
12
13// API endpoint for dynamic location data
14app.get('/api/capitals', (c) => {
15 const capitals = [
16 { id: 1, name: 'Berlin', lat: 52.52, lng: 13.405 },
17 { id: 2, name: 'Paris', lat: 48.8566, lng: 2.3522 },
18 { id: 3, name: 'London', lat: 51.5074, lng: -0.1278 },
19 { id: 4, name: 'Madrid', lat: 40.4168, lng: -3.7038 },
20 { id: 5, name: 'Rome', lat: 41.9028, lng: 12.4964 },
21 { id: 6, name: 'Vienna', lat: 48.2082, lng: 16.3738 },
22 { id: 7, name: 'Amsterdam', lat: 52.3676, lng: 4.9041 },
23 { id: 8, name: 'Brussels', lat: 50.8503, lng: 4.3517 },
24 { id: 9, name: 'Copenhagen', lat: 55.6761, lng: 12.5683 },
25 { id: 10, name: 'Stockholm', lat: 59.3293, lng: 18.0686 },
26 { id: 11, name: 'Oslo', lat: 59.9139, lng: 10.7522 },
27 { id: 12, name: 'Helsinki', lat: 60.1695, lng: 24.9354 },
28 { id: 13, name: 'Lisbon', lat: 38.7223, lng: -9.1393 },
29 { id: 14, name: 'Dublin', lat: 53.3498, lng: -6.2603 },
30 { id: 15, name: 'Warsaw', lat: 52.2297, lng: 21.0122 },
31 { id: 16, name: 'Prague', lat: 50.0755, lng: 14.4378 },
32 { id: 17, name: 'Budapest', lat: 47.4979, lng: 19.0402 },
33 { id: 18, name: 'Bratislava', lat: 48.1482, lng: 17.1067 },
34 { id: 19, name: 'Ljubljana', lat: 46.0569, lng: 14.5051 },
35 { id: 20, name: 'Zagreb', lat: 45.815, lng: 15.9819 },
36 ];
37 return c.json(capitals);
38});
39
40// Redirect root to public/index.html
41app.get('/', (c) => c.html('<script>location.href="/public/index.html"</script>'));
42
43// Start the server
44serve(
45 {
46 fetch: app.fetch,
47 port: 3000,
48 },
49 (info) => {
50 console.log(`Server is running on http://localhost:${info.port}`);
51 }
52);Die API liefert eine Liste von Hauptstädten mit ihren Koordinaten. Diese werden später in der Karte als Marker angezeigt. Und mit den neu hinzugefügten Buttons kann der Nutzer die Hauptstädte dynamisch laden und entfernen.
Neue Daten
Und ins Verzeichnis data kommt neben der GeoJSON-Datei vm_medium_countries.geo.json, die du von geojson-maps.kyd.au heruntergeladen hast, die neue Version der `poi.geo.json Datei welche neue POI enthält.
Wie in der map.js Datei zu sehen ist, werden die Information zur Darstellung der POIs jetzt direkt aus den GeoJSON-Properties (stroke, stroke-width, fill, etc.) übernommen. So kann jeder POI individuell gestaltet werden – z.B. für besondere Städte oder Flächen.
Fazit
Durch die Nutzung von LayerGroups, Panes und klaren Funktionsaufrufen ist der Code jetzt deutlich modularer und leichter zu erweitern.
Wenn alles geklappt hat, solltest du nun eine funktionierende Leaflet-Karte haben, die Ländergrenzen anzeigt und dynamisch POIs und Hauptstädte laden kann und ungefähr so aussieht: