Projekte • 05. September 2023

MapComponents: Wie ich mit Hilfe eines nutzerfreundlichen Open-Source-Frameworks als Azubi bereits nach wenigen Monaten eine visuell ansprechende Anwendung bauen konnte

Hallo! Mein Name ist Tobias. Ich habe im September 2022 meine Ausbildung zum Anwendungsentwickler bei der WhereGroup begonnen. Nach einer kurzen, aber intensiven Einarbeitungsphase inklusive JavaScript- und React Crashkurs bin ich im noch recht jungen MapComponents Team gelandet, wo auch mein Freiburger Azubi-Kollege Martin arbeitet. Aus seinen Federn stammt der Blogartikel „Die Erstellung einer React-App mit MapComponents“, den man auf jeden Fall gelesen haben sollte! Und um die Frage vorwegzunehmen: Ja, ich bin der neue Azubi, den Martin in diesem Artikel in die Welt von MapComponents eingeführt hat. Ohne Frage hat er dabei einen phantastischen Job geleistet.

Nachdem wir die PowerPlantsApp abgeschlossen hatten, war ich auf das Ergebnis unserer Arbeit mächtig stolz, auch wenn Martin mir dabei natürlich sehr viel geholfen hat.

Ich war motiviert, den gelernten Stoff noch einmal in der Praxis zu wiederholen und eine App zu entwerfen, die zumindest zu einem großen Teil von mir selbst konzeptioniert und geschrieben sein würde. Beim Durchstöbern der Möglichkeiten die MapComponents bietet, wurde ich schnell fündig: Eine HeatMap-Anwendung mithilfe des MapLibre-GeoJsonLayer Components sollte es sein! Passend dazu hatte Martin auch gerade ein neues ‘Temporal Navigation Component’ entworfen, und ich war mir sicher, dass sich dies exzellent mit dem Heatmap-GeoJson Layer verbinden lassen würde.

Doch was wollte ich mithilfe der Heatmap darstellen? Feinstaubpartikel? Die Ausbreitung des Corona-Virus? Nein, ein etwas positiveres Thema sollte her. Als ausgebildeter Umweltwissenschaftler kam mir recht schnell ein Thema in den Kopf, welches über die letzten Jahre immer wieder in den Medien aufgetaucht war: Die Ausbreitung des Wolfs in Deutschland und in Europa. Sicherlich ist dies für Nutztierhalter in den betroffenen Regionen auch mit Herausforderungen verbunden, doch wie ich finde kann man sagen, dass die Rückkehr dieses majestätischen (und für den Menschen sehr ungefährlichen) Karnivoren in seinen ursprünglichen Lebensraum etwas Positives ist.

...eine Entscheidung, die ich bereits nach kurzer Zeit bereuen sollte…

Für die Energiekraftwerke hatte uns ein fertiger Datensatz zur Verfügung gestanden, doch genaue Zahlen über Wölfe in verschiedenen Jahren in unterschiedlichen Regionen Europas zu finden stellte sich als deutlich schwerer heraus. Nach einiger Zeit der Recherche, Email-Korrespondenz mit Monitoring-Instituten und jeder Menge Extrapolation hatte ich dann aber endlich einen brauchbaren – wenn auch wahrscheinlich sehr ungenauen - Datensatz zur Hand, und die eigentliche Arbeit konnte endlich losgehen.

Schritt 1: Erstellen der React-App

Dieser recht unkomplizierte Schritt zur Initialisierung der App wurde im Blogartikel zur Powerplants-App von Martin bereits im Detail beschrieben. Bei mir war dieser Schritt damals identisch, jedoch sollte man beachten, dass Create React App nicht mehr gewartet wird und daher für die Initialisierung das neue vite Template verwendet werden sollte.

Beim Erstellen der Anwendung wird das Template von MapComponents (@mapcomponents/react-maplibre) verwendet, welches die grundlegenden Einstellungen bereits beinhaltet. Dies umfasst unter anderem eine Hintergrundkarte sowie einige Elemente zum Navigieren auf jener Karte.

Schritt 2: Laden der Daten und erste Visualisierung

Auch beim Laden der Daten ist MapComponents wieder sehr hilfreich: Die mühsam zusammengetragene und extrapolierte Exceltabelle lässt sich, zum Beispiel mit verschiedensten kostenfreien Online-Tools, ganz einfach in eine GeoJSON Datei umwandeln.

