import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react'; import type { ForwardedRef } from 'react'; import { withFastCompare } from '../../utils'; import type { RouteInfo } from '../utils/types'; import { AnimatedPolyline, Polyline } from './polyline.component'; import type { AnimatedPolylineRef, AnimatedPolylineAnimationProp, } from './polyline.component'; import type { Point } from '../../bare'; type RouteProps = { route: RouteInfo; }; export const Route = withFastCompare((props: RouteProps) => { return ( {props.route.sections.map((section, i) => ( ))} ); }); export type AnimatedRouteRef = { setRoute: (route: RouteInfo) => void; animate: ( section: number, point: number, duration?: number, onNextAnimationTick?: (point: Point, duration: number) => Promise, abortRef?: React.RefObject ) => void; reset: () => void; }; export const AnimatedRoute = withFastCompare( forwardRef( ( props: RouteProps & { animation?: AnimatedPolylineAnimationProp; commonColor?: string; commonWidth?: number; }, ref: ForwardedRef ) => { const [route, setRoute] = useState(props.route); const polylines = useRef>([]); const _currentSection = useRef(0); const _currentPoint = useRef(0); useEffect(() => { setRoute(props.route); _currentPoint.current = 0; _currentSection.current = 0; }, [props.route]); const reset = useCallback(() => { polylines.current.forEach((l) => l?.reset()); _currentPoint.current = 0; _currentSection.current = 0; }, []); const animate = useCallback( async ( section: number, point: number, _duration?: number, onNextAnimationTick?: ( point: Point, duration: number ) => Promise, abortRef?: React.RefObject ) => { //console.log(section, point, duration); const duration = _duration ?? 100; // Check if animatable if ( route.sections.length - 1 < section || route.sections[section]!.points.length - 1 < point || _currentSection.current > section || (_currentPoint.current > point && _currentSection.current === section) ) { return; } // Get sections that we need to manipulate const sectionRange = [ ...Array(section - _currentSection.current + 1).keys(), ].map((s) => s + _currentSection.current); let steps = 0; // If animation in same section if (_currentSection.current === section) { steps = point - _currentPoint.current; } else { // current section points left steps += route.sections[_currentSection.current]!.points.length - 1 - _currentPoint.current; // cause section switch steps += section - _currentSection.current + 1; // for all sections between start and end for (let i = _currentSection.current + 1; i < section; ++i) { steps += route.sections[i]!.points.length; } steps += point; } // If same point if (!steps) { return; } const FRAME_TIME = 16; const pointDuration = (duration - FRAME_TIME * steps) / steps; // If we have less than 16 ms (1 frame time) for 1 step, we // need to optimize animation to remove glitches if (pointDuration < 0) { steps = sectionRange.length; const optimizedPointDuration = (duration - FRAME_TIME * steps) / steps; for await (const sectionId of sectionRange) { if (abortRef?.current) { return; } _currentSection.current = sectionId; _currentPoint.current = route.sections[sectionId]!.points.length - 1; onNextAnimationTick?.( route.sections[sectionId]!.points[_currentPoint.current]!, optimizedPointDuration ); await polylines.current[sectionId]?.animate( _currentPoint.current, optimizedPointDuration, true ); } } else { for await (const sectionId of sectionRange) { if (abortRef?.current) { return; } _currentSection.current = sectionId; for await (const point of route.sections[sectionId]!.points) { if (abortRef?.current) { return; } onNextAnimationTick?.(point, pointDuration); const pointId = route.sections[sectionId]!.points.indexOf(point); _currentPoint.current = pointId; await polylines.current[sectionId]?.animate( pointId, pointDuration ); } } } }, [route] ); useImperativeHandle(ref, () => ({ setRoute, reset, animate, })); return ( {route.sections.map((section: any, i: number) => ( (polylines.current[i] = ref)} sectionId={i} points={section.points} key={section.routeIndex + '' + i} strokeColor={props.commonColor ?? section.strokeColor ?? 'black'} strokeWidth={props.commonWidth ?? section.strokeWidth ?? 3} animation={props.animation} /> ))} ); } ) );