1

I am trying to implement OpenLayers6 ("ol": "^6.14.1") into a ReactJS project, but all the documentation is created in normal JS files and I can't find any good examples or tutorials with functional components and OpenLayers6.

I have troubles figuring out how to implement the majority of things, because the docs seem to ignore the lifecycle of React.

What I've managed to do until now is to add a marker and a popup right above the marker; to close the popup and delete the marker by deleting the whole vector layer(which seems overkill).

import { useState, useEffect, useRef } from 'react';
// import ModalUI from '../UI/ModalUI';
import classes from './MapUI.module.css';
import { drawerActions } from '../../store/drawer-slice';

import 'ol/ol.css';
import { Map, View, Overlay, Feature } from 'ol';
import Point from 'ol/geom/Point';
import { Vector as VectorLayer } from 'ol/layer';
import VectorSource from 'ol/source/Vector';
import { fromLonLat, toLonLat } from 'ol/proj';
import { toStringHDMS } from 'ol/coordinate';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';

import PopUp from './PopUp';
import { useDispatch } from 'react-redux';

export default function MapUI() {
  const mapRef = useRef();
  const popup = useRef();
  const [coordinates, setCoordinates] = useState('');
  const [newMarker, setNewMarker] = useState(
    new Feature({
      geometry: new Point([[]]),
      name: '',
    })
  );
  const [newMarkersLayer, setNewMarkersLayer] = useState(
    new VectorLayer({
      properties: { name: 'newMarkers' },
      source: new VectorSource({
        features: [newMarker],
      }),
    })
  );

  const closePopup = () => {
    map.getOverlayById('map-popup').setPosition(undefined);
    map.removeLayer(newMarkersLayer);
  };

  const [map] = useState(
    new Map({
      target: '',
      layers: [
        new TileLayer({
          source: new OSM(),
        }),
        new VectorLayer({
          properties: { name: 'existingMarkers' },
          source: new VectorSource({
            // features: [marker],
          }),
        }),
      ],
      view: new View({
        center: fromLonLat([26.08, 44.46]),
        zoom: 15,
        minZoom: 10,
        maxZoom: 20,
      }),
    })
  );

  useEffect(() => {
    const overlay = new Overlay({
      element: popup.current,
      id: 'map-popup',
      autoPan: {
        animation: {
          duration: 250,
        },
      },
    });
    // console.log('useEffect in MapUI.jsx');

    map.addOverlay(overlay);
    map.setTarget(mapRef.current);
    map.on('singleclick', function (evt) {
      map.addLayer(newMarkersLayer);
      newMarker.getGeometry().setCoordinates(evt.coordinate);

      setCoordinates(toStringHDMS(toLonLat(evt.coordinate)));
      overlay.setPosition(evt.coordinate);
    });
  }, [map]);

  return (
    <>
      <div
        style={{ height: '100%', width: '100%' }}
        ref={mapRef}
        className='map-container'
      />
      <div id='map-popup' className={classes['ol-popup']} ref={popup}>
        <PopUp coordinates={coordinates} closePopup={closePopup} />
      </div>
    </>
  );
}

The project in the end will have an array of markers that will be requested from a back-end and will populate the given map while also keeping the ability to add new markers to the map (and to the back-end).

The general issue that I face is with how all ol objects are used in the documentation. Everything is just created in a file using const and then operated upon.

But in React I have to use useEffect() and useState() and can't just create dependencies or manipulate state however the docs say.

I am looking for some guidelines on how to properly use OpenLayers6 in React. On this note I have some questions:

  • How can I remove a marker without removing the whole layer ?
  • How can I make a marker stay on the map ?
  • How can I render an array or markers on the map ?
  • Is it correct the way I use useState() to create the initial map ?
  • Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
