React Component Example
This page shows a basic usage example using the <RouteReplay>
component.
Make sure you have a .env
file in the examples/react-vite
directory with your VITE_GOOGLE_MAPS_API_KEY
and VITE_GOOGLE_MAPS_MAP_ID
(if using WebGL rendering).
tsx
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Loader } from "@googlemaps/js-api-loader";
import { RouteReplay, RouteReplayHandle } from "route-replay-googlemaps-react";
import type { RoutePoint, RouteInput, CameraMode, PlayerEventMap } from "route-replay-googlemaps-core";
import "./App.css";
const createSquareRoute = (
startTime: number,
latStart: number,
lngStart: number,
size: number,
durationSec: number,
headingOffset = 0
): RoutePoint[] => {
const points: RoutePoint[] = [];
const durationSegment = (durationSec * 1000) / 4;
points.push({
lat: latStart,
lng: lngStart,
t: startTime,
heading: (0 + headingOffset) % 360,
});
points.push({
lat: latStart,
lng: lngStart + size,
t: startTime + durationSegment,
heading: (90 + headingOffset) % 360,
});
points.push({
lat: latStart + size,
lng: lngStart + size,
t: startTime + durationSegment * 2,
heading: (0 + headingOffset) % 360,
});
points.push({
lat: latStart + size,
lng: lngStart,
t: startTime + durationSegment * 3,
heading: (270 + headingOffset) % 360,
});
points.push({
lat: latStart,
lng: lngStart,
t: startTime + durationSegment * 4,
heading: (180 + headingOffset) % 360,
});
return points;
};
const now = Date.now();
const multiTrackRouteData: RouteInput = {
track1: createSquareRoute(now, 35.68, 139.76, 0.01, 20, 0),
track2: createSquareRoute(now + 5000, 35.685, 139.77, 0.005, 15, 45), square starting 5s later, offset heading
track3: [
{ lat: 35.67, lng: 139.75, t: now + 2000, heading: 45 },
{ lat: 35.675, lng: 139.755, t: now + 12000, heading: 45 },
],
};
const markerOptionsConfig: google.maps.MarkerOptions = {};
const polylineOptionsConfig: google.maps.PolylineOptions = {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 4,
};
function App() {
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
const mapId = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
const [mapInstance, setMapInstance] = useState<google.maps.Map | null>(null);
const [error, setError] = useState<string | null>(null);
const replayHandleRef = useRef<RouteReplayHandle>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [speed, setSpeed] = useState(1);
const [durationMs, setDurationMs] = useState(0);
const [currentCameraMode, setCurrentCameraMode] = useState<CameraMode>("center");
const [isSeeking, setIsSeeking] = useState(false);
useEffect(() => {
if (!apiKey) {
setError("Missing VITE_GOOGLE_MAPS_API_KEY in .env file");
return;
}
const loader = new Loader({
apiKey: apiKey,
version: "weekly",
libraries: ["maps", "geometry"],
});
loader
.importLibrary("maps")
.then((google) => {
console.log("Google Maps API loaded (maps, geometry)");
if (!mapInstance) {
const map = new google.Map(document.getElementById("map")!, {
center: { lat: 35.68, lng: 139.76 },
zoom: 15,
disableDefaultUI: true,
mapId: mapId,
});
setMapInstance(map);
}
})
.catch((e: unknown) => {
console.error("Error loading/initializing Google Maps:", e);
setError("Failed to load/initialize Google Maps.");
});
}, [apiKey, mapId, mapInstance]);
const handleFrame: PlayerEventMap["frame"] = useCallback(
(payload) => {
if (!isSeeking) {
setProgress(payload.progress);
}
},
[isSeeking]
);
const handleStart = useCallback(() => setIsPlaying(true), []);
const handlePause = useCallback(() => setIsPlaying(false), []);
const handleSeek: PlayerEventMap["seek"] = useCallback(
(payload) => {
const currentDuration = replayHandleRef.current?.getDurationMs() ?? 0;
if (currentDuration !== durationMs) {
setDurationMs(currentDuration);
}
if (!isSeeking) {
const newProgress = currentDuration > 0 ? payload.timeMs / currentDuration : 0;
setProgress(Math.min(1, Math.max(0, newProgress)));
}
},
[durationMs, isSeeking]
);
const handleFinish = useCallback(() => {
setIsPlaying(false);
setProgress(1);
}, []);
const handleError: PlayerEventMap["error"] = useCallback((payload) => {
console.error("Error event:", payload.error);
setError(`Replay Error: ${payload.error.message}`);
setIsPlaying(false);
}, []);
const handlePlay = useCallback(() => replayHandleRef.current?.play(), []);
const handlePauseControl = useCallback(() => replayHandleRef.current?.pause(), []);
const handleStop = useCallback(() => {
replayHandleRef.current?.stop();
setProgress(0);
setIsPlaying(false);
}, []);
const handleSpeedChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newSpeed = parseFloat(event.target.value);
if (!isNaN(newSpeed)) {
replayHandleRef.current?.setSpeed(newSpeed);
setSpeed(newSpeed);
}
},
[]
);
const handleSetSpeed = useCallback(
(speed: number) => {
replayHandleRef.current?.setSpeed(speed);
setSpeed(speed);
},
[]
);
const handleCameraModeChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const mode = event.target.value as CameraMode;
replayHandleRef.current?.setCameraMode(mode);
setCurrentCameraMode(mode);
},
[]
);
const handleSeekInput = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setIsSeeking(true);
const newProgress = parseFloat(event.target.value);
setProgress(newProgress);
},
[]
);
const handleSeekChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newProgress = parseFloat(event.target.value);
if (durationMs > 0 && replayHandleRef.current) {
replayHandleRef.current.seek(newProgress * durationMs);
}
setIsSeeking(false);
},
[durationMs]
);
if (error) {
return <div className="error">Error: {error}</div>;
}
const isReady = !!mapInstance && durationMs > 0;
return (
<>
<div
id="map"
style={{ height: "500px", width: "100%" }}
></div>
{mapInstance && (
<RouteReplay
ref={replayHandleRef}
map={mapInstance}
route={multiTrackRouteData}
autoFit={true}
markerOptions={markerOptionsConfig}
polylineOptions={polylineOptionsConfig}
initialSpeed={1}
cameraMode={"center"}
onFrame={handleFrame}
onStart={handleStart}
onPause={handlePause}
onSeek={handleSeek}
onFinish={handleFinish}
onError={handleError}
/>
)}
<div className="controls">
<button onClick={handlePlay} disabled={isPlaying || !isReady}>Play</button>
<button onClick={handlePauseControl} disabled={!isPlaying || !isReady}>Pause</button>
<button onClick={handleStop} disabled={!isReady}>Stop</button>
</div>
</>
);
}
export default App;