From 5aec2bddd4e8b3409baa63faacf6804671302387 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Mon, 2 Dec 2024 10:45:20 +0200 Subject: [PATCH 01/10] feat: coordinate transformations to local tangent plane We need to be able to do lightweight geometric calculations --- app/util/geo-utils.js | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/app/util/geo-utils.js b/app/util/geo-utils.js index b8640bf6eb..9f25753e0d 100644 --- a/app/util/geo-utils.js +++ b/app/util/geo-utils.js @@ -419,3 +419,56 @@ export function getClosestPoint(a, b, c) { } return distA < distB ? a : b; } + +// WGS84 to local planar coordinates +// Usage: origin = GeodeticToEcef(lat0, lon0); +// { x, y } = GeodeticToEnu(lat, lon, origin); + +const a = 6378137.0; // WGS-84 Earth semimajor axis (m) +const b = 6356752.314245; // Derived Earth semiminor axis (m) +const h = (a + b) / 2; // Use a default distance from earth center +const f = (a - b) / a; // Ellipsoid Flatness +const e_sq = f * (2 - f); // Square of Eccentricity + +// Converts WGS-84 Geodetic point (lat, lon) with a default height to the +// Earth-Centered Earth-Fixed (ECEF) coordinates (x, y, z) +export function GeodeticToEcef(lat, lon) { + const lambda = toRad(lat); + const phi = toRad(lon); + const s = Math.sin(lambda); + const N = a / Math.sqrt(1 - e_sq * s * s); + + const sin_lambda = Math.sin(lambda); + const cos_lambda = Math.cos(lambda); + const cos_phi = Math.cos(phi); + const sin_phi = Math.sin(phi); + + const x = (h + N) * cos_lambda * cos_phi; + const y = (h + N) * cos_lambda * sin_phi; + const z = (h + (1 - e_sq) * N) * sin_lambda; + + return { x, y, z, sin_lambda, cos_lambda, cos_phi, sin_phi }; +} + +// Converts the earth-centered earth-fixed (ECEF) coordinates (x, y, z) to +// east-north coordinates in a local tangent plane defined by an ECEF point origin +function EcefToEnu(e, origin) { + const xd = e.x - origin.x; + const yd = e.y - origin.y; + const zd = e.z - origin.z; + + const xEast = -origin.sin_phi * xd + origin.cos_phi * yd; + const yNorth = + -origin.cos_phi * origin.sin_lambda * xd - + origin.sin_lambda * origin.sin_phi * yd + + origin.cos_lambda * zd; + + return { x: xEast, y: yNorth }; +} + +// Converts the geodetic WGS-84 coordinated (lat, lon) at a default height to +// East-North-Up coordinates in a local tangent plane defined by an ECEF point origin +export function GeodeticToEnu(lat, lon, origin) { + const ecef = GeodeticToEcef(lat, lon); + return EcefToEnu(ecef, origin); +} From a7d7f84b22bc9f1f14a264bfbe7cdb8856e12f7c Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Mon, 2 Dec 2024 15:16:46 +0200 Subject: [PATCH 02/10] fix: coordinate conversion bugs --- app/util/geo-utils.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/util/geo-utils.js b/app/util/geo-utils.js index 9f25753e0d..aee0a6b8fd 100644 --- a/app/util/geo-utils.js +++ b/app/util/geo-utils.js @@ -435,17 +435,15 @@ const e_sq = f * (2 - f); // Square of Eccentricity export function GeodeticToEcef(lat, lon) { const lambda = toRad(lat); const phi = toRad(lon); - const s = Math.sin(lambda); - const N = a / Math.sqrt(1 - e_sq * s * s); - const sin_lambda = Math.sin(lambda); const cos_lambda = Math.cos(lambda); const cos_phi = Math.cos(phi); const sin_phi = Math.sin(phi); + const N = a / Math.sqrt(1 - e_sq * sin_lambda * sin_lambda); - const x = (h + N) * cos_lambda * cos_phi; - const y = (h + N) * cos_lambda * sin_phi; - const z = (h + (1 - e_sq) * N) * sin_lambda; + const x = 0.5 * (h + N) * cos_lambda * cos_phi; + const y = 0.5 * (h + N) * cos_lambda * sin_phi; + const z = 0.5 * (h + (1 - e_sq) * N) * sin_lambda; return { x, y, z, sin_lambda, cos_lambda, cos_phi, sin_phi }; } From 518423dad338c60955a96ebfec09307d4e06126c Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Mon, 2 Dec 2024 15:22:55 +0200 Subject: [PATCH 03/10] feat: compute planar leg geometry --- .../itinerary/navigator/NaviContainer.js | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js index dffbcb0d19..bb0313369c 100644 --- a/app/component/itinerary/navigator/NaviContainer.js +++ b/app/component/itinerary/navigator/NaviContainer.js @@ -1,6 +1,8 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import polyUtil from 'polyline-encoded'; import { legTime } from '../../../util/legUtils'; +import { GeodeticToEcef, GeodeticToEnu } from '../../../util/geo-utils'; import { itineraryShape, relayShape } from '../../../util/shapes'; import NaviBottom from './NaviBottom'; import NaviCardContainer from './NaviCardContainer'; @@ -10,20 +12,38 @@ function NaviContainer( { itinerary, focusToLeg, relayEnvironment, setNavigation, mapRef }, { getStore }, ) { - const { legs } = itinerary; + const [planarLegs, setPlanarLegs] = useState([]); + const position = getStore('PositionStore').getLocationState(); + useEffect(() => { + const { lat, lon } = itinerary.legs[0].from; + const origin = GeodeticToEcef(lat, lon); + const legs = itinerary.legs.map(leg => { + const geometry = polyUtil.decode(leg.legGeometry.points); + return { + ...leg, + geometry: geometry.map(p => GeodeticToEnu(p[0], p[1], origin)), + }; + }); + setPlanarLegs(legs); + }, [itinerary.legs]); + const { realTimeLegs, time, isPositioningAllowed } = useRealtimeLegs( - legs, + planarLegs, mapRef, relayEnvironment, ); + if (!realTimeLegs.length) { + return null; + } + // recompute estimated arrival let lastTransitLeg; let arrivalChange = 0; - legs.forEach(leg => { + realTimeLegs.forEach(leg => { if (leg.transitLeg) { lastTransitLeg = leg; } @@ -36,7 +56,8 @@ function NaviContainer( arrivalChange = legTime(rtLeg.end) - legTime(lastTransitLeg.end); } - const arrivalTime = legTime(legs[legs.length - 1].end) + arrivalChange; + const arrivalTime = + legTime(realTimeLegs[realTimeLegs.length - 1].end) + arrivalChange; return ( <> From d840dd21ba62c2ff9c791444157c5260d996ec2e Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 10:38:14 +0200 Subject: [PATCH 04/10] fix: use constant zero height --- app/util/geo-utils.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/util/geo-utils.js b/app/util/geo-utils.js index aee0a6b8fd..69f4385b7b 100644 --- a/app/util/geo-utils.js +++ b/app/util/geo-utils.js @@ -426,11 +426,11 @@ export function getClosestPoint(a, b, c) { const a = 6378137.0; // WGS-84 Earth semimajor axis (m) const b = 6356752.314245; // Derived Earth semiminor axis (m) -const h = (a + b) / 2; // Use a default distance from earth center const f = (a - b) / a; // Ellipsoid Flatness const e_sq = f * (2 - f); // Square of Eccentricity +const inv_e_sq = 1 - e_sq; -// Converts WGS-84 Geodetic point (lat, lon) with a default height to the +// Converts WGS-84 Geodetic point (lat, lon) at ground level 0 to // Earth-Centered Earth-Fixed (ECEF) coordinates (x, y, z) export function GeodeticToEcef(lat, lon) { const lambda = toRad(lat); @@ -441,9 +441,9 @@ export function GeodeticToEcef(lat, lon) { const sin_phi = Math.sin(phi); const N = a / Math.sqrt(1 - e_sq * sin_lambda * sin_lambda); - const x = 0.5 * (h + N) * cos_lambda * cos_phi; - const y = 0.5 * (h + N) * cos_lambda * sin_phi; - const z = 0.5 * (h + (1 - e_sq) * N) * sin_lambda; + const x = N * cos_lambda * cos_phi; + const y = N * cos_lambda * sin_phi; + const z = N * inv_e_sq * sin_lambda; return { x, y, z, sin_lambda, cos_lambda, cos_phi, sin_phi }; } @@ -464,8 +464,8 @@ function EcefToEnu(e, origin) { return { x: xEast, y: yNorth }; } -// Converts the geodetic WGS-84 coordinated (lat, lon) at a default height to -// East-North-Up coordinates in a local tangent plane defined by an ECEF point origin +// Converts the geodetic WGS-84 coordinated (lat, lon) at ground level to +// east-north coordinates in a local tangent plane defined by an ECEF point origin export function GeodeticToEnu(lat, lon, origin) { const ecef = GeodeticToEcef(lat, lon); return EcefToEnu(ecef, origin); From 786cc39b8d6eaa5585579d75efe60de950d68d5c Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 12:15:29 +0200 Subject: [PATCH 05/10] feat: utility to project position to a path --- .../itinerary/navigator/NaviUtils.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/app/component/itinerary/navigator/NaviUtils.js b/app/component/itinerary/navigator/NaviUtils.js index a62c295538..d99bbbc3be 100644 --- a/app/component/itinerary/navigator/NaviUtils.js +++ b/app/component/itinerary/navigator/NaviUtils.js @@ -343,3 +343,73 @@ export const LEGTYPE = { PENDING: 'PENDING', END: 'END', }; + +function dist(p1, p2) { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); +} + +function vSub(p1, p2) { + const dx = p1.x - p2.x; + const dy = p1.y - p2.y; + return { dx, dy }; +} + +// compute how big part of a path has been traversed +// returns position's projection to path, distance from path +// and the ratio traversed/full length +export function pathProgress(pos, geom) { + const lengths = []; + + let p1 = geom[0]; + let distance = dist(pos, p1); + let minI = 0; + let minF = 0; + let totalLength = 0; + + for (let i = 0; i < geom.length - 1; i++) { + const p2 = geom[i + 1]; + const { dx, dy } = vSub(p2, p1); + const d = Math.sqrt(dx * dx + dy * dy); + lengths.push(d); + totalLength += d; + + if (d > 0.001) { + // interval distance in meters, safety check + const dlt = vSub(pos, p1); + const dp = dlt.dx * dx + dlt.dy * dy; // dot prod + + if (dp > 0) { + let f; + let cDist; + if (dp > 1) { + cDist = dist(p2, pos); + f = 1; + } else { + f = dp / d; // normalize + cDist = Math.sqrt(dlt.x * dlt.x + dlt.y * dlt.y - f * f); // pythag. + } + if (cDist < distance) { + distance = cDist; + minI = i; + minF = f; + } + } + p1 = p2; + } + } + + let traversed = minF * lengths[minI]; // last partial segment + for (let i = 0; i < minI; i++) { + traversed += lengths[i]; + } + traversed /= totalLength; + const { dx, dy } = vSub(geom[minI + 1], geom[minI]); + const projected = { + x: geom[minI].x + minF * dx, + y: geom[minI].y + minF * dy, + }; + + return { projected, distance, traversed }; +} From bc90fb6d23207c95a860cb6cb5d083a28eaa97a6 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 12:28:41 +0200 Subject: [PATCH 06/10] chore: pass position to NaviInstructions --- app/component/itinerary/navigator/NaviCard.js | 7 +++++++ app/component/itinerary/navigator/NaviCardContainer.js | 1 + 2 files changed, 8 insertions(+) diff --git a/app/component/itinerary/navigator/NaviCard.js b/app/component/itinerary/navigator/NaviCard.js index ffaea0a7e0..af3c62899a 100644 --- a/app/component/itinerary/navigator/NaviCard.js +++ b/app/component/itinerary/navigator/NaviCard.js @@ -28,6 +28,7 @@ export default function NaviCard({ cardExpanded, startTime, time, + position, }) { if (legType === LEGTYPE.PENDING) { return ( @@ -69,6 +70,7 @@ export default function NaviCard({ instructions={instructions} legType={legType} time={time} + position={position} />
From 8ec10caafac20668ab3e65684a51b4768f1615a8 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 12:42:47 +0200 Subject: [PATCH 07/10] chore: pass coordinate reference to NaviInstructions --- app/component/itinerary/navigator/NaviCard.js | 6 ++++++ app/component/itinerary/navigator/NaviCardContainer.js | 8 ++++++-- app/component/itinerary/navigator/NaviContainer.js | 9 ++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/component/itinerary/navigator/NaviCard.js b/app/component/itinerary/navigator/NaviCard.js index af3c62899a..806165fee0 100644 --- a/app/component/itinerary/navigator/NaviCard.js +++ b/app/component/itinerary/navigator/NaviCard.js @@ -29,6 +29,7 @@ export default function NaviCard({ startTime, time, position, + origin, }) { if (legType === LEGTYPE.PENDING) { return ( @@ -71,6 +72,7 @@ export default function NaviCard({ legType={legType} time={time} position={position} + origin={origin} />
@@ -208,7 +209,10 @@ NaviCardContainer.propTypes = { lat: PropTypes.number, lon: PropTypes.number, }), - + origin: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }).isRequired, /* focusToPoint: PropTypes.func.isRequired, */ diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js index bb0313369c..5f4bcf698f 100644 --- a/app/component/itinerary/navigator/NaviContainer.js +++ b/app/component/itinerary/navigator/NaviContainer.js @@ -13,21 +13,23 @@ function NaviContainer( { getStore }, ) { const [planarLegs, setPlanarLegs] = useState([]); + const [origin, setOrigin] = useState(); const position = getStore('PositionStore').getLocationState(); useEffect(() => { const { lat, lon } = itinerary.legs[0].from; - const origin = GeodeticToEcef(lat, lon); + const orig = GeodeticToEcef(lat, lon); const legs = itinerary.legs.map(leg => { const geometry = polyUtil.decode(leg.legGeometry.points); return { ...leg, - geometry: geometry.map(p => GeodeticToEnu(p[0], p[1], origin)), + geometry: geometry.map(p => GeodeticToEnu(p[0], p[1], orig)), }; }); setPlanarLegs(legs); - }, [itinerary.legs]); + setOrigin(orig); + }, [itinerary]); const { realTimeLegs, time, isPositioningAllowed } = useRealtimeLegs( planarLegs, @@ -69,6 +71,7 @@ function NaviContainer( } time={time} position={position} + origin={origin} /> Date: Tue, 3 Dec 2024 13:20:01 +0200 Subject: [PATCH 08/10] feat: update MOVE leg progress --- .../itinerary/navigator/NaviInstructions.js | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js index 3fc63f3c6a..006ff33e18 100644 --- a/app/component/itinerary/navigator/NaviInstructions.js +++ b/app/component/itinerary/navigator/NaviInstructions.js @@ -1,34 +1,37 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import { GeodeticToEnu, displayDistance } from '../../../util/geo-utils'; import { legShape, configShape } from '../../../util/shapes'; import { legDestination, legTimeStr, legTime } from '../../../util/legUtils'; import RouteNumber from '../../RouteNumber'; -import { LEGTYPE, getLocalizedMode } from './NaviUtils'; -import { displayDistance } from '../../../util/geo-utils'; +import { LEGTYPE, getLocalizedMode, pathProgress } from './NaviUtils'; import { durationToString } from '../../../util/timeUtils'; export default function NaviInstructions( - { leg, nextLeg, instructions, legType, time }, + { leg, nextLeg, instructions, legType, time, position, origin }, { intl, config }, ) { - const [fadeOut, setFadeOut] = useState(false); const withRealTime = (rt, children) => ( {children} ); - useEffect(() => { - const timer = setTimeout(() => { - setFadeOut(true); - }, 10000); - return () => { - setFadeOut(false); - clearTimeout(timer); - }; - }, [leg]); if (legType === LEGTYPE.MOVE) { - const { distance, duration } = leg; + let remainingTraversal; + + if (position?.lat && position?.lon) { + // TODO: maybe apply only when distance is close enough to the path + const posXY = GeodeticToEnu(position.lat, position.lon, origin); + const { traversed } = pathProgress(posXY, leg.geometry); + remainingTraversal = 1.0 - traversed; + } else { + // estimate from elapsed time + remainingTraversal = (legTime(leg.end) - time) / (leg.duration * 1000); + } + const duration = leg.duration * remainingTraversal; + const distance = leg.distance * remainingTraversal; + return ( <>
@@ -36,12 +39,10 @@ export default function NaviInstructions(   {legDestination(intl, leg, null, nextLeg)}
- {distance && duration && ( -
- {displayDistance(distance, config, intl.formatNumber)}   ( - {durationToString(duration * 1000)}) -
- )} +
+ {displayDistance(distance, config, intl.formatNumber)}   ( + {durationToString(duration * 1000)}) +
); } @@ -134,12 +135,21 @@ NaviInstructions.propTypes = { instructions: PropTypes.string.isRequired, legType: PropTypes.string, time: PropTypes.number.isRequired, + position: PropTypes.shape({ + lat: PropTypes.number, + lon: PropTypes.number, + }), + origin: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }).isRequired, }; NaviInstructions.defaultProps = { legType: '', leg: undefined, nextLeg: undefined, + position: undefined, }; NaviInstructions.contextTypes = { intl: intlShape.isRequired, From 4490ef85d49aee77eec521e370d69c0b669d842a Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 14:00:37 +0200 Subject: [PATCH 09/10] fix: no need to estimate arrival change for bottom sheet Whole itinerary is adjusted to include realtime changes --- .../itinerary/navigator/NaviContainer.js | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js index 5f4bcf698f..0e48ee3dd8 100644 --- a/app/component/itinerary/navigator/NaviContainer.js +++ b/app/component/itinerary/navigator/NaviContainer.js @@ -41,26 +41,6 @@ function NaviContainer( return null; } - // recompute estimated arrival - let lastTransitLeg; - let arrivalChange = 0; - - realTimeLegs.forEach(leg => { - if (leg.transitLeg) { - lastTransitLeg = leg; - } - }); - - if (lastTransitLeg) { - const rtLeg = realTimeLegs.find(leg => { - return leg.legId === lastTransitLeg.legId; - }); - arrivalChange = legTime(rtLeg.end) - legTime(lastTransitLeg.end); - } - - const arrivalTime = - legTime(realTimeLegs[realTimeLegs.length - 1].end) + arrivalChange; - return ( <> From 3654e218bf445c56ce5a5548ea3faadd6ef2c3f9 Mon Sep 17 00:00:00 2001 From: Vesa Meskanen Date: Tue, 3 Dec 2024 14:16:28 +0200 Subject: [PATCH 10/10] fix: roll segments correctly even when a segment is collapsed one --- app/component/itinerary/navigator/NaviUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/component/itinerary/navigator/NaviUtils.js b/app/component/itinerary/navigator/NaviUtils.js index d99bbbc3be..5be06af2b3 100644 --- a/app/component/itinerary/navigator/NaviUtils.js +++ b/app/component/itinerary/navigator/NaviUtils.js @@ -396,8 +396,8 @@ export function pathProgress(pos, geom) { minF = f; } } - p1 = p2; } + p1 = p2; } let traversed = minF * lengths[minI]; // last partial segment