From 742b37683fe1036b23e75e195b58e03279f4d52d Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Mon, 16 Dec 2024 10:14:11 +0200 Subject: [PATCH 1/7] Prevent finished leg from reappearing as current --- .../navigator/hooks/useRealtimeLegs.js | 31 ++++++++++++++----- app/util/legUtils.js | 20 ++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js index 83bdc655d2..a3ac804665 100644 --- a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js +++ b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js @@ -1,8 +1,8 @@ import polyUtil from 'polyline-encoded'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { fetchQuery } from 'react-relay'; import { GeodeticToEcef, GeodeticToEnu } from '../../../../util/geo-utils'; -import { legTime } from '../../../../util/legUtils'; +import { legTime, isAnyLegPropertyIdentical } from '../../../../util/legUtils'; import { epochToIso } from '../../../../util/timeUtils'; import { legQuery } from '../../queries/LegQuery'; @@ -87,7 +87,7 @@ function matchLegEnds(legs) { } } -function getLegsOfInterest(legs, time) { +function getLegsOfInterest(legs, time, previousFinishedLeg) { if (!legs?.length) { return { firstLeg: undefined, @@ -99,12 +99,26 @@ function getLegsOfInterest(legs, time) { const firstLeg = legs[0]; const lastLeg = legs[legs.length - 1]; - const nextLeg = legs.find(({ start }) => legTime(start) > time); - const previousLeg = legs.findLast(({ end }) => legTime(end) < time); - const currentLeg = legs.find( + const nextLegIdx = legs.findIndex(({ start }) => legTime(start) > time); + const currentLegIdx = legs.findIndex( ({ start, end }) => legTime(start) <= time && legTime(end) >= time, ); + let previousLeg = legs.findLast(({ end }) => legTime(end) < time); + let nextLeg = legs[nextLegIdx]; + let currentLeg = legs[currentLegIdx]; + + if ( + isAnyLegPropertyIdentical(currentLeg, previousFinishedLeg, [ + 'legId', + 'legGeometry.points', + ]) + ) { + previousLeg = currentLeg; + currentLeg = nextLeg; + nextLeg = legs[nextLegIdx + 1]; + } + return { firstLeg, lastLeg, @@ -117,6 +131,7 @@ function getLegsOfInterest(legs, time) { const useRealtimeLegs = (relayEnvironment, initialLegs = []) => { const [realTimeLegs, setRealTimeLegs] = useState(); const [time, setTime] = useState(Date.now()); + const previousFinishedLeg = useRef(undefined); const origin = useMemo( () => GeodeticToEcef(initialLegs[0].from.lat, initialLegs[0].from.lon), @@ -200,7 +215,9 @@ const useRealtimeLegs = (relayEnvironment, initialLegs = []) => { }, [fetchAndSetRealtimeLegs]); const { firstLeg, lastLeg, currentLeg, nextLeg, previousLeg } = - getLegsOfInterest(realTimeLegs, time); + getLegsOfInterest(realTimeLegs, time, previousFinishedLeg.current); + + previousFinishedLeg.current = previousLeg; return { realTimeLegs, diff --git a/app/util/legUtils.js b/app/util/legUtils.js index 71f3dd35f0..f9f5c4c4ef 100644 --- a/app/util/legUtils.js +++ b/app/util/legUtils.js @@ -3,6 +3,26 @@ import get from 'lodash/get'; import { BIKEAVL_UNKNOWN } from './vehicleRentalUtils'; import { getRouteMode } from './modeUtils'; +function getNestedValue(obj, path) { + return path.split('.').reduce((acc, part) => acc && acc[part], obj); +} + +export function isAnyLegPropertyIdentical(leg1, leg2, properties) { + if (!leg1 || !leg2) { + return false; + } + + for (let i = 0; i < properties.length; i++) { + const property = properties[i]; + const val1 = getNestedValue(leg1, property); + const val2 = getNestedValue(leg2, property); + if (val1 && val2 && val1 === val2) { + return true; + } + } + return false; +} + /** * Get time as milliseconds since the Unix Epoch */ From ce7e9f56fb761ad98bcfdce814b5b2b053646672 Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Wed, 18 Dec 2024 09:52:09 +0200 Subject: [PATCH 2/7] Added jsdoc for new functions --- app/util/legUtils.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/app/util/legUtils.js b/app/util/legUtils.js index f9f5c4c4ef..cf3e8fdc8e 100644 --- a/app/util/legUtils.js +++ b/app/util/legUtils.js @@ -1,13 +1,34 @@ import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; -import { BIKEAVL_UNKNOWN } from './vehicleRentalUtils'; import { getRouteMode } from './modeUtils'; +import { BIKEAVL_UNKNOWN } from './vehicleRentalUtils'; -function getNestedValue(obj, path) { - return path.split('.').reduce((acc, part) => acc && acc[part], obj); +/** + * Gets a (nested) property value from an object + * + * @param {Object.} obj object with properties i.e. { foo: 'bar', baz: {qux: 'quux'} } + * @param {string} propertyString string representation of object property i.e. foo, baz.qux + * @returns {Object} + */ +function getNestedValue(obj, propertyString) { + return propertyString.split('.').reduce((acc, part) => acc && acc[part], obj); } +/** + * Compares if given legs share any of the given properties. + * Can be used to check if two separate leg objects are identical + * Returns true if both legs are null|undefined + * + * @param {Object.|undefined} leg1 + * @param {Object.|undefined} leg2 + * @param {string[]} properties list of object fields to compare i.e. ['foo', 'bar.baz'] + * @returns {boolean} + */ export function isAnyLegPropertyIdentical(leg1, leg2, properties) { + if (leg1 === leg2) { + return true; + } + if (!leg1 || !leg2) { return false; } From 28d8a89c22f4595270f8de353e8af42054d7abe0 Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Wed, 18 Dec 2024 15:06:06 +0200 Subject: [PATCH 3/7] Added LegMode.Wait pseudomode --- app/component/itinerary/Itinerary.js | 3 ++- app/component/map/ItineraryLine.js | 3 ++- app/util/legUtils.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index acf5d4de2a..8394f51440 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -24,6 +24,7 @@ import { getTotalDistance, legTime, legTimeStr, + LegMode, } from '../../util/legUtils'; import { dateOrEmpty, isTomorrow, timeStr } from '../../util/timeUtils'; import withBreakpoint from '../../util/withBreakpoint'; @@ -612,7 +613,7 @@ const Itinerary = ( renderModeIcons={renderModeIcons} duration={waitingTimeinMin} isTransitLeg={false} - mode="WAIT" + mode={LegMode.Wait} large={breakpoint === 'large'} />, ); diff --git a/app/component/map/ItineraryLine.js b/app/component/map/ItineraryLine.js index 7aaf885779..fa96bc0ccf 100644 --- a/app/component/map/ItineraryLine.js +++ b/app/component/map/ItineraryLine.js @@ -9,6 +9,7 @@ import { getInterliningLegs, getLegText, isCallAgencyLeg, + LegMode, } from '../../util/legUtils'; import { getRouteMode } from '../../util/modeUtils'; import { configShape, legShape } from '../../util/shapes'; @@ -62,7 +63,7 @@ class ItineraryLine extends React.Component { const transitLegs = []; this.props.legs.forEach((leg, i) => { - if (!leg || leg.mode === 'WAIT') { + if (!leg || leg.mode === LegMode.Wait) { return; } const nextLeg = this.props.legs[i + 1]; diff --git a/app/util/legUtils.js b/app/util/legUtils.js index cf3e8fdc8e..366a8e833d 100644 --- a/app/util/legUtils.js +++ b/app/util/legUtils.js @@ -122,6 +122,7 @@ export const LegMode = { Walk: 'WALK', Car: 'CAR', Rail: 'RAIL', + Wait: 'WAIT', }; /** From 92f0117061a7fa8c986dfc2429723d283098ba48 Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Wed, 18 Dec 2024 16:46:11 +0200 Subject: [PATCH 4/7] Use dummy leg enriched leg data to deduce legs of interest --- .../navigator/hooks/useRealtimeLegs.js | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js index a3ac804665..30e7206033 100644 --- a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js +++ b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js @@ -2,7 +2,11 @@ import polyUtil from 'polyline-encoded'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { fetchQuery } from 'react-relay'; import { GeodeticToEcef, GeodeticToEnu } from '../../../../util/geo-utils'; -import { legTime, isAnyLegPropertyIdentical } from '../../../../util/legUtils'; +import { + isAnyLegPropertyIdentical, + legTime, + LegMode, +} from '../../../../util/legUtils'; import { epochToIso } from '../../../../util/timeUtils'; import { legQuery } from '../../queries/LegQuery'; @@ -87,8 +91,8 @@ function matchLegEnds(legs) { } } -function getLegsOfInterest(legs, time, previousFinishedLeg) { - if (!legs?.length) { +function getLegsOfInterest(initialLegs, time, previousFinishedLeg) { + if (!initialLegs?.length) { return { firstLeg: undefined, lastLeg: undefined, @@ -97,18 +101,28 @@ function getLegsOfInterest(legs, time, previousFinishedLeg) { }; } - const firstLeg = legs[0]; - const lastLeg = legs[legs.length - 1]; + const legs = initialLegs.reduce((acc, curr, i, arr) => { + acc.push(curr); + const next = arr[i + 1]; + + // A wait leg is added, if next leg exists but it does not start when current ends + if (next && legTime(curr.end) !== legTime(next.start)) { + acc.push({ mode: LegMode.Wait, start: curr.end, end: next.start }); + } + + return acc; + }, []); + const nextLegIdx = legs.findIndex(({ start }) => legTime(start) > time); - const currentLegIdx = legs.findIndex( + let currentLeg = legs.find( ({ start, end }) => legTime(start) <= time && legTime(end) >= time, ); - let previousLeg = legs.findLast(({ end }) => legTime(end) < time); let nextLeg = legs[nextLegIdx]; - let currentLeg = legs[currentLegIdx]; + // Indices are shifted by one if a previously completed leg reappears as current. if ( + nextLeg && isAnyLegPropertyIdentical(currentLeg, previousFinishedLeg, [ 'legId', 'legGeometry.points', @@ -119,12 +133,13 @@ function getLegsOfInterest(legs, time, previousFinishedLeg) { nextLeg = legs[nextLegIdx + 1]; } + // return wait legs as undefined as they are not a global concept return { - firstLeg, - lastLeg, - previousLeg, - currentLeg, - nextLeg, + firstLeg: legs[0], + lastLeg: legs[legs.length - 1], + previousLeg: previousLeg?.mode === LegMode.Wait ? undefined : previousLeg, + currentLeg: currentLeg?.mode === LegMode.Wait ? undefined : currentLeg, + nextLeg: nextLeg?.mode === LegMode.Wait ? undefined : nextLeg, }; } From 0d1563ddf8cefece528f4dacf4e2780e2579beb2 Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Thu, 19 Dec 2024 15:37:13 +0200 Subject: [PATCH 5/7] Fix issue in flakiness correction --- .../navigator/hooks/useRealtimeLegs.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js index 30e7206033..83dd513fe3 100644 --- a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js +++ b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js @@ -107,7 +107,13 @@ function getLegsOfInterest(initialLegs, time, previousFinishedLeg) { // A wait leg is added, if next leg exists but it does not start when current ends if (next && legTime(curr.end) !== legTime(next.start)) { - acc.push({ mode: LegMode.Wait, start: curr.end, end: next.start }); + acc.push({ + id: null, + legGeometry: { points: null }, + mode: LegMode.Wait, + start: curr.end, + end: next.start, + }); } return acc; @@ -122,7 +128,6 @@ function getLegsOfInterest(initialLegs, time, previousFinishedLeg) { // Indices are shifted by one if a previously completed leg reappears as current. if ( - nextLeg && isAnyLegPropertyIdentical(currentLeg, previousFinishedLeg, [ 'legId', 'legGeometry.points', @@ -130,16 +135,15 @@ function getLegsOfInterest(initialLegs, time, previousFinishedLeg) { ) { previousLeg = currentLeg; currentLeg = nextLeg; - nextLeg = legs[nextLegIdx + 1]; + nextLeg = nextLegIdx !== -1 ? legs[nextLegIdx + 1] : undefined; } - // return wait legs as undefined as they are not a global concept return { firstLeg: legs[0], lastLeg: legs[legs.length - 1], - previousLeg: previousLeg?.mode === LegMode.Wait ? undefined : previousLeg, - currentLeg: currentLeg?.mode === LegMode.Wait ? undefined : currentLeg, - nextLeg: nextLeg?.mode === LegMode.Wait ? undefined : nextLeg, + previousLeg, + currentLeg, + nextLeg, }; } @@ -234,15 +238,16 @@ const useRealtimeLegs = (relayEnvironment, initialLegs = []) => { previousFinishedLeg.current = previousLeg; + // return wait legs as undefined as they are not a global concept return { realTimeLegs, time, origin, firstLeg, lastLeg, - previousLeg, - currentLeg, - nextLeg, + previousLeg: previousLeg?.mode === LegMode.Wait ? undefined : previousLeg, + currentLeg: currentLeg?.mode === LegMode.Wait ? undefined : currentLeg, + nextLeg: nextLeg?.mode === LegMode.Wait ? undefined : nextLeg, }; }; From eaad04c9ef738e10b305e1b2bb9ba97efe7b9c6a Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Thu, 19 Dec 2024 15:38:26 +0200 Subject: [PATCH 6/7] Prevent negative remainingDuration --- app/component/itinerary/navigator/NaviInstructions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js index 6f85368c03..f66a4fceb5 100644 --- a/app/component/itinerary/navigator/NaviInstructions.js +++ b/app/component/itinerary/navigator/NaviInstructions.js @@ -48,7 +48,10 @@ export default function NaviInstructions( const hs = headsign || nextLeg.trip?.tripHeadsign; const localizedMode = getLocalizedMode(mode, intl); - const remainingDuration = Math.ceil((legTime(start) - time) / 60000); // ms to minutes + const remainingDuration = Math.max( + Math.ceil((legTime(start) - time) / 60000), + 0, + ); // ms to minutes const rt = nextLeg.realtimeState === 'UPDATED'; const values = { duration: withRealTime(rt, remainingDuration), From 69dfcb0ce22a5cb5cb81b9935d73e8954bdfb229 Mon Sep 17 00:00:00 2001 From: Simo Partinen Date: Thu, 19 Dec 2024 16:44:44 +0200 Subject: [PATCH 7/7] Added missing Math.max call --- app/component/itinerary/navigator/NaviInstructions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js index f66a4fceb5..9676c0879f 100644 --- a/app/component/itinerary/navigator/NaviInstructions.js +++ b/app/component/itinerary/navigator/NaviInstructions.js @@ -51,7 +51,7 @@ export default function NaviInstructions( const remainingDuration = Math.max( Math.ceil((legTime(start) - time) / 60000), 0, - ); // ms to minutes + ); // ms to minutes, >= 0 const rt = nextLeg.realtimeState === 'UPDATED'; const values = { duration: withRealTime(rt, remainingDuration), @@ -104,7 +104,7 @@ export default function NaviInstructions( : intl.formatMessage({ id: 'navileg-from-stop' }); const localizedMode = getLocalizedMode(leg.mode, intl); - const remainingDuration = Math.ceil((t - time) / 60000); // ms to minutes + const remainingDuration = Math.max(Math.ceil((t - time) / 60000), 0); // ms to minutes, >= 0 const values = { stopOrStation, stop: leg.to.stop.name,