Zum Inhalt springen

Erste Schritte mit react-map-gl und MapLibre

Einrichten der Umgebung

Zu Beginn richten wir die React-Anwendung ein und besorgen uns alle notwendigen Abhängigkeiten. Dazu führen wir die folgenden Befehle aus, die ich nachfolgend erkläre.

npx create-react-app maplibre-app
cd maplibre-app
npm install
npm install maplibre-gl
npm install react-map-gl
npm install react-app-rewired --save-dev

Details zur Einrichtung

npx create-react-app maplibre-app

npx create-react-app maplibre-app
cd maplibre-app
...
...
...

Mit dem Befehl npx create-react-app maplibre-app erstellen wir eine React Anwendung mit dem Namen maplibre-app. create-react-app ermöglicht das Erstellen einer React-Applikation über die Eingabe eines einzelnen Befehls. Eine auf diese Weise erstellte App verfügt bereits über viele Entwicklerwerkzeuge wie Webpack, ESLint oder Babel. Du kannst dich somit voll auf die eigentliche Programmieraufgabe konzentrieren.

Was ist NPX? An dieser Stelle möchte ich kurz erklären, was Node und NPM sind. Node ermöglicht es uns, JavaScript außerhalb eines Browsers auszuführen. Es ermöglicht uns auch, JavaScript auf der Server-Seite auszuführen. NPM steht für Node Package Manager und ist ein Tool, mit dem wir Node-Pakete als Abhängigkeiten installieren und verwalten können. NPX ist ein NPM-Package-Runner mit dem vereinfacht ausgedrückt Node-Pakete ausführbar sind, ohne sie installieren zu müssen. Warum NPX verwenden? Zum einen ist es mit NPX nicht erforderlich, Software zu installieren, die man nur einmal benötigt. Zum anderen greift man so jederzeit auf die aktuellste Version zu.

npm install maplibre-gl

...
...
npm install maplibre-gl
...
...

MapLibre GL ist ein von der Community geführter Fork, der von mapbox-gl-js abgeleitet wurde, bevor diese zu einer Nicht-OSS-Lizenz wechselten.

npm install react-map-gl

...
...
...
npm install react-map-gl
...

react-map-gl ist eine Suite von React-Komponenten, die entwickelt wurde, um eine React-API für Mapbox GL JS-kompatible Bibliotheken bereitzustellen.

Mit v2.0 und höher ist es nicht mehr erlaubt mapbox-gl ohne Mapbox Token zu verwenden. Auch dann nicht, wenn man eine eigene Karte verwendet. Eine Alternative: Verwende einen freien Fork von mapbox-gl, zum Beispiel maplibre-gl. In der Dokumentation ist beschrieben, was dabei zu beachten ist. Im Grunde genommen reicht es aus, einen Alias zu erstellen, der mapbox-gl in maplibre-gl verändert. Falls du diesem Beispiel folgst, reicht die im nächsten Schritt beschriebene Konfiguration mit react-app-rewired aus.

npm install react-app-rewired —save-dev

--save-dev wird als Parameter von npm install verwendet, um das Paket react-app-rewired ausschließlich für die Entwicklung zu speichern.

...
...
...
...
npm install react-app-rewired --save-dev

Der Vorteil der create-react-app ist, dass die Konfiguration der Werkzeuge für uns übernommen wird. Als Nachteil nehmen wir in Kauf, dass die Einstellungen nicht einfach individuell anpassbar sind. Alles ist aber überschreibbar. Hierfür nutzen wir react-app-rewired.

Erstelle im Stammverzeichnis deiner Anwendung eine Datei mit dem Namen config-overrides.js und folgendem Inhalt.

// https://raw.githubusercontent.com/astridx/maplibre-app/main/config-overrides.js
module.exports = function override(config, env) {
  config.module.rules.push({
    resolve: {
      alias: {
        ...config.resolve.alias,
        'mapbox-gl': 'maplibre-gl',
      },
    },
  })

  return config
}

Passe als nächstes die Skripte in der Datei package.json so an, dass dein Override mithilfe von react-app-rewired verwendet wird.

