From 01cc842b5565c57e7a7af8012bfda78f4e05b649 Mon Sep 17 00:00:00 2001 From: hiba9201 Date: Sun, 15 Jan 2023 16:56:39 +0500 Subject: [PATCH] 1 iteration smooth moving with moving marker --- components/Map/Stations/MapStations.tsx | 2 + .../Map/Vehicles/Item/MapVehiclesItem.tsx | 205 ++++++++++-------- .../Vehicles/Item/MapVehiclesItem.types.ts | 3 +- .../leaflet-extensions/moving-marker.ts | 150 +++++++++++-- 4 files changed, 250 insertions(+), 110 deletions(-) diff --git a/components/Map/Stations/MapStations.tsx b/components/Map/Stations/MapStations.tsx index ebcf62c6..86884d28 100644 --- a/components/Map/Stations/MapStations.tsx +++ b/components/Map/Stations/MapStations.tsx @@ -34,6 +34,7 @@ export function MapStations() { id={station.ID} name={station.NAME} direction={station.DIRECTION} + key={station.ID} /> ); })} @@ -48,6 +49,7 @@ export function MapStations() { id={station.ID} name={station.NAME} direction={station.DIRECTION} + key={station.ID} /> ); })} diff --git a/components/Map/Vehicles/Item/MapVehiclesItem.tsx b/components/Map/Vehicles/Item/MapVehiclesItem.tsx index 4e564cc4..5fc2727a 100644 --- a/components/Map/Vehicles/Item/MapVehiclesItem.tsx +++ b/components/Map/Vehicles/Item/MapVehiclesItem.tsx @@ -1,15 +1,15 @@ -import React, { Component, createRef } from 'react'; +import React, { + useRef, useEffect, useMemo, useCallback, useState, +} from 'react'; import ReactDOMServer from 'react-dom/server'; -import { isEqual } from 'lodash'; -import { Marker } from 'react-leaflet'; +import { useMap } from 'react-leaflet'; import L from 'leaflet'; import classNames from 'classnames/bind'; -import { withMap } from 'components/hocs/withMap'; +import { MovingMarker } from 'components/leaflet-extensions/moving-marker'; import { MapVehicleMarker } from '../Marker/MapVehicleMarker'; -import { startMoveInDirection, clearIntervals } from './MapVehiclesItem.utils'; import { EAST_COURSE_RANGE } from './MapVehiclesItem.constants'; import { MapVehiclesItemProps } from './MapVehiclesItem.types'; @@ -17,49 +17,46 @@ import styles from './MapVehiclesItem.module.css'; const cn = classNames.bind(styles); -export class MapVehiclesItemComponent extends Component { - private icon: L.DivIcon; +const geoCoordsToEuqlid = (oPoint: [number, number], point: [number, number]) => { + const yPoint: [number, number] = [oPoint[0], point[1]]; + const xPoint: [number, number] = [point[0], oPoint[1]]; - private markerRef = createRef(); + let x = new L.LatLng(...xPoint).distanceTo(point); + let y = new L.LatLng(...yPoint).distanceTo(point); - constructor(props: MapVehiclesItemProps) { - super(props); - - this.icon = this.getIcon(); - } - - componentDidMount(): void { - setTimeout(this.updateTranslate, 0); - - const { map } = this.props; - - map.addEventListener('zoomend', () => { - this.updateTranslate(); - }); + if (point[0] < oPoint[0]) { + x = -x; } - componentDidUpdate(prevProps: Readonly): void { - const { course, position } = this.props; - - if (prevProps.course !== course) { - const icon = this.getIcon(); - - this.markerRef?.current?.setIcon(icon); - } - - if (!isEqual(prevProps.position, position)) { - this.updateTranslate(); - } + if (point[1] < oPoint[1]) { + y = -y; } - componentWillUnmount(): void { - clearIntervals(); - } + return [x, y]; +}; + +export function MapVehiclesItem({ + position, + velocity, + course, + boardId, + routeNumber, + type, + disability, + warning, + onClick, +}: MapVehiclesItemProps) { + const map = useMap(); + const vehicleRef = useRef(); + const [isFirstPosition, setIsFirstPosition] = useState(true); + const [isActive, setIsActive] = useState(false); + + const onClickEventHandler = useCallback(() => { + setIsActive(true); + onClick(routeNumber); + }, [routeNumber, onClick]); - getIcon() { - const { - boardId, routeNumber, course, type, disability, warning, - } = this.props; + const icon = useMemo(() => { const isCourseEast = course > EAST_COURSE_RANGE.left || course < EAST_COURSE_RANGE.right; return new L.DivIcon({ @@ -79,66 +76,92 @@ export class MapVehiclesItemComponent extends Component { />, ), }); - } + }, [course, boardId, routeNumber, type, disability, warning]); - getScale = () => { - const { map } = this.props; + const rotateIcon = useCallback(() => { + vehicleRef.current.setIcon(icon); + }, [course, icon]); - // Get the y,x dimensions of the map - const { x, y } = map.getSize(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const isNewPositionBehind = useCallback(() => { + const prevPosition = vehicleRef.current.getLatLng(); + const newPosition = new L.LatLng(...position); + const center: [number, number] = [prevPosition.lat, prevPosition.lng]; + const xPosition: [number, number] = [prevPosition.lat, prevPosition.lng + 0.01]; - // calculate the distance the one side of the map to the other using the haversine formula - const maxMeters = map - .containerPointToLatLng([0, y]) - .distanceTo(map.containerPointToLatLng([x, y])); + const newPositionEuqlid = geoCoordsToEuqlid(center, position); + const xPositionEuqlid = geoCoordsToEuqlid(center, xPosition); - // calculate how many meters each pixel represents - return maxMeters / x; - }; + const newPositionDistance = prevPosition.distanceTo(newPosition); - onClickEventHandler = () => { - const { routeNumber, onClick } = this.props; + if (newPositionDistance > 150) { + return false; + } - if (!routeNumber) { - return; + const xPositionDistance = prevPosition.distanceTo(xPosition); + + const xMult = newPositionEuqlid[0] * xPositionEuqlid[0]; + const yMult = newPositionEuqlid[1] * xPositionEuqlid[1]; + const scalarMult = xMult + yMult; + const a = Math.acos(scalarMult / (xPositionDistance * newPositionDistance)); + const angle = (a * 180) / Math.PI; + + const diff = Math.abs(course - angle); + + return diff >= 160 && diff <= 200; + }, [position, course]); + + const startVehicleMoving = useCallback( + (isFirstMove: boolean) => { + if (isActive) { + // eslint-disable-next-line no-console + console.log('start moving'); + } + + vehicleRef.current.moveToWithDuration({ + latlng: new L.LatLng(...position), + duration: isFirstMove ? 0 : 2000, + inertialMove: { + speed: velocity, + direction: course, + callback: course === 270 || velocity === 0 ? null : rotateIcon, + }, + }); + }, + [position, velocity, course, rotateIcon], + ); + + useEffect(() => { + vehicleRef.current?.cancelMove(); + const prevMap = vehicleRef.current ? vehicleRef.current.getMap() : null; + + if (prevMap !== map) { + if (vehicleRef.current) { + vehicleRef.current.addTo(map); + } else { + const vehicle = new MovingMarker(position, { + icon, + }).addTo(map); + + vehicle.on('click', onClickEventHandler); + vehicleRef.current = vehicle; + } } - onClick(routeNumber); - }; + startVehicleMoving(isFirstPosition); - private updateTranslate = () => { - const { - boardId, routeNumber, velocity, course, - } = this.props; + if (isFirstPosition) { + setIsFirstPosition(false); + } - const marker = document.querySelector( - `#vehicle-${boardId}-${routeNumber}`, - ) as HTMLDivElement; + return () => { + const prevMapCheck = vehicleRef.current ? vehicleRef.current.getMap() : null; - if (!marker) { - return; - } + if (prevMapCheck !== map) { + vehicleRef.current.remove(); + } + }; + }, [map, position, velocity, course]); - startMoveInDirection({ - direction: course, - vehicle: marker, - velocity, - scale: this.getScale(), - }); - }; - - render() { - const { position } = this.props; - - return ( - - ); - } + return null; } - -export const MapVehiclesItem = withMap(MapVehiclesItemComponent); diff --git a/components/Map/Vehicles/Item/MapVehiclesItem.types.ts b/components/Map/Vehicles/Item/MapVehiclesItem.types.ts index 3798d186..d423fd88 100644 --- a/components/Map/Vehicles/Item/MapVehiclesItem.types.ts +++ b/components/Map/Vehicles/Item/MapVehiclesItem.types.ts @@ -4,13 +4,12 @@ export type MapVehiclesItemProps = { boardId: number; velocity: number; position: [number, number]; - routeNumber: number | null; + routeNumber: number; course: number; type: VehicleType; disability?: boolean; warning?: boolean; onClick: (routeNumber: number) => void; - map: L.Map; }; export type MoveInDirectionParams = { diff --git a/components/leaflet-extensions/moving-marker.ts b/components/leaflet-extensions/moving-marker.ts index 67caaccc..4ee0dce8 100644 --- a/components/leaflet-extensions/moving-marker.ts +++ b/components/leaflet-extensions/moving-marker.ts @@ -1,5 +1,29 @@ import L from 'leaflet'; +export const ANIMATION_INTERVAL = 50; +export const METERS_IN_KILOMETER = 1000; +export const SECONDS_IN_HOUR = 3600; + +function round(num: number, decimalPlaces: number = 0) { + const multyplier = 10 ** decimalPlaces; + + return Math.round((num + Number.EPSILON) * multyplier) / multyplier; +} + +function getDeltaCoords(velocity: number, course: number, animInterval = ANIMATION_INTERVAL) { + const distance = (velocity * animInterval) / 1000; + const angleInRad = (course * Math.PI) / 180; + + // Calculating cathets (x and y) from hypotenuse (distance) + return [round(Math.cos(angleInRad) * distance, 5), round(Math.sin(angleInRad) * distance, 5)]; +} + +function getVelocityInPixelsPerSecond(velocity: number, scale: number) { + const velocityMetersPerSecond = (velocity * METERS_IN_KILOMETER) / SECONDS_IN_HOUR; + + return velocityMetersPerSecond / scale; +} + export class MovingMarker extends L.Marker { private slideToUntil = 0; @@ -9,13 +33,34 @@ export class MovingMarker extends L.Marker { private slideFromLatLng: L.LatLngExpression = [0, 0]; + private slideSpeed = 0; + + private slideDirection = 0; + private animationId: number; - moveToWithDuration({ latlng, duration }: { latlng: L.LatLng; duration: number }) { + private prevTime = 0; + + private inertialMove; + + moveToWithDuration({ + latlng, + duration, + inertialMove, + }: { + latlng: L.LatLng; + duration: number; + inertialMove?: { + speed: number; + direction: number; + callback?: () => void; + }; + }) { this.slideFromLatLng = this.getLatLng(); this.slideToLatLng = latlng; this.slideToDuration = duration; this.slideToUntil = performance.now() + this.slideToDuration; + this.inertialMove = inertialMove; this.moveToWithDurationAnim(); } @@ -23,9 +68,24 @@ export class MovingMarker extends L.Marker { private moveToWithDurationAnim = () => { const timeRemains = this.slideToUntil - performance.now(); - if (timeRemains <= 0) { + // eslint-disable-next-line no-underscore-dangle + if (timeRemains <= 0 || !this._map) { this.setLatLng(this.slideToLatLng); + if (this.inertialMove) { + const { callback, ...moveWithSpeedOptions } = this.inertialMove; + + if (callback) { + callback(); + } + + this.moveToWithSpeed(moveWithSpeedOptions); + + this.inertialMove = null; + } + + this.animationId = null; + return; } @@ -46,22 +106,78 @@ export class MovingMarker extends L.Marker { cancelMove() { L.Util.cancelAnimFrame(this.animationId); + this.slideFromLatLng = null; + this.slideToLatLng = null; this.animationId = null; + this.prevTime = 0; + this.inertialMove = null; + } + + private getScale() { + // eslint-disable-next-line no-underscore-dangle + if (!this._map) { + return 1; + } + + // Get the x, y dimensions of the map + // eslint-disable-next-line no-underscore-dangle + const { x, y } = this._map.getSize(); + + // calculate the distance the one side of the map to the other using the haversine formula + // eslint-disable-next-line no-underscore-dangle + const maxMeters = this._map + .containerPointToLatLng([0, y]) + // eslint-disable-next-line no-underscore-dangle + .distanceTo(this._map.containerPointToLatLng([x, y])); + + // calculate how many meters each pixel represents + return maxMeters / x; + } + + public getMap() { + // eslint-disable-next-line no-underscore-dangle + return this._map; + } + + moveToWithSpeed({ speed, direction }: { speed: number; direction: number }) { + this.slideSpeed = speed; + this.slideDirection = direction; + + // eslint-disable-next-line no-underscore-dangle + if (!this._map) { + return; + } + + this.moveToWithSpeedAnim(); } - // Template for method to use in MapVehicleItem - - // moveToWithSpeed({ - // latlng, - // speed, - // acceleration, - // direction, - // }: { - // latlng: L.LatLng; - // speed: number; - // acceleration?: number; - // direction?: number; - // }) { - - // } + private moveToWithSpeedAnim = () => { + // eslint-disable-next-line no-underscore-dangle + if (!this._map) { + return; + } + + const currentTime = performance.now(); + const timeDiff = currentTime - this.prevTime; + + const velocityPxPerSecond = getVelocityInPixelsPerSecond(this.slideSpeed, this.getScale()); + const [deltaX, deltaY] = getDeltaCoords(velocityPxPerSecond, this.slideDirection, timeDiff); + + // eslint-disable-next-line no-underscore-dangle + const currentPosition = this._map.options.crs + .latLngToPoint( + this.getLatLng(), + // eslint-disable-next-line no-underscore-dangle + this._map.getZoom(), + ) + // eslint-disable-next-line no-underscore-dangle + .subtract(this._map.getPixelOrigin()); + const nextPosition = currentPosition.add([deltaX, deltaY]); + + // eslint-disable-next-line no-underscore-dangle + this.setLatLng(this._map.layerPointToLatLng(nextPosition)); + + this.prevTime = currentTime; + this.animationId = L.Util.requestAnimFrame(this.moveToWithSpeedAnim); + }; }