Bei einem GeoJSON handelt es sich um eine JSON-Datei bei der in jedem Objekt noch geometrische Informationen - in meinem Fall Koordinaten - gespeichert sind. Diese Datei lässt sich mithilfe der MapComponents-eigenen Komponente “MlGeoJsonLayer” ganz einfach visualisieren. MlGeoJsonLayer bietet dabei viele verschiedene Arten der Visualisierung von GeoJSON Objekten an: Über die Eigenschaft “type” lassen sich verschiedene Visualisierungsformen wie “symbol”, “fill”, “circle” oder “line” auswählen. Für meine Vorstellung, wie die Anwendung am Ende aussehen sollte, war das “heatmap”-Property am besten geeignet.

Konkret umsetzten ließ sich dies durch das Anlegen einer einfachen Variable mit Hilfe der ‘useState’-Hook, welche von React zur Verfügung gestellt wird.

const [wolfData, setWolfData] = useState();

Unter Verwendung der ‘useEffect’-Hook lassen sich die Daten dann ganz einfach mit einem fetch-Befehl der Variable zuweisen:

  // For Heatmap Data display
   useEffect(() => {
      fetch("assets/wolves_all.json")
         .then(function (response) {
            return response.json();
         })
         .then(function (json) {
            setWolfData(json);
         });
   }, []);

