0
votes

I am currently displaying a Map, thanks to react-leaflet, with a GeoJSON Component. I'm also displaying some tooltips on hover over some countries and cities(for example, when I hover France, a tooltip display "France"). I'm also using i18n for internationalization. The internationalization works fine for the country tooltips, they are updated in real time.

I have a function updateDisplay, that switch between a GeoJson component for the countries, or a list of Marker for the cities, on zoom change.

The problem is, that when i'm switching languages, it works fine for the whole page, but not for the city tooltips. They are updated only when I zoom (so when the updateDisplay is called).

I would have the expected behaviour : regardless of the zoom, I would like that the city tooltips update in real time, when i switch language.

I hope I've made myself clear

Here is my code :

/**
 * Display a Leaflet Map, containing a GeoJson object, or a list of Markers, depending on the zoom
 */
export default function CustomMap(): ReactElement {
  const { t }: { t: TFunction } = useTranslation();

  const countryToString = (countries: string[]): string => countries.map(c => t(c)).join(", ");


  // Contains the json containing the polygons of the countries
  const data: geojson.FeatureCollection = geoJsonData as geojson.FeatureCollection;
  let geoJson: JSX.Element = <GeoJSON
    key='my-geojson'
    data={data}
    style={() => ({
      color: '#4a83ec',
      weight: 1,
      fillColor: "#1a1d62",
      fillOpacity: 0.25,
    })}
    onEachFeature={(feature: geojson.Feature<geojson.GeometryObject>, layer: Layer) => {
      layer.on({
        'mouseover': (e: LeafletMouseEvent) => {
          const country = countries[e.target.feature.properties.adm0_a3];
          layer.bindTooltip(countryToString(country.tooltip as string[]));
          layer.openTooltip(country.latlng);
        },
        'mouseout': () => {
          layer.unbindTooltip();
          layer.closeTooltip();
        },
      });
    }}
  />

  // Contains a list of marker for the cities
  const cityMarkers: JSX.Element[] = cities.map(
    (
      c: position,
      i: number
    ) => {
      return (
        // Here are the tooltips that doesn't update in real time, when we switch language
        // FIX ME
        <Marker key={c.latlng.lat + c.latlng.lng} position={c.latlng}>
          <Tooltip>{t(c.tooltip as string)}</Tooltip>
        </Marker>
      );
    }
  );

  const [state, setState] = useState<state>({
    zoom: 3,
    display: geoJson,
  });


  // Update on zoom change
  function onZoom(e: LeafletMouseEvent): void {
    const zoom = e.target._zoom;
    const newDisplay = updateDisplay(zoom);
    setState({
      ...state,
      zoom,
      display: newDisplay,
    });
  }

  // Called on every zoom change, in order to display either the GeoJson, or the cities Marker
  function updateDisplay(zoom: number): Marker[] | any {
    if (zoom >= 4) {
      return cityMarkers;
    } else {
      return geoJson;
    }
  }


  return (
    <Map
      style={{ height: "500px" }}
      center={[54.370138916189596, -29.918133437500003]}
      zoom={state.zoom}
      onZoomend={onZoom}
    >
      <TileLayer url="https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw" />
      {state.display}
    </Map>
  );
}

You can also look at it here : https://github.com/TheTisiboth/WebCV/blob/WIP/src/components/customMap.tsx
It is on the branch WIP

1

1 Answers

1
votes

You can do the following to overcome this issue:

  1. Create a boolean flag to keep in memory if the markers have been added
  2. Add the markers on the map using native leaflet code instead of react'leaflet's wrappers.

    • If the markers are added and zoom >= 4 set the flag to true
    • if zoom < 4 remove the markers to be able to show countries, set flag to false
  3. When language is changed, if zoom is bigger, equal than 4 and markers have been added remove the previous, add new ones with the new tooltip

you can achieve all these by holding a reference to the map instance.

Here is the whole code you will need, (parts of cities, markers removed):