package.json
...
  "scripts": {
+    "start": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-app-rewired test",
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
...

Die Konfiguration ist fertig. Im nächsten Schritt löschen wir Inhalte, die wir nicht benötigen.

Entfernen nicht benötigter Inhalte

Wir räumen auf. Dazu löschen wir alle Inhalte im src-Ordner, mit Ausnahme der Datei index.js. Den Inhalt der Datei index.js minimieren wir auf die nachfolgenden zwei Zeilen.

// https://raw.githubusercontent.com/astridx/maplibre-app/main/src/index.js

import React from 'react'
\
import ReactDOM from 'react-dom'
ReactDOM.render(<></>, document.getElementById('root'))

Jetzt ist die Anwendung starklar.

Anwendung starten

Starte nun die Anwendung.

npm start

Im Browser siehst du unter http://localhost:3000/ noch keine Ausgabe. Dies ändert sich im im nächsten Kapitel.

Den fertigen Code zu diesem ersten Abschnitt findest du auf Github.

Einrichten der MapLibre-Karte

Die Karte integrieren

Lege im src-Ordner die Datei Map.js an, die wir für die Anzeige der Karte verwenden:

// Map.js

import React from 'react'
import ReactMapGL from 'react-map-gl'

export const Map = () => {
  return (
    <ReactMapGL mapStyle="https://api.maptiler.com/maps/streets/style.json?key=my_key"></ReactMapGL>
  )
}

Ich nutze ein Maptiler Token in diesem Tutorial an der Stelle https://api.maptiler.com/maps/streets/style.json?key=my_key, und implementiere die Karte mit MapLibre und MapTiler. Wenn du das Beispiel selbst nachvollziehen möchtest, erstelle dir bitte einen eigenen Zugangsschlüssel.

Hinweis: Zum Lernen, ist das Einfügen des Token in den Quellcode eine Vereinfachung. In einem Echtsystem sollte es nicht im Quellcode zu sehen sein. Integriere dieses per Umgebungsvariable.

Karte integrieren

Importiere die Komponente, welche die Karte anzeigt, in der Datei index.js.

// index.js
import React from "react";
import ReactDOM from "react-dom";
-ReactDOM.render(<></>, document.getElementById('root'));
+import { Map } from "./Map";
+
+ReactDOM.render(<Map />, document.getElementById("root"));

Status der Karte

Bisher siehst du noch keine Karte. In react-map-gl wird der Kartenstatus über das Viewport-Objekt verwaltet. Es enthält alle Informationen zum Zustand der Karte wie Koordinaten, Zoom, Neigung, Größe im Browser. Um dies zu verwalten, nutzen wir den useState-Hook von React.

// Map.js
-import React from "react";
+import React, { useState } from "react";
import ReactMapGL from "react-map-gl";

export const Map = () => {
+  const [mapViewport, setMapViewport] = useState({
+    height: "100vh",
+    width: "100wh",
+    longitude: 7.571606,
+    latitude: 50.226913,
+    zoom: 4
+  });

  return (
    <ReactMapGL
+      {...mapViewport}
      mapStyle="https://api.maptiler.com/maps/streets/style.json?key=my_key"
+      onViewportChange={setMapViewport}
    ></ReactMapGL>
  );
};

Jedes Mal, wenn sich einer der Viewport-Werte in der Karte ändert, wird onViewportChange ausgelöst, wodurch unsere Statuswerte aktualisiert werden.

Den fertigen Code zu diesem Abschnitt findest du auf Github.

Marker auf der MapLibre-Karte

Hooks

In diesem Beispiel werden wir die Hooks useContext, useReducer und createContext nutzen. Zunächst implementieren wir diese in der Datei src/hooks/mapHook.js an.

// mapHook.js
import React, { createContext, useContext, useReducer } from "react";

const MapStateContext = createContext();
const MapDispatchContext = createContext();

export const MapProvider = ({ children }) => {
  const [state, dispatch] = useReducer(MapReducer, { markers: [] });
  return (
    <MapStateContext.Provider value={state}>
      <MapDispatchContext.Provider value={dispatch}>
        {children}
      </MapDispatchContext.Provider>
    </MapStateContext.Provider>
  );
};

export const useStateMap = () => {
  const context = useContext(MapStateContext);

  if (context === undefined) {
    throw new Error("useStateMap must be used within a MapProvider");
  }
  return context;
};

export const useDispatchMap = () => {
  const context = useContext(MapDispatchContext);

  if (context === undefined) {
    throw new Error("useDispatchMap must be used within a MapProvider");
  }
  return context;
};

export const MapReducer = (state, action) => {
  ...
  return state;
};

Der komplizierte Aufbau ermöglicht es uns, während der Laufzeit ein erneutes Laden der Karte zu minimieren.

Wie man React Context effektiv nutzt erklärt der verlinkte Beitrag.

Reducer

In unserer Anwendung werden wir Marker zur Karte hinzufügen und entfernen. Folgerichtig erstellen wir zwei Aktionen für unseren Reducer ADD_MARKER und REMOVE_MARKER.

// mapHook.js
export const MapReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_MARKER':
      return {
        ...state,
        markers: [...state.markers, action.payload.marker],
      }
    case 'REMOVE_MARKER':
      return {
        ...state,
        markers: [
          ...state.markers.filter(
            (x) =>
              x[0] !== action.payload.marker[0] &&
              x[1] !== action.payload.marker[1]
          ),
        ],
      }
  }
  return state
}

