mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-07-27 23:35:15 +02:00
799 lines
22 KiB
JavaScript
799 lines
22 KiB
JavaScript
import distance from '@digitransit-search-util/digitransit-search-util-distance';
|
|
import cx from 'classnames';
|
|
import React from 'react';
|
|
import { FormattedMessage } from 'react-intl';
|
|
import { ExtendedRouteTypes } from '../../../constants';
|
|
import { addAnalyticsEvent } from '../../../util/analyticsUtils';
|
|
import { GeodeticToEnu } from '../../../util/geo-utils';
|
|
import { legTime, legTimeAcc } from '../../../util/legUtils';
|
|
import { getRouteMode } from '../../../util/modeUtils';
|
|
import { locationToUri } from '../../../util/otpStrings';
|
|
import { getItineraryPagePath } from '../../../util/path';
|
|
import { durationToString, epochToIso, timeStr } from '../../../util/timeUtils';
|
|
import {
|
|
getFeedWithoutId,
|
|
isExternalFeed,
|
|
} from '../../../util/feedScopedIdUtils';
|
|
import Icon from '../../Icon';
|
|
import { getModeIconColor } from '../../../util/colorUtils';
|
|
import RouteNumberContainer from '../../RouteNumberContainer';
|
|
import Duration from '../Duration';
|
|
import {
|
|
formatFare,
|
|
getFaresFromLegs,
|
|
shouldShowFareInfo,
|
|
} from '../../../util/fareUtils';
|
|
|
|
const DISPLAY_MESSAGE_THRESHOLD = 120 * 1000; // 2 minutes
|
|
const EARLIEST_NEXT_STOP = 60 * 1000;
|
|
const NOTED_SEVERITY = ['WARNING', 'ALERT'];
|
|
|
|
export const DESTINATION_RADIUS = 20; // meters
|
|
export const ACCEPT_LOCATION_RADIUS = 1000;
|
|
|
|
export const LEGTYPE = {
|
|
WAIT: 'WAIT',
|
|
MOVE: 'MOVE',
|
|
TRANSIT: 'TRANSIT',
|
|
PENDING: 'PENDING',
|
|
END: 'END',
|
|
WAIT_IN_VEHICLE: 'WAIT_IN_VEHICLE',
|
|
};
|
|
|
|
export function summaryString(legs, time, previousLeg, currentLeg, nextLeg) {
|
|
const parts = epochToIso(time).split('T')[1].split('+');
|
|
let msg = `${parts[0]} `;
|
|
const colors = [];
|
|
|
|
legs.forEach(l => {
|
|
let color = 'color:gray';
|
|
if (legTime(l.start) <= time && time <= legTime(l.end)) {
|
|
color = 'color:green';
|
|
} else if (l.transitLeg) {
|
|
color = 'color:pink';
|
|
}
|
|
colors.push(l.freezeStart ? 'color:lightblue' : color);
|
|
msg += `%c${legTimeAcc(l.start)}`;
|
|
colors.push('color:gray');
|
|
msg += `%c-`;
|
|
colors.push(l.freezeEnd ? 'color:lightblue' : color);
|
|
msg += `%c${legTimeAcc(l.end)} `;
|
|
});
|
|
colors.push('color:gray');
|
|
msg += `%c${previousLeg?.mode} ${currentLeg?.mode} ${nextLeg?.mode}`;
|
|
colors.unshift(msg);
|
|
|
|
return colors;
|
|
}
|
|
|
|
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, orthogonalDistance: dst, traversed };
|
|
}
|
|
|
|
export function getRemainingTraversal(leg, pos, origin, time) {
|
|
if (pos) {
|
|
const posXY = GeodeticToEnu(pos.lat, pos.lon, origin);
|
|
const { traversed, orthogonalDistance } = pathProgress(posXY, leg.geometry);
|
|
if (orthogonalDistance < ACCEPT_LOCATION_RADIUS) {
|
|
return 1.0 - traversed;
|
|
}
|
|
}
|
|
// estimate from elapsed time
|
|
const duration = Math.max(legTime(leg.end) - legTime(leg.start), 1); // min 1 ms
|
|
return Math.min(Math.max((legTime(leg.end) - time) / duration, 0), 1.0);
|
|
}
|
|
|
|
export function legTraversal(leg, origin, pos) {
|
|
const posXY = GeodeticToEnu(pos.lat, pos.lon, origin);
|
|
const { traversed, orthogonalDistance } = pathProgress(posXY, leg.geometry);
|
|
const metersToGo = (1.0 - traversed) * leg.distance;
|
|
|
|
return orthogonalDistance > ACCEPT_LOCATION_RADIUS
|
|
? null
|
|
: { traversed, metersToGo };
|
|
}
|
|
|
|
function transferId(transfer) {
|
|
return `transfer-${transfer.fromLeg.legId}-${transfer.toLeg.legId}`;
|
|
}
|
|
|
|
function findTransferProblems(legs, time, position, tailLength, slack) {
|
|
const transfers = [];
|
|
|
|
for (let i = 1; i < legs.length - 1; i++) {
|
|
const prev = legs[i - 1];
|
|
const leg = legs[i];
|
|
const next = legs[i + 1];
|
|
|
|
if (prev.transitLeg && leg.transitLeg && !leg.interlineWithPreviousLeg) {
|
|
// transfer at a stop
|
|
const start = legTime(leg.start);
|
|
const end = legTime(prev.end);
|
|
if (start > time) {
|
|
const duration = start - end;
|
|
let severity;
|
|
if (start < end) {
|
|
severity = 'ALERT';
|
|
} else if (duration < slack) {
|
|
severity = 'WARNING';
|
|
} else {
|
|
severity = 'INFO'; // normal transfer
|
|
}
|
|
transfers.push({
|
|
severity,
|
|
fromLeg: prev,
|
|
toLeg: leg,
|
|
duration,
|
|
slack: duration,
|
|
originalDuration:
|
|
legTime(leg.originalStart) - legTime(prev.originalEnd),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (prev.transitLeg && next.transitLeg && !leg.transitLeg) {
|
|
// transfer with some walking
|
|
const t1 = legTime(prev.end);
|
|
const t2 = legTime(next.start);
|
|
const duration = t2 - t1;
|
|
const originalDuration =
|
|
legTime(next.originalStart) - legTime(prev.originalEnd);
|
|
if (t2 > time) {
|
|
// transfer is not over yet
|
|
if (t1 > t2) {
|
|
// certain failure, next transit departs before previous arrives
|
|
transfers.push({
|
|
severity: 'ALERT',
|
|
fromLeg: prev,
|
|
toLeg: next,
|
|
});
|
|
} else {
|
|
const legDuration = leg.duration * 1000; // this is original duration
|
|
// check if user is already at the next departure stop
|
|
const atStop =
|
|
position && distance(position, leg.to) <= DESTINATION_RADIUS;
|
|
let currentSlack = duration - legDuration;
|
|
if (!atStop && currentSlack < slack) {
|
|
// original transfer not possible
|
|
let severity = 'WARNING';
|
|
let toGo;
|
|
let timeLeft;
|
|
// has transit walk already started ?
|
|
if (time > legTime(leg.start)) {
|
|
// compute how transit is proceeding
|
|
toGo = tailLength;
|
|
timeLeft = (t2 - time) / 1000;
|
|
} else {
|
|
toGo = leg.distance;
|
|
timeLeft = duration / 1000; // should we consider also transfer slack here?
|
|
}
|
|
if (toGo > 0) {
|
|
const originalSpeed = leg.distance / leg.duration;
|
|
const newSpeed = toGo / (timeLeft + 0.0001);
|
|
if (newSpeed > 1.5 * originalSpeed) {
|
|
// too high speed compared to user's routing preference
|
|
severity = 'ALERT';
|
|
}
|
|
}
|
|
transfers.push({
|
|
severity,
|
|
fromLeg: prev,
|
|
toLeg: next,
|
|
duration,
|
|
originalDuration,
|
|
});
|
|
} else {
|
|
if (atStop) {
|
|
currentSlack *= 2; // no slack prob if at stop
|
|
}
|
|
transfers.push({
|
|
severity: 'INFO',
|
|
fromLeg: prev,
|
|
toLeg: next,
|
|
duration,
|
|
originalDuration,
|
|
slack: currentSlack,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return transfers;
|
|
}
|
|
|
|
export const getLocalizedMode = (mode, intl) => {
|
|
return intl.formatMessage({
|
|
id: `${mode.toLowerCase()}`,
|
|
defaultMessage: `${mode}`,
|
|
});
|
|
};
|
|
|
|
export const getToLocalizedMode = (mode, intl) => {
|
|
return intl.formatMessage({
|
|
id: `to-${mode.toLowerCase()}`,
|
|
defaultMessage: `${mode}`,
|
|
});
|
|
};
|
|
|
|
export const getAdditionalMessages = (
|
|
leg,
|
|
nextLeg,
|
|
firstLeg,
|
|
time,
|
|
config,
|
|
messages,
|
|
intl,
|
|
legs,
|
|
) => {
|
|
// Todo: multiple fares?
|
|
const fare = getFaresFromLegs([nextLeg], config)?.find(f => !f.isUnknown);
|
|
const isTicketSaleActive =
|
|
!config.hideNaviTickets && shouldShowFareInfo(config, legs) && fare;
|
|
|
|
const msgs = [];
|
|
const closed = messages.get('ticket')?.closed;
|
|
if (
|
|
!closed &&
|
|
leg === firstLeg &&
|
|
legTime(leg.end) - time < DISPLAY_MESSAGE_THRESHOLD &&
|
|
isTicketSaleActive
|
|
) {
|
|
// Todo: multiple fares?
|
|
const fares = getFaresFromLegs([nextLeg], config);
|
|
|
|
if (fares?.length && !fares[0].isUnknown) {
|
|
const title = intl.formatMessage({ id: 'navigation-remember-ticket' });
|
|
const body = `${fares[0].ticketName} ${formatFare(fares[0])}`;
|
|
|
|
msgs.push({
|
|
severity: 'INFO',
|
|
id: 'ticket',
|
|
title,
|
|
body,
|
|
});
|
|
}
|
|
}
|
|
return msgs;
|
|
};
|
|
|
|
export const getTransitLegState = (leg, intl, messages, time, settings) => {
|
|
const { start, realtimeState, from, mode, legId, route } = leg;
|
|
const { scheduledTime, estimated } = start;
|
|
if (messages.get(legId)?.closed) {
|
|
return [];
|
|
}
|
|
const slack = settings.minTransferTime * 1000;
|
|
const notInSchedule = estimated?.delay > slack || estimated?.delay < -slack;
|
|
const localizedMode = getLocalizedMode(mode, intl);
|
|
let content;
|
|
let title;
|
|
let body = '';
|
|
let severity;
|
|
const isRealTime = realtimeState === 'UPDATED';
|
|
const shortName = route.shortName || '';
|
|
|
|
if (notInSchedule) {
|
|
const lMode = getLocalizedMode(mode, intl);
|
|
const routeName = `${lMode} ${shortName}`;
|
|
const { delay } = estimated;
|
|
|
|
const translationId = `navigation-mode-${delay > 0 ? 'late' : 'early'}`;
|
|
title = intl.formatMessage({ id: translationId }, { name: routeName });
|
|
severity = 'WARNING';
|
|
} else if (!isRealTime) {
|
|
const departure = leg.trip.stoptimesForDate[0];
|
|
const departed =
|
|
1000 * (departure.serviceDay + departure.scheduledDeparture);
|
|
if (time + DISPLAY_MESSAGE_THRESHOLD < departed) {
|
|
// vehicle just departed, maybe no realtime yet
|
|
severity = 'INFO';
|
|
} else {
|
|
severity = 'WARNING';
|
|
}
|
|
title = intl.formatMessage({ id: 'navileg-mode-schedule' });
|
|
body = intl.formatMessage(
|
|
{ id: 'navileg-start-schedule' },
|
|
{
|
|
route: shortName,
|
|
time: timeStr(scheduledTime),
|
|
mode: localizedMode,
|
|
},
|
|
);
|
|
} else {
|
|
const { parentStation, name } = from.stop;
|
|
|
|
const fromId = // eslint-disable-next-line no-nested-ternary
|
|
mode === 'FERRY'
|
|
? 'from-ferrypier'
|
|
: parentStation
|
|
? 'from-station'
|
|
: 'from-stop';
|
|
const stopOrStation = intl.formatMessage({ id: fromId });
|
|
title = intl.formatMessage(
|
|
{ id: 'navileg-mode-realtime' },
|
|
{ route: shortName, mode: localizedMode },
|
|
);
|
|
body = intl.formatMessage(
|
|
{ id: 'navileg-start-realtime' },
|
|
{
|
|
time: timeStr(estimated.time),
|
|
stopOrStation,
|
|
stopName: name,
|
|
},
|
|
);
|
|
severity = 'INFO';
|
|
}
|
|
|
|
return [
|
|
{
|
|
severity,
|
|
content,
|
|
id: legId,
|
|
expiresOn: legTime(start),
|
|
title,
|
|
body,
|
|
},
|
|
];
|
|
};
|
|
|
|
export function itinerarySearchPath(time, leg, nextLeg, position, to) {
|
|
let from;
|
|
if (leg?.transitLeg) {
|
|
from = leg.intermediatePlaces.find(
|
|
p => legTime(p.arrival) > time + EARLIEST_NEXT_STOP,
|
|
);
|
|
if (!from) {
|
|
from = leg.to;
|
|
}
|
|
} else {
|
|
from = position || leg?.to || nextLeg?.from;
|
|
}
|
|
const location = { ...from, ...from.stop };
|
|
|
|
return getItineraryPagePath(locationToUri(location), to);
|
|
}
|
|
|
|
function withNewSearchBtn(children, searchCallback, alertType) {
|
|
addAnalyticsEvent({
|
|
category: 'Itinerary',
|
|
event: 'navigator',
|
|
action: 'notification_alert',
|
|
});
|
|
|
|
const handleClick = callback => () => {
|
|
addAnalyticsEvent({
|
|
category: 'Itinerary',
|
|
event: 'navigator',
|
|
action: 'notification_alert_click',
|
|
type: alertType,
|
|
});
|
|
callback();
|
|
};
|
|
|
|
return (
|
|
<div className="navi-info-content">
|
|
{children}
|
|
<FormattedMessage id="navigation-abort-trip" />
|
|
<button
|
|
className="new-itinerary-search"
|
|
type="button"
|
|
onClick={handleClick(searchCallback)}
|
|
>
|
|
<span className="notification-header">
|
|
<FormattedMessage id="settings-dropdown-open-label" />
|
|
</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Transfer(route1, route2, config) {
|
|
const mode1 = getRouteMode(route1, config);
|
|
const mode2 = getRouteMode(route2, config);
|
|
|
|
return (
|
|
<span className="navi-transfer-container">
|
|
<div className="navi-transfer">
|
|
<RouteNumberContainer
|
|
className={cx('line', mode1)}
|
|
route={route1}
|
|
mode={mode1}
|
|
isTransitLeg
|
|
vertical
|
|
withBar
|
|
/>
|
|
|
|
<div className="arrow-center">
|
|
<Icon img="icon-icon_arrow-right" omitViewBox />
|
|
</div>
|
|
|
|
<RouteNumberContainer
|
|
className={cx('line', mode2)}
|
|
route={route2}
|
|
mode={mode2}
|
|
isTransitLeg
|
|
vertical
|
|
withBar
|
|
/>
|
|
</div>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function TransferText(route1, route2, config, intl) {
|
|
const from = `${getLocalizedMode(getRouteMode(route1, config), intl)} ${
|
|
route1.shortName || ''
|
|
}`;
|
|
const to = `${getLocalizedMode(getRouteMode(route2, config), intl)} ${
|
|
route2.shortName || ''
|
|
}`;
|
|
return `${from} -> ${to}`;
|
|
}
|
|
|
|
export const getItineraryAlerts = (
|
|
legs,
|
|
time,
|
|
position,
|
|
tailLength,
|
|
intl,
|
|
messages,
|
|
itinerarySearchCallback,
|
|
config,
|
|
settings,
|
|
) => {
|
|
const alerts = [];
|
|
const slack = settings.minTransferTime * 1000;
|
|
legs.forEach(leg => {
|
|
if (leg.transitLeg && legTime(leg.end) > time) {
|
|
const id = `alert-${leg.legId}`; // allow only one alert per leg
|
|
if (!messages.get(id)?.closed) {
|
|
const alert = leg.alerts.find(al => {
|
|
return (
|
|
// show only alerts that are active during the leg
|
|
legTime(leg.end) / 1000 > al.effectiveStartDate &&
|
|
legTime(leg.start) / 1000 < al.effectiveEndDate &&
|
|
NOTED_SEVERITY.includes(al.alertSeverityLevel)
|
|
);
|
|
});
|
|
if (alert) {
|
|
alerts.push({
|
|
severity: 'ALERT',
|
|
id,
|
|
title: alert.alertHeaderText,
|
|
body: '',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const canceled = legs.filter(
|
|
leg => leg.realtimeState === 'CANCELED' && legTime(leg.start) > time,
|
|
);
|
|
|
|
if (canceled.length) {
|
|
// show new itinerary search button only for first canceled leg
|
|
canceled.forEach((leg, i) => {
|
|
const { legId, mode, route } = leg;
|
|
const id = `canceled-${legId}`;
|
|
if (!messages.get(id)) {
|
|
const lMode = getLocalizedMode(mode, intl);
|
|
const routeName = `${lMode} ${route.shortName}`;
|
|
const title = intl.formatMessage(
|
|
{ id: 'navigation-mode-canceled' },
|
|
{ name: routeName },
|
|
);
|
|
const jsxBody =
|
|
i === 0
|
|
? withNewSearchBtn(
|
|
'',
|
|
itinerarySearchCallback,
|
|
`canceled_${route.shortName}${mode.toLowerCase()}`,
|
|
)
|
|
: undefined;
|
|
alerts.push({
|
|
severity: 'ALERT',
|
|
id,
|
|
hideClose: true,
|
|
expiresOn: alert.effectiveEndDate * 1000,
|
|
title,
|
|
body: '',
|
|
jsxBody,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
const transfers = findTransferProblems(
|
|
legs,
|
|
time,
|
|
position,
|
|
tailLength,
|
|
slack,
|
|
);
|
|
if (transfers.length) {
|
|
let title;
|
|
let body;
|
|
let jsxBody;
|
|
const prob =
|
|
transfers.find(p => p.severity === 'ALERT') ||
|
|
transfers.find(p => p.severity === 'WARNING');
|
|
if (prob) {
|
|
const id = transferId(prob);
|
|
const alert = messages.get(id);
|
|
if (!alert?.closed || alert?.severity !== prob.severity) {
|
|
const transfer = Transfer(
|
|
prob.fromLeg.route,
|
|
prob.toLeg.route,
|
|
config,
|
|
);
|
|
const desc = TransferText(
|
|
prob.fromLeg.route,
|
|
prob.toLeg.route,
|
|
config,
|
|
intl,
|
|
);
|
|
|
|
if (prob.severity === 'ALERT') {
|
|
title = intl.formatMessage({ id: 'navigation-transfer-problem' });
|
|
body = intl.formatMessage(
|
|
{ id: 'navigation-transfer-problem-details' },
|
|
{ transfer: desc },
|
|
);
|
|
jsxBody = withNewSearchBtn(
|
|
<FormattedMessage
|
|
id="navigation-transfer-problem-details"
|
|
values={{ transfer }}
|
|
/>,
|
|
itinerarySearchCallback,
|
|
`transfer-${
|
|
prob.fromLeg.route.shortName
|
|
}${prob.fromLeg.mode.toLowerCase()}-${
|
|
prob.toLeg.route.shortName
|
|
}${prob.toLeg.mode.toLowerCase()}`,
|
|
);
|
|
} else {
|
|
title = intl.formatMessage({ id: 'navigation-hurry-transfer' });
|
|
const change = Math.floor(
|
|
(prob.duration - prob.originalDuration) / 60000,
|
|
);
|
|
|
|
body = intl.formatMessage(
|
|
{ id: 'navigation-hurry-transfer-value' },
|
|
{ transfer: desc, time: durationToString(prob.duration), change },
|
|
);
|
|
jsxBody = (
|
|
<FormattedMessage
|
|
id="navigation-hurry-transfer-value"
|
|
values={{
|
|
transfer,
|
|
time: <Duration duration={prob.duration} />,
|
|
change,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
alerts.push({
|
|
severity: prob.severity,
|
|
id,
|
|
hideClose: prob.severity === 'ALERT',
|
|
expiresOn: legTime(prob.toLeg.start),
|
|
title,
|
|
body,
|
|
jsxBody,
|
|
});
|
|
}
|
|
}
|
|
// show notification when problem gets solved
|
|
transfers.forEach(tr => {
|
|
if (tr.severity === 'INFO' && tr.slack > 1.1 * slack) {
|
|
const id = transferId(tr);
|
|
const alert = messages.get(id);
|
|
if (alert && alert.severity !== 'INFO') {
|
|
title = intl.formatMessage({
|
|
id: 'navigation-hurry-transfer-solved',
|
|
});
|
|
body = intl.formatMessage(
|
|
{ id: 'navigation-hurry-transfer-solved-details' },
|
|
{
|
|
transfer: TransferText(
|
|
tr.fromLeg.route,
|
|
tr.toLeg.route,
|
|
config,
|
|
intl,
|
|
),
|
|
time: durationToString(tr.duration),
|
|
},
|
|
);
|
|
jsxBody = (
|
|
<FormattedMessage
|
|
id="navigation-hurry-transfer-solved-details"
|
|
values={{
|
|
transfer: Transfer(tr.fromLeg.route, tr.toLeg.route, config),
|
|
time: <Duration duration={tr.duration} />,
|
|
}}
|
|
/>
|
|
);
|
|
|
|
// a warning/alert has been shown
|
|
alerts.push({
|
|
severity: 'INFO',
|
|
id,
|
|
expiresOn: legTime(tr.toLeg.start),
|
|
title,
|
|
body,
|
|
jsxBody,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return alerts;
|
|
};
|
|
|
|
/*
|
|
* Get the properties of the destination based on the leg.
|
|
*
|
|
*/
|
|
export const getDestinationProperties = (
|
|
rentalVehicle,
|
|
vehicleParking,
|
|
vehicleRentalStation,
|
|
stop,
|
|
config,
|
|
) => {
|
|
const { routes, vehicleMode } = stop;
|
|
let destination = {};
|
|
let mode = vehicleMode;
|
|
if (routes && vehicleMode === 'BUS' && config.useExtendedRouteTypes) {
|
|
if (routes.some(p => p.type === ExtendedRouteTypes.BusExpress)) {
|
|
mode = 'bus-express';
|
|
}
|
|
} else if (routes && vehicleMode === 'TRAM' && config.useExtendedRouteTypes) {
|
|
if (routes.some(p => p.type === ExtendedRouteTypes.SpeedTram)) {
|
|
mode = 'speedtram';
|
|
}
|
|
} else if (routes && vehicleMode === 'FERRY') {
|
|
if (routes.some(p => isExternalFeed(getFeedWithoutId(p.gtfsId), config))) {
|
|
mode = 'ferry-external';
|
|
}
|
|
}
|
|
// todo: scooter and citybike icons etc.
|
|
if (rentalVehicle) {
|
|
destination.name = rentalVehicle.rentalNetwork.networkId;
|
|
} else if (vehicleParking) {
|
|
destination.name = vehicleParking.name;
|
|
} else if (vehicleRentalStation) {
|
|
destination.name = vehicleRentalStation.name;
|
|
} else {
|
|
let iconProps = {};
|
|
switch (mode) {
|
|
case 'TRAM,BUS':
|
|
iconProps = {
|
|
iconId: 'icon-icon_bustram-stop-lollipop',
|
|
className: 'tram-stop',
|
|
};
|
|
break;
|
|
case 'SUBWAY':
|
|
iconProps = {
|
|
iconId: 'icon-icon_subway',
|
|
className: 'subway-stop',
|
|
};
|
|
break;
|
|
case 'RAIL':
|
|
iconProps = {
|
|
iconId: 'icon-icon_rail-stop-lollipop',
|
|
className: 'rail-stop',
|
|
};
|
|
|
|
break;
|
|
case 'FERRY':
|
|
iconProps = {
|
|
iconId: 'icon-icon_ferry',
|
|
className: 'ferry-stop',
|
|
};
|
|
break;
|
|
case 'ferry-external':
|
|
iconProps = {
|
|
iconId: 'icon-icon_ferry-external',
|
|
className: 'ferry-external-stop',
|
|
};
|
|
break;
|
|
case 'bus-express':
|
|
iconProps = {
|
|
iconId: 'icon-icon_bus-stop-express-lollipop',
|
|
className: 'bus-stop',
|
|
};
|
|
break;
|
|
case 'speedtram':
|
|
iconProps = {
|
|
iconId: 'icon-icon_speedtram-stop-lollipop',
|
|
className: 'speedtram-stop',
|
|
};
|
|
break;
|
|
default:
|
|
iconProps = {
|
|
iconId: `icon-icon_${mode.toLowerCase()}-stop-lollipop`,
|
|
};
|
|
}
|
|
destination = {
|
|
...iconProps,
|
|
iconColor: getModeIconColor(config, mode),
|
|
name: stop.name,
|
|
};
|
|
}
|
|
|
|
return destination;
|
|
};
|
|
|
|
export const withRealTime = (rt, children) => (
|
|
<span className={cx({ bold: rt, realtime: rt })}>{children}</span>
|
|
);
|