javascriptreactjsleafletreact-leafletleaflet-routing-machine

How to Use SVG Component in React Leaflet?


I converted the svg I created into a component. I'm sending color and a string or integer expression into it as props and I want to create a dynamic marker accordingly. enter image description here

I want my svg icon appearing in the top left to replace the blue markers on the map below.

TrackingMap.tsx

import React, { useState } from 'react';
//Third Party imports
import 'leaflet/dist/leaflet.css';
import { MapContainer, TileLayer } from 'react-leaflet';
import L from 'leaflet';

//Components imports
import Routing from './Routing';

//Utils import
import { MapWithPopupdatav2 } from './fakeData2';
import { CoordinatInterface, EachPointforRouting } from 'types/Map';



interface DefaultMapWithPopupProps {
  // dataSource: RootMapWithPopupData;
  height?: string;
  width?: string;
}

const TrackingMap = ({ height }: DefaultMapWithPopupProps) => {
  const [markersDataSource, setMarkersDataSource] = useState(
    MapWithPopupdatav2.data.map(item => item.gridData.data.map(item => item))
  );

  const [routeWayColor, setRouteWayColor] = useState<string[]>(
    MapWithPopupdatav2.data.map(item => item.color)
  );

  const [dataForRouting, setDataForRouting] = useState<EachPointforRouting[][]>(
    MapWithPopupdatav2.data.map(eachPoint =>
      eachPoint.gridData.data.map(point =>
        L.latLng(point.latitude, point.longitude)
      )
    )
  );

  const markersLatLon: CoordinatInterface[][] = MapWithPopupdatav2.data.map(
    eachPoint =>
      eachPoint.gridData.data.map(point => ({
        latitude: point.latitude,
        longitude: point.longitude,
      }))
  );
  function centerMapDataCalculate(data: CoordinatInterface[][]) {
    let newArray: CoordinatInterface[] = [];
    data.forEach(item => {
      item.map(point => {
        newArray.push(point);
      });
    });
    return newArray;
  }

  const markersCoordinatesForMapCentering: CoordinatInterface[] =
    centerMapDataCalculate(markersLatLon);
  return (
    <MapContainer scrollWheelZoom={true} style={{ height: `${height}` }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {dataForRouting.map((eachPoint, index) => {
        return (
          <React.Fragment key={index}>
            <Routing
              eachPoint={eachPoint}
              dataSource={markersDataSource[index]}
              color={routeWayColor[index]}
              bounds={markersCoordinatesForMapCentering}
            />
            <Marker />
          </React.Fragment>
        );
      })}
    </MapContainer>
  );
};

export default TrackingMap;

Routing.tsx

import { useEffect } from 'react';
//Third Party ─░mports
import L, { latLngBounds } from 'leaflet';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import 'leaflet-routing-machine';
import { useMap } from 'react-leaflet';

//Components
import { Marker } from 'components/icons';

//Utils import
import { RoutingPropsforTrackingMap } from 'types/Map';


L.Marker.prototype.options.icon = L.icon({
  iconUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon.png',
});

const Routing = ({
  eachPoint,
  dataSource,
  color,
  bounds,
}: RoutingPropsforTrackingMap) => {
  const map = useMap();
  let markerBounds = latLngBounds([]);
  useEffect(() => {
    if (!map) return;

    const routingControl = L.Routing.control({
      waypoints: eachPoint,
      createMarker: function (
        waypointIndex: any,
        waypoint: any,
        numberOfWaypoints: any
      ) {
        //  return L.marker(waypoint.latLng, { icon: svgIcon }).bindPopup(
        return L.marker(waypoint.latLng).bindPopup(
          `<p><b>Location Code:</b>${dataSource[waypointIndex].locationCode}</p>
          <p><b>Location Type:</b>${dataSource[waypointIndex].locationType}</p>
          <p><b>Visit Order:</b>${dataSource[waypointIndex].visitOrder}</p>
          `
        );
      },
      lineOptions: {
        styles: [
          {
            color: color,
            opacity: 1,
            weight: 7,
          },
        ],
      },
      addWaypoints: false,
      draggableWaypoints: false,
      fitSelectedRoutes: false,
      showAlternatives: true,
    }).addTo(map);

    if (bounds.length && bounds.length > 0) {
      bounds.forEach(marker => {
        markerBounds.extend([marker.latitude, marker.longitude]);
      });
      map.fitBounds(markerBounds);
    }

    return () => map.removeControl(routingControl);
  }, [map]);

  return null;
};
export default Routing;

Marker.tsx

import * as React from 'react';

const SvgComponent = (props: any) => (
  <div className="markerIcon">
    <p>{props.visitOrder}</p>
    <svg
      width={99}
      height={122}
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <g
        style={{
          mixBlendMode: 'darken',
        }}
      >
        <g filter="url(#a)">
          <ellipse cx={49} cy={33} rx={36} ry={33} fill="#0AF291" />
        </g>
        <path
          d="M83.5 33c0 17.277-15.32 31.5-34.5 31.5S14.5 50.277 14.5 33 29.82 1.5 49 1.5 83.5 15.723 83.5 33Z"
          stroke="#000"
          strokeWidth={3}
        />
      </g>
      <g filter="url(#b)">
        <path d="m49.5 116 43.734-75H5.766L49.5 116Z" fill="#0AF291" />
      </g>
      <path
        d="m50.796 116.756-1.296 2.222-1.296-2.222-43.734-75L3.155 39.5h92.69l-1.315 2.256-43.734 75Z"
        stroke="#000"
        strokeWidth={3}
      />
      <defs>
        <filter
          id="a"
          x={13}
          y={0}
          width={72}
          height={70}
          filterUnits="userSpaceOnUse"
          colorInterpolationFilters="sRGB"
        >
          <feFlood floodOpacity={0} result="BackgroundImageFix" />
          <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
          <feColorMatrix
            in="SourceAlpha"
            values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
            result="hardAlpha"
          />
          <feOffset dy={4} />
          <feGaussianBlur stdDeviation={2} />
          <feComposite in2="hardAlpha" operator="arithmetic" k2={-1} k3={1} />
          <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
          <feBlend in2="shape" result="effect1_innerShadow_0_1" />
        </filter>
        <filter
          id="b"
          x={0.544}
          y={38}
          width={97.913}
          height={87.956}
          filterUnits="userSpaceOnUse"
          colorInterpolationFilters="sRGB"
        >
          <feFlood floodOpacity={0} result="BackgroundImageFix" />
          <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
          <feColorMatrix
            in="SourceAlpha"
            values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
            result="hardAlpha"
          />
          <feOffset dy={4} />
          <feGaussianBlur stdDeviation={2} />
          <feComposite in2="hardAlpha" operator="arithmetic" k2={-1} k3={1} />
          <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
          <feBlend in2="shape" result="effect1_innerShadow_0_1" />
        </filter>
      </defs>
    </svg>
  </div>
);

export default SvgComponent;

Props color and visitOrder values in Routing.tsx as

<Marker color={color} visitOrder={dataSource[waypointIndex].visitOrder}/>

I want to use it as.


Solution

  • To my knowledge I think it is not possible to render a react svg component on Leaflet map simply because the map rendering is handled by Leaflet, not by React. I think the same applies for other well known libraries like Mapbox. I have faced a similar issue there and the solution is similar with the own that follows.

    What you can do is render a L.divicon and pass as html the svg icon. I played a bit with your svg but did not manage to get the actual image 100% you got. Anyways is really close to what you have and I think you can easily adapt it. So here is a simple example how you would render an svg icon as a Leaflet marker in react-leaflet.

    So simply create an svg icon variable which is an instance of L.divIcon

    const svgIcon = L.divIcon({
      html: `
      <svg
          width="99"
          height="122"
          fill="none"
        xmlns="http://www.w3.org/2000/svg"
        >
       ...your svg icon content
    </svg>`,
      className: "svg-icon",
      iconSize: [24, 40],
      iconAnchor: [12, 40]
    });
    

    and then pass it to Marker's icon prop

    <Marker position={position} icon={svgIcon}>
    

    Demo