GeorgeMet
  • 35
  • 8
  • It would be better to use a ref to box the Map object, like [this](https://github.com/dietrichmax/openlayers-react-functional-component/blob/main/src/App.js) example I found by googling `react openlayers 6`. – AKX Jul 26 '22 at 13:37
  • Anyway – you would use `useEffect` to enact imperative effects on the `Map` based on your other state (e.g. marker data, coordinates, what-have-you). – AKX Jul 26 '22 at 13:40
  • I moved from using refs to using state for the map (I forgot why) and I don't understand the second comment `Anyway – you would use useEffect to enact imperative effects on the Map `. – GeorgeMet Jul 26 '22 at 13:53
  • More importantly, is there a way of removing a specified marker from a specified layer ? The only way I found is to remove the whole layer on which that marker is. But what if that layer is already populated with markers that should remain there. – GeorgeMet Jul 26 '22 at 14:06
  • pass null to popup .setMap(null), this will remove the overly from the map. – BR75 Jul 26 '22 at 17:01
  • Quite sure you'd modify the vector source for that layer - that's where you put the marker in, that's where you take it out..? – AKX Jul 26 '22 at 18:10

2 Answers2

2

Try using this library, Rlayers this was very helpfull for me to combine with another gis library, like turf and d3

How can I remove a marker without removing the whole layer ? You can use react-query as hooks (useMutation) fuction to call inside useCallback

How can I make a marker stay on the map ? What are you meaning about stay? is like show and not hide? just get state and put that's value into true

How can I render an array or markers on the map ? again use react-query

Is it correct the way I use useState() to create the initial map ? Yes if you have another map on your application

Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ? Yes

  • 1
    As the author of `rlayers` I am very happy to mark the first time it has been mentioned in a SO answer by another person. – mmomtchev Jul 29 '22 at 22:10
1

As you know, OpenLayers uses an imperative API where you add and remove features to layers, etc., while in React land, we generally do things declaratively. You'll need some imperative glue code that takes your React state and mutates the OpenLayers state to match what your React state is - that's what React itself does when it reconciles the DOM.

You can do that in a couple of different ways:

  • useEffects to do what useEffect does: imperative side effects (updating OL state) based on declarative inputs (React state); described below
  • The other option, used by e.g. react-leaflet, is that you describe the whole Leaflet state as React elements, and the library reconciles the Leaflet state based on that. It's a lot more work, but feels more React-y.

Here's a pair of custom hooks that make things a bit easier, loosely based on your example. It's in TypeScript, but if you need plain JS, just remove the TypeScript specific things (by e.g. running this through the typescriptlang.org transpiler).

  • useOpenLayersMap is a hook that sets up an OL Map in a ref and returns it, and takes care that a given container renders the map.
  • useOpenLayersMarkerSource is a hook that receives a list of marker data (here quite a limited definition, but you could amend it as you like), returns a VectorSource and also takes care that modifications to the list of marker data are reflected into the VectorSource as new Features.
  • Now, all the MapUI component needs to do is use those two hooks with valid initial data; it can modify its markerData state and the changes get reflected into the map.
import React, { useRef, useState } from "react";

import "ol/ol.css";
import { Feature, Map, View } from "ol";
import Point from "ol/geom/Point";
import { Vector as VectorLayer } from "ol/layer";
import VectorSource from "ol/source/Vector";
import { fromLonLat } from "ol/proj";
import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import { MapOptions } from "ol/PluggableMap";

interface MarkerDatum {
  lat: number;
  lon: number;
  name: string;
}

function convertMarkerDataToFeatures(markerData: MarkerDatum[]) {
  return markerData.map(
    (md) =>
      new Feature({
        geometry: new Point(fromLonLat([md.lon, md.lat])),
        name: md.name,
      }),
  );
}

const centerLat = 60.45242;
const centerLon = 22.27831;

function useOpenLayersMap(
  mapDivRef: React.MutableRefObject<HTMLDivElement | null>,
  getInitialOptions: () => Partial<MapOptions>,
) {
  const mapRef = useRef<Map | null>(null);
  React.useEffect(() => {
    if (!mapRef.current) {
      // markersSourceRef.current = new VectorSource();
      mapRef.current = new Map({
        target: mapDivRef.current ?? undefined,
        ...getInitialOptions(),
      });
    }
  }, []);
  React.useLayoutEffect(() => {
    if (mapRef.current && mapDivRef.current) {
      mapRef.current.setTarget(mapDivRef.current);
    }
  }, []);
  return mapRef.current;
}

function useOpenLayersMarkerSource(markerData: MarkerDatum[]) {
  const [markersSource] = useState(() => new VectorSource());
  React.useEffect(() => {
    // TODO: this would do better to only remove removed features,
    //       modify modified features and add new features
    markersSource.clear(true);
    markersSource.addFeatures(convertMarkerDataToFeatures(markerData));
  }, [markerData]);
  return markersSource;
}

export default function MapUI() {
  const mapDivRef = useRef<HTMLDivElement>(null);
  const [markerData, setMarkerData] = React.useState<MarkerDatum[]>([
    {
      lon: centerLon,
      lat: centerLat,
      name: "turku",
    },
  ]);
  const markerSource = useOpenLayersMarkerSource(markerData);
  const map = useOpenLayersMap(mapDivRef, () => ({
    layers: [
      new TileLayer({
        source: new OSM(),
      }),
      new VectorLayer({
        properties: { name: "existingMarkers" },
        source: markerSource,
      }),
    ],
    view: new View({
      center: fromLonLat([centerLon, centerLat]),
      zoom: 12,
      minZoom: 12,
      maxZoom: 20,
    }),
  }));

  const addNewFeature = React.useCallback(() => {
    setMarkerData((prevMarkerData) => [
      ...prevMarkerData,
      {
        lat: centerLat - 0.05 + Math.random() * 0.1,
        lon: centerLon - 0.05 + Math.random() * 0.1,
        name: "marker " + (0 | +new Date()).toString(36),
      },
    ]);
  }, []);

  return (
    <>
      <button onClick={addNewFeature}>Add new feature</button>
      <div
        style={{ height: "800px", width: "800px" }}
        ref={mapDivRef}
        className="map-container"
      />
    </>
  );
}
AKX
  • 152,115
  • 15
  • 115
  • 172