Einbinden in die Anwendung

Es fehlt noch die Integration in die Anwendung. Diese implementieren wir, indem wir die Map-Komponente als Kind-Komponente in MapProvider integrieren.

//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Map } from './Map'
import { MapProvider } from './hooks/mapHook'

ReactDOM.render(
  +(
    <MapProvider>
      <Map />+{' '}
    </MapProvider>
  ),
  document.getElementById('root')
)

Marker dynamisch verwalten

In unserer Anwendung wollen wir ADD_MARKER jedes Mal aufrufen, wenn der Benutzer mit der linken Maustaste auf die Karte klickt.

//map.js
import React, { useState } from "react";
import ReactMapGL from "react-map-gl";

export const Map = () => {
  const [mapViewport, setMapViewport] = useState({
    height: "100vh",
    width: "100wh",
    longitude: 7.571606,
    latitude: 50.226913,
    zoom: 4
  });

  return (
    <ReactMapGL
      {...mapViewport}
      mapStyle="https://api.maptiler.com/maps/streets/style.json?key=my_key"
      onViewportChange={setMapViewport}
+      onClick={x => {
+        x.srcEvent.which === 1 &&
+          mapDispatch({ type: "ADD_MARKER", payload: { marker: x.lngLat } });
+      }}
    >
        <MarkerList />
    </ReactMapGL>
  );
};

Der nächste Schritt ist die Darstellung der Markern an den gespeicherten Koordinaten. Erstellen wir hierfür die zwei neuen Komponenten Marker und MarkerList.

//Marker/marker.js

import React from 'react'
import { Marker as MapMarker } from 'react-map-gl'

export const Marker = ({ marker, handleRemove }) => {
  return (
    <MapMarker
      offsetTop={-48}
      offsetLeft={-24}
      latitude={marker[1]}
      longitude={marker[0]}
    >
      <img
        onContextMenu={(x) => {
          x.preventDefault()
          handleRemove()
        }}
        src="https://img.icons8.com/color/48/000000/marker.png"
      />
    </MapMarker>
  )
}
//Marker/markerList.js

import React from 'react'
import { Marker } from './Marker'
import { useStateMap, useDispatchMap } from '../hooks/mapHook'

export const MarkerList = () => {
  const { markers } = useStateMap()
  const mapDispatch = useDispatchMap()
  return (
    <>
      {markers?.map((marker, index) => (
        <Marker
          key={index}
          marker={marker}
          handleRemove={() =>
            mapDispatch({ type: 'REMOVE_MARKER', payload: { marker } })
          }
        />
      ))}
    </>
  )
}

Standardmäßig werden Marker in der linken oberen Ecke verankert. Praktischerweise gibt es die Eigenschaften für den Marker-Offset, so dass wir die Marker wenn erforderlich etwas verschieben können.

offsetTop={-48}}
offsetLeft={-24}

Nun ist die Anzeige in der Karte mittels <MarkerList /> möglich.

Den fertigen Code zu diesem Abschnitt findest du auf Github.