Grundlegend funktioniert die Visualisierung mit Hilfe des MlGeoJsonLayer-Properties von MapComponents dann sehr intuitiv: Im return-statement wird das MlGeoJsonLayer eingefügt, in dessen Eigenschaften man unter “geojson” den zu visualisierenden Datensatz angibt. Weiterhin können eine große Menge Konfigurationsparameter wie zum Beispiel der Farbverlauf, die Farbintensität, der Radius der einzelnen Punkte sowie ein Grad an Durchsichtigkeit für die Heatmap mit angegeben werden.

 {wolfData && (
            <MlGeoJsonLayer
               mapId="map_1"
               type="heatmap"
               layerId="Wolf_map"
               geojson={wolfData}
               paint={{
                  "heatmap-color": [
                     "interpolate",
                     ["exponential", 8],
                     ["heatmap-density"],
                     0,
                     "rgba(0, 0, 255, 0)",
                     0.1,
                     "#72ff62",
                     0.3,
                     "#c4ff40",
                     0.6,
                     "#ffc020",
                     0.75,
                     "#ff8500",
                     0.95,
                     "#F00",
                  ],
                  "heatmap-intensity": {
                     stops: [
                        [1, 0.1],
                        [2, 0.25],
                        [3, 0.3],
                        [4, 0.45],
                        [5, 0.7],
                        [7, 0.8],
                        [8, 0.9],
                        [10, 1.0],
                     ],
                  },
                  "heatmap-opacity": 0.5,
                  "heatmap-weight": 0.8,
                  "heatmap-radius": [

Das Ergebnis war zwar bereits eine Heatmap, jedoch war die Karte an diesem Punkt noch komplett statisch: Es wurde der komplette Datensatz abgebildet, unabhängig vom jeweiligen Jahr. Die Implementierung der zeitlichen Komponente war also der nächste logische Schritt.

Schritt 3: Die zeitliche Komponente und erste Interaktivität

Dass die gesamte Wolfspopulation zwischen 2012 und 2022 gleichzeitig dargestellt wurde, war natürlich so nicht gewollt, und es brauchte ein Element, mit dem sich die Darstellung filtrieren und steuern ließ. Diese Hürde war mir bereits bei der Konzeption der Anwendung bewusst, und glücklicherweise hatte Martin kurz zuvor eine neue Komponente für MapComponents Entwickelt, die genau für derartige Probleme zugeschnitten war: Den MlTemporalController.

Zuerst musste der Datensatz nach den jeweiligen Jahren (2012-2022) gefiltert werden. Da diese Information für mehr als eine Komponente relevant sein würde, reichte eine einfache „useState“-Hook nicht aus – stattdessen verwendete ich die „useContext“-Hook, um die Information an alle Komponenten weitergeben zu können. Die relevanten Informationen wurden in einer zusätzlichen Komponente namens AppContext gespeichert und können jederzeit dynamisch verändert werden. „useContext“ bietet die Möglichkeit, von jeder Komponente aus auf die Informationen von AppContext.js zugreifen zu können, ohne dass die Variablen mittels „prop-drilling“ durch den React-DOM gereicht werden müssen.

import { createContext } from "react";
import { useState } from "react";

const AppContext = createContext({});

const AppContextProvider = ({ children }) => {
  const [selectedCountry, setSelectedCountry] = useState();
  const [selectedCountryID, setSelectedCountryID] = useState();
  const [currentYear, setCurrentYear] = useState();

  const value = {
     selectedcountry: selectedCountry,
     setselectedcountry: setSelectedCountry,
     selectedCountryID: selectedCountryID,
     setSelectedCountryID: setSelectedCountryID,
     setCurrentYear: setCurrentYear,
     currentYear: currentYear,
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

export default AppContext;
export { AppContextProvider };

Per useState-Hook definierte ich dann noch eine Variable „currentYear“ mit dem darzustellenden Jahr, deren Anfangswert ich auf 2012 festlegte.

function DataLayer() {
   const [currentYear, setCurrentYear] = useState(2012);
   const mapHook = useMap({ mapId: "map_1" });

   const appContext = useContext(AppContext);

Eine weitere „useEffect“-Hook sorgte dafür, dass das korrekte Jahr in dieser useState-Variable gespeichert wird, wenn ein neues Jahr ausgewählt wurde. Die Karte war jetzt so weit, dass sie nur noch die Wolfspopulation des Jahres anzeigte, welches in der Variable „currentYear“ der useState-Hook festgelegt wurde.

Zur Auswahl der Anzeige der jeweiligen Jahre kam nun der MlTemporalController ins Spiel – und dessen Integration gestaltete sich fast schon überraschend einfach: Importieren des MlTemporalControllers aus der von MapComponents-Bibliothek, den TemporalController im JSX einfügen und die für die Komponente benötigten Parameter angeben.

{wolfData && (
            <MlTemporalController
               className="TempController"
               fitBounds={false}
               step={1.0}
               interval={1000}
               displayCurrentValue={true}
               geojson={wolfData}
               initialVal={currentYear}
               timeField={"Year"}
               ownLayer={false}
               onStateChange={getValues}
            />
         )}

An diesem Punkt war die Hauptfunktion der Karte gegeben: Mithilfe des TemporalControllers erwachte die Heatmap zum Leben und es war möglich, die Dynamik der Populationsverbreitung für jedes einzelne Jahr über den gesamten Zehnjahresverlauf zu betrachten.

Abgesehen davon bot die Anwendung allerdings noch keine Interaktivität – dies wollte ich im nächsten Schritt ändern.

Schritt 4: Einzelne Länder, die Sidebar und weitere Interaktive Elemente

Bis hier zeigte die Anwendung ausschließlich die grafische Darstellung der Wolfspopulation in Europa an. Es war wünschenswert, einzelne Länder auswählen zu können und detailliertere Informationen abzufragen. Eine aufklappbare Sidebar, welche ein Drop-down Menü zur Länderauswahl anbietet und weitere Informationen darstellt, erschien mir als intuitive, benutzerfreundliche Lösung. Für diesen Zweck war es notwendig, einen weiteren Datensatz anzulegen, welcher die Anzahl der Wölfe pro Jahr in jedem der im ursprünglichen Datensatz enthaltenen Länder festhielt. Also gut – zurück zu QGIS, den ursprünglichen Datensatz laden, einen Layer mit den Grenzen der darzustellenden Länder heraussuchen, beide Layer verknüpfen und die Punkte nach Ländern sowie Jahren filtrieren.

Mit diesem Datensatz zur Hand konnte ich mich jetzt an die Entwicklung der Sidebar machen. Durch die useContext-Hook konnte ich spielend leicht auf alle benötigten Informationen zugreifen. Ich initialisierte einen einfachen Info-Button zum Öffnen der Sidebar und richtete eine zusätzliche Komponente namens „DropdownMenu“, welche – wie der Name schon vorweg nimmt – ein Dropdown Menü mit allen im Datensatz enthaltenen Ländern initialisiert. All das war weniger schwer als es klingen mag, da die Open-Source React Library MaterialUI bereits alle notwendigen Komponenten zur Verfügung stellt – es galt also nur, diese zu importieren. Vielen Dank, MUI!

Ich integrierte das DropdownMenu-Component problemlos in das JSX return-statement der Sidebar, und es war direkt dort zu sehen – nur leider noch ohne die gewünschte Funktion: Ich wollte, dass die Ansicht auf das ausgewählte Land wechselt, und nur noch die Population des gewählten Landes dargestellt wird.

Der neue Datensatz musste also nach dem ausgewählten Land und dem jeweiligen Jahr gefiltert werden. Per „Callback“ ließ sich das Dropdown Menü als das Element einrichten, welches für die useState-Variable „selectedCountry“ einen String als Wert auswählen konnte.

<DropDownMenu callback={getCountry} />

Die Anzahl der Wölfe speicherte ich dann per fetch-Befehl aus dem neuen Datensatz in einer eigenen useState-Variable. Im Data Layer filterte ich die darzustellende Population abschließend nach ihrem jeweiligen Land und nutzte die in MapComponents integrierte „FlyTo“ Funktion, um jedes ausgewählte Land automatisch zu zentrieren. Hierfür musste im Datensatz für jedes Land ein eigenes Zoomlevel initialisiert werden, da z.B. Luxemburg eine höhere Zoomstufe benötigt als Finnland, Schweden oder Italien.

function getCountry(value) {
      setSelectedCountry(value);
   }

   const [wolfSum, setWolfSum] = useState();

   useEffect(() => {
      fetch("assets/wolf_aggregate.json")
         .then(function (response) {
            return response.json();
         })
         .then(function (json) {
            setWolfSum(json);
         });
   }, []);

   useEffect(() => {
      setYear(appContext.currentYear);
   }, [appContext.currentYear]);

   var filteredData = wolfSum?.features.filter((item) => {
      if (selectedCountry === undefined) {
         return item.properties.Country === "all_of_europe";
      } else {
         return item.properties.Country === selectedCountry;
      }
   });

Fetchen des DatenSatzes in useState-Variable namens "WolfSum"

useLayerFilter({
      layerId: "Wolf_map",
      filter: ["==", "Country", selected],
   });

   useEffect(() => {
      if (
         countryData &&
         countryData.features &&
         countryData.features[countryID]
      ) {
         setCurrentYear(2012);
         const { Longitude, Latitude } =
            countryData.features[countryID].properties;
         mapHook.map?.map.flyTo({
            center: new mapboxgl.LngLat(
               parseFloat(Longitude),
               parseFloat(Latitude)
            ),
            zoom: countryData.features[countryID].properties.Zoom,
         });
      }
   }, [countryID, countryData, mapHook.map]);

UseEffect-Hook mit "FlyTo-Funktion"

Zu guter Letzt verschönerte ich die Sidebar noch mit einer Infografik, welche mit Hilfe von recharts.org - einer Open-Source Bibliothek zur Darstellung von Daten und Statistiken - erstellt wurde und integrierte eine Anzeige für die Anzahl der Wölfe außerhalb der Sidebar in der oberen rechten Ecke der Anwendung.

Ich baute auch noch einen Button ein, mit dem die Hintergrundkarte von Nacht auf Tag umgestellt werden konnte. Diesen konnte ich mit ein paar Anpassungen von Martin und meiner vorherigen PowerPlants-Anwendung übernehmen.

Mit diesen letzten kosmetischen Details war die Anwendung fertiggestellt und sie konnte in den Katalog der MapComponents Beispielanwendungen hinzugefügt werden, wo man sie auch aktuell finden und ausprobieren kann.

Fazit

Der gesamte Prozess hat mir großen Spaß gemacht: Das Thema war interessant und der Prozess des Programmierens der Anwendung war motivierend, da nach jedem Schritt ein deutlicher Fortschritt zu erkennen war und die App nach und nach immer mehr zum Leben erwachte.

Natürlich stand ich immer mal wieder vor Problemen und musste das ein oder andere mal Hilfe erfragen (erneut vielen Dank an Martin), doch der Lernfortschritt im Vergleich zu meiner Arbeit an der PowerPlants-App war enorm und ich bin jedes Mal sehr glücklich, die App im MapComponents Katalog zu sehen.
Vor allen Dingen aber zeigte mir der Prozess noch einmal die beeindruckende Vielfalt an Möglichkeiten auf, welche MapComponents für das Programmieren von GIS-Anwendungen bietet.

Hier können Sie meine MapComponents-Anwendung testen: Wolf-App

Weitere Beiträge, die Dich interessieren könnten:

Tobias Drisch

Tobias (B.Sc. Umweltwissenschaften) ist seit September 2022 Auszubildender bei der WhereGroup am Standort Freiburg und seitdem im Entwicklerteam von MapComponents beteiligt.

Artikel teilen: