diff --git a/app/component/itinerary/navigator/NaviCardContainer.js b/app/component/itinerary/navigator/NaviCardContainer.js
index 89366f9817..b680a0c72c 100644
--- a/app/component/itinerary/navigator/NaviCardContainer.js
+++ b/app/component/itinerary/navigator/NaviCardContainer.js
@@ -12,9 +12,9 @@ import {
getItineraryAlerts,
getTransitLegState,
LEGTYPE,
+ DESTINATION_RADIUS,
} from './NaviUtils';
-const DESTINATION_RADIUS = 20; // meters
const TIME_AT_DESTINATION = 3; // * 10 seconds
const TOPBAR_PADDING = 8; // pixels
@@ -82,7 +82,16 @@ function NaviCardContainer(
// Alerts for NaviStack
addMessages(
incomingMessages,
- getItineraryAlerts(legs, intl, messages, match.params, router),
+ getItineraryAlerts(
+ legs,
+ time,
+ position,
+ origin,
+ intl,
+ messages,
+ match.params,
+ router,
+ ),
);
if (currentLeg) {
@@ -107,16 +116,11 @@ function NaviCardContainer(
: activeMessages;
// handle messages that are updated.
- const updatedMessages = previousValidMessages.map(msg => {
- const incoming = incomingMessages.get(msg.id);
- if (incoming) {
- incomingMessages.delete(msg.id);
- return incoming;
- }
- return msg;
- });
+ const keptMessages = previousValidMessages.filter(
+ msg => !!incomingMessages.get(msg.id),
+ );
const newMessages = Array.from(incomingMessages.values());
- setActiveMessages([...updatedMessages, ...newMessages]);
+ setActiveMessages([...keptMessages, ...newMessages]);
setMessages(new Map([...messages, ...incomingMessages]));
}
@@ -210,7 +214,8 @@ NaviCardContainer.propTypes = {
lat: PropTypes.number,
lon: PropTypes.number,
}),
- mapLayerRef: PropTypes.func.isRequired,
+ mapLayerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
+ .isRequired,
origin: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js
index e4a4092a53..505916eb41 100644
--- a/app/component/itinerary/navigator/NaviContainer.js
+++ b/app/component/itinerary/navigator/NaviContainer.js
@@ -9,8 +9,8 @@ import NaviBottom from './NaviBottom';
import NaviCardContainer from './NaviCardContainer';
import { useRealtimeLegs } from './hooks/useRealtimeLegs';
import NavigatorOutroModal from './navigatoroutro/NavigatorOutroModal';
+import { DESTINATION_RADIUS } from './NaviUtils';
-const DESTINATION_RADIUS = 20; // meters
const ADDITIONAL_ARRIVAL_TIME = 60000; // 60 seconds in ms
function NaviContainer(
@@ -27,7 +27,10 @@ function NaviContainer(
) {
const [isPositioningAllowed, setPositioningAllowed] = useState(false);
- const position = getStore('PositionStore').getLocationState();
+ let position = getStore('PositionStore').getLocationState();
+ if (!position.hasLocation) {
+ position = null;
+ }
const {
realTimeLegs,
@@ -41,7 +44,7 @@ function NaviContainer(
} = useRealtimeLegs(relayEnvironment, legs);
useEffect(() => {
- if (position.hasLocation) {
+ if (position) {
mapRef?.enableMapTracking();
setPositioningAllowed(true);
} else {
@@ -107,7 +110,8 @@ NaviContainer.propTypes = {
isNavigatorIntroDismissed: PropTypes.bool,
// eslint-disable-next-line
mapRef: PropTypes.object,
- mapLayerRef: PropTypes.func.isRequired,
+ mapLayerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
+ .isRequired,
};
NaviContainer.contextTypes = {
diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js
index 0786f8bbbb..7af09f1909 100644
--- a/app/component/itinerary/navigator/NaviInstructions.js
+++ b/app/component/itinerary/navigator/NaviInstructions.js
@@ -2,11 +2,11 @@ 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 { 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, pathProgress } from './NaviUtils';
+import { LEGTYPE, getLocalizedMode, getRemainingTraversal } from './NaviUtils';
import { durationToString } from '../../../util/timeUtils';
export default function NaviInstructions(
@@ -18,20 +18,12 @@ export default function NaviInstructions(
);
if (legType === LEGTYPE.MOVE) {
- 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 = Math.max(
- (legTime(leg.end) - time) / (leg.duration * 1000),
- 0,
- );
- }
+ const remainingTraversal = getRemainingTraversal(
+ leg,
+ position,
+ origin,
+ time,
+ );
const duration = leg.duration * remainingTraversal;
const distance = leg.distance * remainingTraversal;
@@ -43,7 +35,7 @@ export default function NaviInstructions(
{legDestination(intl, leg, null, nextLeg)}
-
+
{displayDistance(distance, config, intl.formatNumber)} (
{durationToString(duration * 1000)})
diff --git a/app/component/itinerary/navigator/NaviUtils.js b/app/component/itinerary/navigator/NaviUtils.js
index 16483126ff..a8d970074f 100644
--- a/app/component/itinerary/navigator/NaviUtils.js
+++ b/app/component/itinerary/navigator/NaviUtils.js
@@ -1,5 +1,7 @@
import React from 'react';
+import distance from '@digitransit-search-util/digitransit-search-util-distance';
import { FormattedMessage } from 'react-intl';
+import { GeodeticToEnu } from '../../../util/geo-utils';
import { legTime } from '../../../util/legUtils';
import { timeStr } from '../../../util/timeUtils';
import { getFaresFromLegs } from '../../../util/fareUtils';
@@ -8,7 +10,93 @@ import { getItineraryPagePath } from '../../../util/path';
const TRANSFER_SLACK = 60000;
const DISPLAY_MESSAGE_THRESHOLD = 120 * 1000; // 2 minutes
-function findTransferProblem(legs) {
+
+export const DESTINATION_RADIUS = 20; // meters
+
+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 dst = 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 < dst) {
+ dst = 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: dst, traversed };
+}
+
+export function getRemainingTraversal(leg, pos, origin, time) {
+ if (pos) {
+ // TODO: maybe apply only when distance is close enough to the path
+ const posXY = GeodeticToEnu(pos.lat, pos.lon, origin);
+ const { traversed } = pathProgress(posXY, leg.geometry);
+ return 1.0 - traversed;
+ }
+ // estimate from elapsed time
+ return Math.max((legTime(leg.end) - time) / (leg.duration * 1000), 0);
+}
+
+function findTransferProblems(legs, time, position, origin) {
+ const problems = [];
+
for (let i = 1; i < legs.length - 1; i++) {
const prev = legs[i - 1];
const leg = legs[i];
@@ -16,8 +104,14 @@ function findTransferProblem(legs) {
if (prev.transitLeg && leg.transitLeg && !leg.interlineWithPreviousLeg) {
// transfer at a stop
- if (legTime(leg.start) - legTime(prev.end) < TRANSFER_SLACK) {
- return [prev, leg];
+ const start = legTime(leg.start);
+ const end = legTime(prev.end);
+ if (start > time && start - end < TRANSFER_SLACK) {
+ problems.push({
+ severity: start > end ? 'ALERT' : 'WARNING',
+ fromLeg: prev,
+ toLeg: leg,
+ });
}
}
@@ -25,14 +119,45 @@ function findTransferProblem(legs) {
// transfer with some walking
const t1 = legTime(prev.end);
const t2 = legTime(next.start);
- const transferDuration = legTime(leg.end) - legTime(leg.start);
- const slack = t2 - t1 - transferDuration;
- if (slack < TRANSFER_SLACK) {
- return [prev, next];
+ if (t2 > time) {
+ // transfer is not over yet
+ if (t1 > t2) {
+ // certain failure, next transit departs before previous arrives
+ problems.push({
+ severity: 'ALERT',
+ fromLeg: prev,
+ toLeg: next,
+ });
+ } else {
+ const transferDuration = legTime(leg.end) - legTime(leg.start);
+ // check if user is already at the next departure stop
+ const atStop =
+ position && distance(position, leg.to) <= DESTINATION_RADIUS;
+ const slack = t2 - t1 - transferDuration;
+ if (!atStop && slack < TRANSFER_SLACK) {
+ // original transfer not possible
+ let severity = 'WARNING';
+ const toGo = getRemainingTraversal(leg, position, origin, time);
+ const timeLeft = (t2 - time) / 1000;
+ if (toGo > 0 && timeLeft > 0) {
+ const originalSpeed = leg.distance / leg.duration;
+ const newSpeed = (toGo * leg.distance) / timeLeft;
+ if (newSpeed > 2 * originalSpeed) {
+ // double speed compared to user's routing preference
+ severity = 'ALERT';
+ }
+ }
+ problems.push({
+ severity,
+ fromLeg: prev,
+ toLeg: next,
+ });
+ }
+ }
}
}
}
- return null;
+ return problems;
}
export const getLocalizedMode = (mode, intl) => {
return intl.formatMessage({
@@ -144,8 +269,19 @@ export const getTransitLegState = (leg, intl, messages, time) => {
return state;
};
-export const getItineraryAlerts = (legs, intl, messages, location, router) => {
- const canceled = legs.filter(leg => leg.realtimeState === 'CANCELED');
+export const getItineraryAlerts = (
+ legs,
+ time,
+ position,
+ origin,
+ intl,
+ messages,
+ location,
+ router,
+) => {
+ const canceled = legs.filter(
+ leg => leg.realtimeState === 'CANCELED' && legTime(leg.start) > time,
+ );
let content;
const alerts = legs.flatMap(leg => {
return leg.alerts
@@ -178,7 +314,6 @@ export const getItineraryAlerts = (legs, intl, messages, location, router) => {
id: alert.id,
}));
});
- const transferProblem = findTransferProblem(legs);
const abortTrip =
;
const withShowRoutesBtn = children => (
@@ -228,26 +363,33 @@ export const getItineraryAlerts = (legs, intl, messages, location, router) => {
});
}
- if (transferProblem !== null) {
- const transferId = `transfer-${transferProblem[0].legId}-${transferProblem[1].legId}}`;
- if (!messages.get(transferId)) {
+ const transferProblems = findTransferProblems(legs, time, position, origin);
+ if (transferProblems.length) {
+ let prob = transferProblems.find(p => p.severity === 'ALERT');
+ if (!prob) {
+ // just take first
+ [prob] = transferProblems;
+ }
+ const transferId = `transfer-${prob.fromLeg.legId}-${prob.toLeg.legId}}`;
+ const alert = messages.get(transferId);
+ if (!alert || alert.severity !== prob.severity) {
content = withShowRoutesBtn(
{abortTrip}
,
);
alerts.push({
- severity: 'ALERT',
+ severity: prob.severity,
content,
id: transferId,
- hideClose: true,
+ hideClose: prob.severity === 'ALERT',
});
}
}
@@ -345,73 +487,3 @@ 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 };
-}