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:

Reply to this post by email

Nach oben