Search code examples
reactjsrequestanimationframemaptiler

How do you animate a line between points on a map within Reactjs using pidgeon-maps?


Within my reactjs app, I'm using a light-weight map library called pidegon-maps to display vessel locations. Without trying to use the bigger libraries (leaflet, Google Maps react), I'm trying to animate the route a vessel takes.

Taking inspiration from this question, I tried to create a similar implementation.

useEffect(() => {

let start = [0.108266, 52.202758];
let end = [0.11556, 52.201733];

const speedFactor = 500;

let diffX = parseFloat(end[0]) - parseFloat(start[0]);
let diffY = parseFloat(end[1]) - parseFloat(start[1]);
let sfX = diffX / speedFactor;
let sfY = diffY / speedFactor;

let i = 0;
let j = 0;

let lineCoordinates = [];

while (i < diffX || Math.abs(j) < Math.abs(diffY)) {
  lineCoordinates.push([start[0] + i, start[0] + j]);

  if (i < diffX) {
    i += sfX;
  }

  if (Math.abs(j) < Math.abs(diffY)) {
    j += sfY;
  }
}

console.log(lineCoordinates);
let animationCounter = 0;

const animateLine = () => {
  if (animationCounter < lineCoordinates.length) {
    geojson.features[0].geometry.coordinates.push(lineCoordinates[animationCounter]);
    requestAnimationFrame(animateLine);
    animationCounter++;
    
  }

}

animateLine();

}, []);

For some reason, it runs through the animation really fast, then disappears. It also only shows the line as straight (north and south, no angle at all), so it doesn't actually connect. Distance is correct, but not angle. I tried moving it to state instead, bc when zooming in and out, it causes the map to re-render. This worked okay, but it only animates when zooming in and out. So I can slow it down to 1000 speed factor and zoom in and out and watch it animate, but it won't do it by itself.

Currently, it's in a useEffect, but I also removed it and tried without as well.


Solution

  • There are two ways to achieve this, one is growing the lineString by modifying the coordinates like what you are trying to implement.

    Another method is to use CSS to have the svg draw itself, which is way easier and recommended. The key CSS property stroke-dashoffset is now widely supported by modern browsers.


    Method 1: Use a line with two points with CSS

    Sandbox Demo

    Note: Don't forget to specify the path length for the SVG as shown below.

    #VesslePath1 {
        stroke-dasharray: 1;
        stroke-dashoffset: 0;
        /*remove 'infinite' if you don't want the animation to repeat*/
        animation: dash 3s linear forward infinite;
    }
    
    @keyframes dash {
        from{
            stroke-dashoffset: 1;
        }
        to {
            stroke-dashoffset: 0;
        }
    }
    
    import { GeoJson, GeoJsonFeature, Map, Marker } from "pigeon-maps";
    
    const start = [0.108266, 52.202758];
    const end = [0.11556, 52.201733];
    
    export function MyMap() {
    
        const feature = {
            "type": "Feature",
            "properties": {
            },
            "geometry": {
                "type": "LineString",
                "coordinates": [start, end]
            }
        }
    
        return (
            <div>
                <Map height={600} width={1000} defaultCenter={[52.202245, 0.111913]} defaultZoom={15}>
                <Marker width={50} anchor={[52.202758, 0.108266]} />
                <Marker width={50} anchor={[52.201733, 0.11556]} />
                    <GeoJson
                        svgAttributes={{
                            id: "VesslePath1",
                            strokeWidth: "4",
                            stroke: "black",
                            pathLength:"1"
                        }}
                    >
                        <GeoJsonFeature feature={feature} />
                    </GeoJson>
                </Map>
            </div>
        );
    }
    

    Method 2: Your original method with multiple points in the LineString

    I'm not sure what problem your code has since only the useEffect method is shown. However, there are some potential problems:

    1. GeoJson coordinates ([lng,lat]) are opposite with pigeon-maps coordinates ([lat,lng])
    2. Not sure how your requestAnimationFrame frame is used here and if it's used correctly. Hence, I've switched to setIntervals.
    3. There's no point to use more than two points for the lineString if it is a straight line.
    setCoordinates((prevCoordinates) => [
        //...prevCoordinates,
        start,
        [start[0] + currentDiff[0], start[1] + currentDiff[1]],
    ]);
    

    The modified code is shown as below:

    import { GeoJson, GeoJsonFeature, Map, Marker } from "pigeon-maps";
    import { useEffect, useState } from "react";
    
    const speedFactor = 300;
    const start = [0.108266, 52.202758];
    const end = [0.11556, 52.201733];
    const diffX = parseFloat(end[0]) - parseFloat(start[0]);
    const diffY = parseFloat(end[1]) - parseFloat(start[1]);
    const sfX = diffX / speedFactor;
    const sfY = diffY / speedFactor;
    
    export function MyMap() {
    
        const [coordinates, setCoordinates] = useState([start, [start[0] + sfX, start[1] + sfY]]);
    
        useEffect(() => {
            let currentDiff = [sfX, sfY]
            const interval = setInterval(() => {
                
                currentDiff[0] += sfX;
                currentDiff[1] += sfY;
    
                if (Math.abs(currentDiff[0]) > Math.abs(diffX)  || Math.abs(currentDiff[1]) > Math.abs(diffY)) {
                    setCoordinates([
                        start,
                        end,
                    ]);
                    clearInterval(interval);
                    return;
                }
                
                setCoordinates([
                    start,
                    [start[0] + currentDiff[0], start[1] + currentDiff[1]],
                ]);
            }, 10);
    
            return () => clearInterval(interval);
        }, []);
    
        const feature = {
            "type": "Feature",
            "properties": {
            },
            "geometry": {
                "type": "LineString",
                "coordinates": coordinates
            }
        }
    
        return (
            <div>
                <Map height={600} width={1000} defaultCenter={[52.202245, 0.111913]} defaultZoom={15}>
                <Marker width={50} anchor={[52.202758, 0.108266]} />
                <Marker width={50} anchor={[52.201733, 0.11556]} />
    
                    <GeoJson
                        svgAttributes={{
                            strokeWidth: "4",
                            stroke: "black",
                        }}
                    >
                        <GeoJsonFeature feature={feature} />
                    </GeoJson>
                </Map>
            </div>
        );
    }