visgl / react-google-maps

React components and hooks for the Google Maps JavaScript API
https://visgl.github.io/react-google-maps/
MIT License
1.01k stars 65 forks source link

How can I animate the route to show a pulsating trail? #387

Closed fzkhan19 closed 1 month ago

fzkhan19 commented 1 month ago

Description

I want to animate the route displayed using direction renderer, I think deck.gl might help in this but im not sure how. i want a functionality same as show in this video: https://www.youtube.com/watch?v=H_BWRTf0d8g

Steps to Reproduce

none

Environment

Logs

No response

usefulthink commented 1 month ago

yep, I don't think something like that can be done with the vanilla DirectionsRenderer that comes with the Maps JavaScript API. I think the best choice for this would be the deck.gl TripsLayer (see also the demo here: https://deck.gl/examples/trips-layer).

For that you need to specify the points along the path together with timestamps and then animate the 'currentTime' value of the layer. If you don't have a 'true' timing reference you can use e.g. the relative distance along the path as timestamp.

That TripsLayer can then be added to the map with an interleaved overlay as shown here: https://visgl.github.io/react-google-maps/docs/guides/deckgl-integration#using-googlemapsoverlay

fzkhan19 commented 1 month ago

I tried to write the code, now i'm struggling to get it to animate

here's the code -

Directions.tsx:

"use client";

import {Color, Position} from "@deck.gl/core";
import {TripsLayer} from "@deck.gl/geo-layers";
import {GoogleMapsOverlay} from "@deck.gl/google-maps";
import {useMap, useMapsLibrary} from "@vis.gl/react-google-maps";
import {useEffect, useState} from "react";

const LOOP_LENGTH = 100;

type DataType = {
  vendor: number;
  path: Position[];
  timestamps: number[];
};

export default function Directions({
  directionServiceResponse,
}: {
  directionServiceResponse: google.maps.DirectionsResult;
}) {
  const map = useMap();
  const routesLibrary = useMapsLibrary("routes");
  const [directionsRenderer, setDirectionsRenderer] = useState<google.maps.DirectionsRenderer>();
  const [routes, setRoutes] = useState<google.maps.DirectionsRoute[]>([]);
  const selected = routes[0];
  const leg = selected?.legs[0];

  // Initialize directions service and renderer
  useEffect(() => {
    if (!routesLibrary || !map) return;
    setDirectionsRenderer(new routesLibrary.DirectionsRenderer({map}));
  }, [routesLibrary, map]);

  // Assign direction response to renderer
  useEffect(() => {
    if (!directionsRenderer) return;

    directionsRenderer.setDirections(directionServiceResponse);
    setRoutes(directionServiceResponse.routes);

    return () => directionsRenderer.setMap(null);
  }, [directionsRenderer]);

  // Update direction route
  useEffect(() => {
    if (!directionsRenderer) return;
    directionsRenderer.setRouteIndex(0);
  }, [directionsRenderer]);

  // Initialize and animate TripsLayer
  useEffect(() => {
    if (!map || !leg) return;

    const path = leg.steps.flatMap((step) =>
      step.path.map((latlng) => [latlng.lng(), latlng.lat()]),
    );

    const timestamps = Array(path.length)
      .fill(0)
      .map((_, i) => i * 10); // Assuming each step takes 10 units of time

    const data = [{vendor: 0, path, timestamps}];

    console.log("DATA:::", data);

    let currentTime = 0;

    const props = {
      id: "trips",
      data,
      getPath: (d: DataType) => d.path,
      getTimestamps: (d: DataType) => d.timestamps,
      getColor: [0, 255, 255] as Color,
      opacity: 1,
      widthMinPixels: 2,
      trailLength: 180,
      currentTime,
      shadowEnabled: false,
    };

    const overlay = new GoogleMapsOverlay({interleaved: true});

    const animate = () => {
      currentTime = (currentTime + 1) % LOOP_LENGTH;

      const tripsLayer = new TripsLayer({
        ...props,
        currentTime,
      });

      overlay.setProps({
        layers: [tripsLayer],
      });

      window.requestAnimationFrame(animate);
    };

    window.requestAnimationFrame(animate);

    overlay.setMap(map);

    return () => overlay.setMap(null);
  }, [map, leg]);

  if (!leg) return null;

  return null;
}

RouteMap.tsx:

import {Map} from "@vis.gl/react-google-maps";

import {MAP_STYLE} from "@/constants";

import Directions from "./Directions";

export default function RouteMap({
  directionServiceResponse,
}: {
  directionServiceResponse: google.maps.DirectionsResult;
}) {
  console.log(directionServiceResponse);

  return (
    <div>
      <Map
        className="m-0 h-full w-full rounded-md border-0 p-0"
        clickableIcons={false}
        controlSize={30}
        defaultZoom={11}
        disableDefaultUI={true}
        fullscreenControl={true}
        fullscreenControlOptions={{position: google.maps.ControlPosition.LEFT_TOP}}
        gestureHandling={"greedy"}
        mapId={"4f6dde3310be51d7"}
        mapTypeControl={false}
        streetViewControl={false}
        styles={MAP_STYLE}
        zoomControl={false}
      >
        <Directions directionServiceResponse={directionServiceResponse} />
      </Map>
    </div>
  );
}

image

and the route is just sitting there

fzkhan19 commented 1 month ago

its working now there was some problem with loop length, now I'm having issues with default center and default zoom, they don't seem to be working

usefulthink commented 1 month ago

I believe the default behavior of the DirectionsRenderer is to fit the map to the bounds of the route. Also, what do you need the DirectionsRenderer for if you're rendering the Route using the TripsLayer?

usefulthink commented 1 month ago

Closing this since it's not a bugreport or feature request. Feel free to ask followup questions in our Discussions section.