Karte mit Punkten

Do, 18. November 2021, Ralf Hersel

Angenommen, ihr habt eine Webseite und wollt darauf eine Landkarte mit Punkten darstellen. Das können zum Beispiel Orte sein, zu denen ihr eine Reise unternommen habt, oder eure Lieblingsrestaurants, oder irgendwelche Points of Interest. In diesem Artikel beschreibe ich, wie man das macht.

Bei diesem kleinen Projekt geht es um drei Sachen: eure Webseite, die Karte und die Punkte. Auf der Webseite solltet ihr in der Lage sein, einen iFrame in die Seite einzubetten, auf der die Karte erscheinen soll. Die Karte basiert auf OpenStreetMap und wird über Leaflet bezogen. Die Punkte sollen in einer separaten Datei abgelegt werden, damit ihr diese einfach erweitern könnt. Lest weiter, wie man das macht:

Ich selbst verwende eine solche Karte, um meine Reiseziele zu dokumentieren. Auf einer Weltkarte gibt es jede Menge Punkte, die man anklicken kann, um weitere Informationen zum Reiseziel und einen Link zum zugehörigen Artikel zu erhalten. Im Titelbild seht ihr ein Beispiel: Madrid, 2016, Link zum Blogpost.

Wer eine generische Lösung sucht, findet auf der Seite von Leaflet viele praktische Beispiele. Meine Lösung besteht aus zwei Dateien: eine HTML-Datei, die die Karte darstellt und aus einer zweiten Datei die Daten für die Punkte liest. Diese Trennung hat den grossen Vorteil, dass die HTML-Datei per iFrame in die Webseite eingebunden werden kann und danach nicht mehr angefasst werden muss. Das ist insbesondere deshalb wichtig, weil viele CMS-Editoren (z.B. TinyMCE) das Einbinden von IFrames unterdrücken. Möchte man die Seite bearbeiten, muss vorher der Editor getauscht werden, was ziemlich nervig ist. Die Datei mit den Punkte-Daten kann man bequem erweitern und auf den Webserver hochladen.

Die HTML-Datei für den IFrame

Hier seht ihr die Datei, mit der die Karte dargestellt wird, die Punkte aus der Datendatei geladen und auf der Karte positioniert werden:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">
<head>
    <title>Map</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
</head>
<body>
<div id="map" style="width: 1130px; height: 680px"></div>
<script>
    /*
    ============================================================================
    Travel map
    Version 0.06
    Date: 12. Nov. 2021
    ============================================================================
    How to integrate the map:
    <p><iframe src="https://yourwebsite.de/tmp/map.html" width="1150" height="700"></iframe></p>
    Switch from TinyMCE for Raw-Editor before you paste the iframe tag.
    ============================================================================
    */

    var mymap = L.map('map').setView([45, 10], 2);

    L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
        maxZoom: 18,
        attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, ' +
            'Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        id: 'mapbox/streets-v11',
        tileSize: 512,
        zoomOffset: -1
    }).addTo(mymap);

    // Read POI data from file =================================================

    function readTextFile(file)
    {
        var rawFile = new XMLHttpRequest();
        rawFile.open("GET", file, false);
        rawFile.onreadystatechange = function ()
        {
            if(rawFile.readyState === 4)
            {
                if(rawFile.status === 200 || rawFile.status == 0)
                {
                    var allText = rawFile.responseText;
                    poi_text = allText;
                }
            }
        }
        rawFile.send(null);
    }

    // Show the POIs on the map ================================================

    var poi_text
    readTextFile("map.data");
    var lines = poi_text.split(";");
    lines.forEach(function(entry) {
        var poi = entry.split("|");
        var pop_head = poi[0];
        var pop_body = poi[1];
        var lat = poi[2];
        var lon = poi[3];
        var pop_url = poi[4];

        if (pop_url != '') {
            var pop_url = "<br/><a href='https://yourwebsite.de/index.php/" + pop_url + "' target='_top'>Link zum Artikel</a>";
        } else {
            var pop_url = '';
        }

        if (typeof lat != 'undefined') {
            var pop_text = "<b>" + pop_head + "</b><br/>" + pop_body + pop_url;
            L.marker([lat, lon]).addTo(mymap).bindPopup(pop_text);
            };
        });
</script>
</body>
</html>

Den <head> Teil könnt ihr so übernehmen wie er ist; dort wird Leaflet initialisiert. Im <body> wird am Anfang die Grösse der Karte bestimmt. Dort müsst ihr schauen, ob die Masse zu eurem IFrame passen. Im <script> Teil könnt ihr bei var mymap den Ausschnitt und den Zoom-Faktor für die Karte bestimmen. Dann wird mit L.tileLayer die OSM-Karte bei MapBox geholt.

Nun kommt die Funktion readTextFile, mit der die Daten für die Punkte gelesen werden. Der Aufbau dieser Datei sieht so aus:

Riga|Oktober 2014|56.9566|24.1373|reisen/schwarze-katzen-und-schwarzer-schnaps;
Rom|März 2007|41.8945|12.4775|reisen/auf-nach-rom;
Paris|Mai 2008|48.8584|2.3523|reisen/die-stadt-der-liebe;
Sankt Petersburg|März 2008,Juni 2013|59.9237|30.3127|reisen/sankt-petersburg;
Fuertventura|Juli 2008|28.0780|-14.2997|;

Jeder Eintrag besteht aus fünf Elementen: Ort, Datum, Breitengrad (lat), Längengrad (lon), URL zum Artikel. Die URL kann weggelassen werden. Alle Elemente sind durch den Pipe-Separator getrennt. Ein Eintrag endet mit einem Semikolon.

Hinweis: readTextFile verwendet die veraltete Klasse XMLHttpRequest. Das macht man besser mit fetch:

<!DOCTYPE html>
<html><body>
<p id="demo">Fetch a file to change this text.</p>
<script>
let file = "map.data"
fetch (file)
.then(x => x.text())
.then(y => document.getElementById("demo").innerHTML = y);
</script></body></html>

Leider ist es mir nicht gelungen, die Daten aus x auszulesen. Hinweise von euch sind sehr willkommen.

Die Funktion readTextFile liefert einen Textstring zurück, der anschliessend im Teil 'Show the POIs on the map' in seine Bestandteile zerlegt wird. Dann wird geprüft, ob es eine Artikel-URL gibt. Anschliessend werden die Punkte auf die Karte gesetzt und das Popup definiert, welches erscheint, wenn man auf einen Punkt klickt.

Wenn ihr den Code für eigene Zwecke verwenden möchtet, empfiehlt es sich, diesen Zeile für Zeile durchzugehen, und die Angaben auf eure Bedürfnisse anzupassen. Dann ladet ihr beide Dateien auf den Webserver hoch und bindet die HTML-Datei als IFrame in die Seite ein, die die Karte enthalten soll.

Verbesserungsvorschläge nehme ich gerne entgegen.