javascriptreactjsleafletreact-leaflet

How to dynamically move and rotate marker in react-leaflet?


I have wrote a React project with react-leaflet and hook. My goal is to move and rotate marker every second. However, moving part is working fine. But the rotation of marker is not working. I am really stuck for last few days. I couldn't find a good solution. Please help me.

The map component is as follows.

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import { useState, useEffect } from "react";
import L from 'leaflet';
import 'leaflet-marker-rotation';

const MySimpleMap = () => {
  const [lat, setLat] = useState(22.899397);
  const [lon, setLon] = useState(89.508279);
  const [heading, setHeading] = useState(30)

  useEffect(() => {
    const interval = setInterval(() => {
      myfun();
    }, 1000);
    return () => {
      clearInterval(interval);
    };
  }, [lat]);

  const defaultIcon = L.icon({
    iconUrl: "https://unpkg.com/leaflet@1.0.3/dist/images/marker-icon.png",
    iconSize: [20, 40],
    iconAnchor: [18, 18],
    popupAnchor: [0, -10],
    shadowAnchor: [10, 10]
  }); 

  const myfun = () => {    
    setLat(lat + 0.00001);
    setLon(lon + 0.00001);
    setHeading(heading+5);
    console.log("angle:" + heading);
  };
  return (
    <MapContainer className="map" center={[lat, lon]} zoom={21}>
      <TileLayer
        attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={[lat, lon]} icon={defaultIcon} rotationAngle={heading} rotationOrigin="center">
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  );
};

export default MySimpleMap;

The complete problem is in this sandbox: https://codesandbox.io/s/vigilant-wood-kgv4p?file=/src/App.js


Solution

  • Using some of the provided answers for rotating markers in this post , was able to get it to work with your provided codesandbox. The default Marker component does not have rotationAngle or rotationOrigin as valid props, you'd need to do something like done here to get that. So instead we can use re-use the rotation code and plug it into your code by lifting it into a re-usable function:

    const applyRotation = (marker, _options) => {
      const oldIE = L.DomUtil.TRANSFORM === "msTransform";
      const options = Object.assign(_options, { rotationOrigin: "center" });
      const { rotationAngle, rotationOrigin } = options;
    
      if (rotationAngle && marker) {
        marker._icon.style[L.DomUtil.TRANSFORM + "Origin"] = rotationOrigin;
    
        if (oldIE) {
          // for IE 9, use the 2D rotation
          marker._icon.style[L.DomUtil.TRANSFORM] = `rotate(${rotationAngle} deg)`;
        } else {
          // for modern browsers, prefer the 3D accelerated version
          marker._icon.style[
            L.DomUtil.TRANSFORM
          ] += ` rotateZ(${rotationAngle}deg)`;
        }
      }
    };
    

    And then calling it once the rotation has been changed with a useEffect because we also want the Markers position to be updated otherwise the rotation won't work:

      useEffect(() => {
        applyRotation(markerRef.current, { rotationAngle: heading + 5 });
      }, [heading]);
    

    When you call myFunc it updates the position (lat,lon) as well as the header (rotation) so by placing it in the useEffect, on next render Marker should have updated its position.

    I have a codebox example here.

    The above solution does have some issue's you'll need to sift through the leaflet-rotatedmarker library to get those missing edge cases. However if you can add that library a better solution would be importing that:

    import L from "leaflet";
    import "leaflet-rotatedmarker";
    

    and using the extended setRotationAngle instead.

      useEffect(() => {
        if (markerRef.current) {
          markerRef.current.setRotationAngle(heading);
        }
      }, [heading]);
    

    You can also use that to lift it into a RotatedMarker component so that your original use case of the props rotationAngle={heading} rotationOrigin="center" will work:

    const RotatedMarker = forwardRef(({ children, ...props }, forwardRef) => {
      const markerRef = useRef();
    
      const { rotationAngle, rotationOrigin } = props;
      useEffect(() => {
        const marker = markerRef.current;
        if (marker) {
          marker.setRotationAngle(rotationAngle);
          marker.setRotationOrigin(rotationOrigin);
        }
      }, [rotationAngle, rotationOrigin]);
    
      return (
        <Marker
          ref={(ref) => {
            markerRef.current = ref;
            if (forwardRef) {
              forwardRef.current = ref;
            }
          }}
          {...props}
        >
          {children}
        </Marker>
      );
    });
    

    Here's the codepen where i tested the above out