import React, { useState, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { Map, Marker, TileLayer, GeoJSON } from "react-leaflet";
import geoJsonData from "../assets/geoJsonData.json";
import { LatLngLiteral, Layer, LeafletMouseEvent } from "leaflet";
import geojson from "geojson";
import { TFunction } from "i18next";
import L from "leaflet";

interface position {
  latlng: LatLngLiteral;
  tooltip: string;
}

interface state {
  markers: position[];
  zoom: number;
  display: position[] | any;
  geoJson: JSX.Element;
  countries: { [key: string]: position };
}

/**
 * Display a Leaflet Map, containing a GeoJson object, or a list of Markers, depending on the zoom
 */
export default function CustomMap(): ReactElement {
  const mapRef: any = React.useRef();
  const { t, i18n }: { t: TFunction; i18n: any } = useTranslation();
  const [markersAdded, setMarkersAdded] = useState(false);

  i18n.on("languageChanged", (lng: any) => {
    if (lng) {
      const map = mapRef.current;
      if (map && map.leafletElement.getZoom() >= 4 && markersAdded) {
        map.leafletElement.eachLayer(function (layer: L.Layer) {
          if (layer instanceof L.Marker) map.leafletElement.removeLayer(layer);
        });
        state.markers.map((c: position, i: number) => {
          L.marker(c.latlng)
            .addTo(map.leafletElement)
            .bindTooltip(t(c.tooltip));
        });
      }
    }
  });

  // const countryToString = (countries: string[]): string => countries.join(", ");

  // List of position and label of tooltip for the GeoJson object, for each country
  const countries: { [key: string]: position } = {
    DEU: {
      latlng: {
        lat: 51.0834196,
        lng: 10.4234469,
      },
      tooltip: "travel.germany",
    },
    CZE: {
      latlng: {
        lat: 49.667628,
        lng: 15.326962,
      },
      tooltip: "travel.tchequie",
    },
    BEL: {
      latlng: {
        lat: 50.6402809,
        lng: 4.6667145,
      },
      tooltip: "travel.belgium",
    },
  };

  // List of position and tooltip for the cities Markers
  const cities: position[] = [
    {
      latlng: {
        lat: 48.13825988769531,
        lng: 11.584508895874023,
      },
      tooltip: "travel.munich",
    },
    {
      latlng: {
        lat: 52.51763153076172,
        lng: 13.40965747833252,
      },
      tooltip: "travel.berlin",
    },
    {
      // greece
      latlng: {
        lat: 37.99076843261719,
        lng: 23.74122428894043,
      },
      tooltip: "travel.athens",
    },
    {
      // greece
      latlng: {
        lat: 37.938621520996094,
        lng: 22.92695426940918,
      },
      tooltip: "travel.corinth",
    },
  ];

  // Contains the json containing the polygons of the countries
  const data: geojson.FeatureCollection = geoJsonData as geojson.FeatureCollection;
  let geoJson: JSX.Element = (
    <GeoJSON
      key='my-geojson'
      data={data}
      style={() => ({
        color: "#4a83ec",
        weight: 1,
        fillColor: "#1a1d62",
        fillOpacity: 0.25,
      })}
      // PROBLEM : does not update the tooltips when we switch languages
      // FIX ME
      onEachFeature={(
        feature: geojson.Feature<geojson.GeometryObject>,
        layer: Layer
      ) => {
        layer.on({
          mouseover: (e: LeafletMouseEvent) => {
            const country =
              state.countries[e.target.feature.properties.adm0_a3];
            layer.bindTooltip(t(country?.tooltip));
            layer.openTooltip(country?.latlng);
          },
          mouseout: () => {
            layer.unbindTooltip();
            layer.closeTooltip();
          },
        });
      }}
    />
  );

  const [state, setState] = useState<state>({
    markers: cities,
    zoom: 3,
    geoJson: geoJson,
    display: geoJson,
    countries: countries,
  });

  // Update on zoom change
  function onZoom(e: LeafletMouseEvent): void {
    const zoom = e.target._zoom;
    const newDisplay = updateDisplay(zoom);
    setState({
      ...state,
      zoom,
      display: newDisplay,
    });
  }

  // Called on every zoom change, in order to display either the GeoJson, or the cities Marker
  function updateDisplay(zoom: number): Marker[] | any {
    const map = mapRef.current;
    if (zoom >= 4) {
      return state.markers.map((c: position, i: number) => {
        console.log(t(c.tooltip));
        if (map && !markersAdded) {
          console.log(map.leafletElement);
          L.marker(c.latlng)
            .addTo(map.leafletElement)
            .bindTooltip(t(c.tooltip));
          setMarkersAdded(true);
        }
      });
    } else {
      map.leafletElement.eachLayer(function (layer: L.Layer) {
        if (layer instanceof L.Marker) map.leafletElement.removeLayer(layer);
      });
      setMarkersAdded(false);
      return state.geoJson;
    }
  }

  return (
    <Map
      ref={mapRef}
      style={{ height: "500px" }}
      center={[54.370138916189596, -29.918133437500003]}
      zoom={state.zoom}
      onZoomend={onZoom}
    >
      <TileLayer url='https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw' />
      {state.display}
    </Map>
  );
}

Eng:

"travel": {
    "germany": "Munich, Berlin, Hambourg, Münster, Allemagne",
    "munich": "Munchen",
    "berlin": "Berlin",
    "tchequie": "Tchéquie, Prague",
    "belgium": "Belgique",
    "athens": "Athènes",
    "corinth": "Corinthe",
    ...
 }

Fr:

"travel": {
    "germany": "Munich, Berlin, Hamburg, Münster, Germany",
    "munich": "Munich",
    "berlin": "Berlin",
    "tchequie": "Czech Republic, Prague",
    "belgium": "Belgium",
    "athens": "Athens",
    "corinth": "Corinth",
     ...
 }

You can make it more clean by reusing the markers removal code chunk and markers addition code chunk respectively.