mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-08-02 06:51:13 +02:00
525 lines
15 KiB
JavaScript
525 lines
15 KiB
JavaScript
import isNumber from 'lodash/isNumber';
|
|
import routeNameCompare from '@digitransit-search-util/digitransit-search-util-route-name-compare';
|
|
|
|
import {
|
|
RealtimeStateType,
|
|
AlertSeverityLevelType,
|
|
AlertEntityType,
|
|
} from '../constants';
|
|
|
|
import { legTime } from './legUtils';
|
|
|
|
/**
|
|
* Checks if the stoptime has a cancelation.
|
|
*
|
|
* @param {*} stoptime the stoptime object to check.
|
|
*/
|
|
export const stoptimeHasCancelation = stoptime => {
|
|
if (!stoptime) {
|
|
return false;
|
|
}
|
|
return stoptime.realtimeState === RealtimeStateType.Canceled;
|
|
};
|
|
|
|
/**
|
|
* Checks if the trip has a cancelation for the given stop.
|
|
*
|
|
* @param {*} trip the trip object to check.
|
|
* @param {*} stop the stop object to look a cancelation for.
|
|
*/
|
|
export const tripHasCancelationForStop = (trip, stop) => {
|
|
if (
|
|
!trip ||
|
|
(!Array.isArray(trip.stoptimes) && !Array.isArray(trip.stoptimesForDate)) ||
|
|
!stop ||
|
|
!stop.gtfsId
|
|
) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(trip.stoptimesForDate)) {
|
|
return trip.stoptimesForDate
|
|
.filter(stoptimeHasCancelation)
|
|
.some(st => st.stop && st.stop.gtfsId === stop.gtfsId);
|
|
}
|
|
return trip.stoptimes
|
|
.filter(stoptimeHasCancelation)
|
|
.some(st => st.stop && st.stop.gtfsId === stop.gtfsId);
|
|
};
|
|
|
|
/**
|
|
* Checks if the trip has a cancelation.
|
|
*
|
|
* @param {*} trip the trip object to check.
|
|
* @param {number} currentTime the current unix time.
|
|
* @param { before: number, after: number } validityPeriod validity before/after current time.
|
|
*/
|
|
export const tripHasCancelation = (trip, currentTime, validityPeriod) => {
|
|
if (
|
|
!trip ||
|
|
(!Array.isArray(trip.stoptimes) && !Array.isArray(trip.stoptimesForDate))
|
|
) {
|
|
return false;
|
|
}
|
|
const first = trip.stoptimes[0];
|
|
const departureTime = first.serviceDay + first.scheduledDeparture;
|
|
const last = trip.stoptimes[trip.stoptimes.length - 1];
|
|
if (
|
|
validityPeriod &&
|
|
currentTime &&
|
|
(currentTime < departureTime - validityPeriod.before ||
|
|
currentTime >
|
|
last.serviceDay + last.scheduledArrival + validityPeriod.after)
|
|
) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(trip.stoptimesForDate)) {
|
|
return trip.stoptimesForDate.every(stoptimeHasCancelation);
|
|
}
|
|
return trip.stoptimes.every(stoptimeHasCancelation);
|
|
};
|
|
/**
|
|
* Checks if the leg has a cancelation.
|
|
*
|
|
* @param {*} leg the leg object to check.
|
|
*/
|
|
export const legHasCancelation = leg => {
|
|
if (!leg) {
|
|
return false;
|
|
}
|
|
return leg.realtimeState === RealtimeStateType.Canceled;
|
|
};
|
|
|
|
/**
|
|
* The default validity period (5 minutes) for an alert without a set end time.
|
|
*/
|
|
export const DEFAULT_VALIDITY = 5 * 60;
|
|
|
|
/**
|
|
* Checks if the given validity period is valid or not.
|
|
*
|
|
* @param {{ effectiveStartDate: number, effectiveEndDate: number }} alert containing validity period.
|
|
* @param {number} referenceUnixTime the reference unix time stamp (in seconds).
|
|
* @param {number} defaultValidity the default validity period length in seconds.
|
|
*/
|
|
export const isAlertValid = (
|
|
alert,
|
|
referenceUnixTime,
|
|
{ defaultValidity = DEFAULT_VALIDITY, isFutureValid = false } = {},
|
|
) => {
|
|
if (!alert) {
|
|
return false;
|
|
}
|
|
const { effectiveStartDate, effectiveEndDate } = alert;
|
|
if (!effectiveStartDate || !isNumber(referenceUnixTime)) {
|
|
return true;
|
|
}
|
|
if (isFutureValid && referenceUnixTime < effectiveStartDate) {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
effectiveStartDate <= referenceUnixTime &&
|
|
referenceUnixTime <=
|
|
(effectiveEndDate || effectiveStartDate + defaultValidity)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Checks if the given (canceled) stoptime has expired or not.
|
|
*
|
|
* @param {*} referenceUnixTime the reference unix time stamp (in seconds).
|
|
* @param {*} stoptime the stoptime to check.
|
|
*/
|
|
export const cancelationHasExpired = (
|
|
referenceUnixTime,
|
|
{ scheduledArrival, scheduledDeparture, serviceDay } = {},
|
|
) =>
|
|
!isAlertValid(
|
|
{
|
|
effectiveStartDate: serviceDay + scheduledArrival,
|
|
effectiveEndDate: serviceDay + scheduledDeparture,
|
|
},
|
|
referenceUnixTime,
|
|
{ isFutureValid: true },
|
|
);
|
|
|
|
/**
|
|
* Retrieves canceled stoptimes for the given route.
|
|
*
|
|
* @param {*} route the route to get cancelations for.
|
|
* @param {*} patternId the pattern's id, optional.
|
|
* @param {number} currentTime the current unix time.
|
|
* @param { before: number, after: number } validityPeriod validity before/after current time.
|
|
*/
|
|
export const getCancelationsForRoute = (
|
|
route,
|
|
patternId,
|
|
currentTime,
|
|
validityPeriod,
|
|
) => {
|
|
if (!route || !Array.isArray(route.patterns)) {
|
|
return [];
|
|
}
|
|
return route.patterns
|
|
.filter(pattern => (patternId ? pattern.code === patternId : true))
|
|
.map(pattern =>
|
|
Array.isArray(pattern.trips)
|
|
? pattern.trips.filter(trip =>
|
|
tripHasCancelation(trip, currentTime, validityPeriod),
|
|
)
|
|
: [],
|
|
)
|
|
.reduce((a, b) => a.concat(b), [])
|
|
.map(trip => trip.stoptimes || [])
|
|
.reduce((a, b) => a.concat(b), []);
|
|
};
|
|
|
|
/**
|
|
* Retrieves canceled stoptimes for the given stop.
|
|
*
|
|
* @param {*} stop the stop to get cancelations for.
|
|
*/
|
|
export const getCancelationsForStop = stop => {
|
|
if (!stop || !Array.isArray(stop.stoptimes)) {
|
|
return [];
|
|
}
|
|
return stop.stoptimes.filter(stoptimeHasCancelation);
|
|
};
|
|
|
|
/**
|
|
* Retrieves Service Alerts from the given object (or an empty list if no alerts exist).
|
|
*
|
|
* @param {Object.<string,*>} object should contain alerts key.
|
|
*/
|
|
export const getAlertsForObject = object => {
|
|
if (!object || !Array.isArray(object.alerts)) {
|
|
return [];
|
|
}
|
|
|
|
return object.alerts;
|
|
};
|
|
|
|
/**
|
|
* Retrieves Service Alerts from the given station and its stops
|
|
* or an empty array.
|
|
*
|
|
* @param {string} station the stop object to retrieve alerts from.
|
|
*/
|
|
export const getServiceAlertsForStation = station => {
|
|
const stopAlerts = station.stops
|
|
? station.stops.flatMap(stop => getAlertsForObject(stop))
|
|
: [];
|
|
return [...getAlertsForObject(station), ...stopAlerts];
|
|
};
|
|
|
|
const isValidArray = array => Array.isArray(array) && array.length > 0;
|
|
|
|
/**
|
|
* Iterates through the alerts and returns the highest severity level found.
|
|
* Order of severity (in descending order): Severe, Warning, Info, Unknown.
|
|
* Returns undefined if the severity level cannot be determined.
|
|
*
|
|
* @param {*} alerts the alerts to check.
|
|
*/
|
|
export const getMaximumAlertSeverityLevel = alerts => {
|
|
if (!isValidArray(alerts)) {
|
|
return undefined;
|
|
}
|
|
const levels = alerts
|
|
.map(alert => alert.alertSeverityLevel)
|
|
.reduce((obj, level) => {
|
|
if (level) {
|
|
obj[level] = level; // eslint-disable-line no-param-reassign
|
|
}
|
|
return obj;
|
|
}, {});
|
|
return (
|
|
levels[AlertSeverityLevelType.Severe] ||
|
|
levels[AlertSeverityLevelType.Warning] ||
|
|
levels[AlertSeverityLevelType.Info] ||
|
|
levels[AlertSeverityLevelType.Unknown] ||
|
|
undefined
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Checks if any of the alerts is active at the given time and
|
|
* returns its severity level.
|
|
*
|
|
* @param {*} alerts the alerts to check.
|
|
* @param {*} referenceUnixTime the reference unix time stamp (in seconds).
|
|
*/
|
|
export const getActiveAlertSeverityLevel = (alerts, referenceUnixTime) => {
|
|
if (!isValidArray(alerts)) {
|
|
return undefined;
|
|
}
|
|
return getMaximumAlertSeverityLevel(
|
|
alerts
|
|
.filter(alert => !!alert)
|
|
.filter(alert => isAlertValid(alert, referenceUnixTime)),
|
|
);
|
|
};
|
|
|
|
export const isAlertActive = (referenceUnixTime, alert) => {
|
|
if (!alert) {
|
|
return false;
|
|
}
|
|
return (
|
|
isAlertValid(alert, referenceUnixTime) &&
|
|
alert.alertSeverityLevel !== AlertSeverityLevelType.Info
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Checks if any of the given cancelations or alerts are active at the given time.
|
|
*
|
|
* @param {*} referenceUnixTime the reference unix time stamp (in seconds).
|
|
* @param {*} cancelations the cancelations to check.
|
|
* @param {*} alerts the alerts to check.
|
|
*/
|
|
export const checkActiveDisruptions = (
|
|
referenceUnixTime,
|
|
cancelations = [],
|
|
alerts = [],
|
|
) => {
|
|
if (
|
|
cancelations.some(
|
|
cancelation => !cancelationHasExpired(referenceUnixTime, cancelation),
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return !!alerts.find(alert => isAlertActive(referenceUnixTime, alert));
|
|
};
|
|
|
|
/**
|
|
* Checks whether the given leg has an active cancelation or an active
|
|
* service alert.
|
|
*
|
|
* @param {Object.<string.*>} leg the itinerary leg to check.
|
|
*/
|
|
export const getActiveLegAlertSeverityLevel = leg => {
|
|
if (!leg) {
|
|
return undefined;
|
|
}
|
|
|
|
if (legHasCancelation(leg)) {
|
|
return AlertSeverityLevelType.Warning;
|
|
}
|
|
|
|
const { route } = leg;
|
|
|
|
const serviceAlerts = [
|
|
...getAlertsForObject(route),
|
|
...getAlertsForObject(leg?.from?.stop),
|
|
...getAlertsForObject(leg?.to?.stop),
|
|
];
|
|
|
|
return getActiveAlertSeverityLevel(
|
|
serviceAlerts,
|
|
legTime(leg.start) / 1000, // this field is in ms format
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Compares the given alert entities in order to sort them based route shortName
|
|
* or the stop name (and code).
|
|
*
|
|
* @param {*} entityA the first entity to compare.
|
|
* @param {*} entityB the second entity to compare.
|
|
*/
|
|
export const entityCompare = (entityA, entityB) => {
|
|
if (entityA.shortName) {
|
|
return routeNameCompare(entityA, entityB);
|
|
}
|
|
const nameCompare = `${entityA.name}`.localeCompare(entityB.name);
|
|
if (nameCompare !== 0) {
|
|
return nameCompare;
|
|
}
|
|
if (entityA.code && entityB.code) {
|
|
return `${entityA.code}`.localeCompare(entityB.code);
|
|
}
|
|
return nameCompare;
|
|
};
|
|
|
|
/**
|
|
* Compares the given alerts in order to sort them based on severity level.
|
|
* The most severe alerts are sorted first.
|
|
*
|
|
* @param {*} a the first alert to compare.
|
|
* @param {*} b the second alert to compare.
|
|
*/
|
|
export const alertSeverityCompare = (a, b) => {
|
|
const severityLevels = [
|
|
AlertSeverityLevelType.Info,
|
|
AlertSeverityLevelType.Unknown,
|
|
AlertSeverityLevelType.Warning,
|
|
AlertSeverityLevelType.Severe,
|
|
];
|
|
|
|
const severityLevelDifference =
|
|
severityLevels.indexOf(b.alertSeverityLevel) -
|
|
severityLevels.indexOf(a.alertSeverityLevel);
|
|
return severityLevelDifference;
|
|
};
|
|
|
|
/**
|
|
* Compares the given alerts in order to sort them based on the severity,
|
|
* entities and the validity period (in that order).
|
|
*
|
|
* @param {*} alertA the first alert to compare.
|
|
* @param {*} alertB the second alert to compare.
|
|
*/
|
|
export const alertCompare = (alertA, alertB) => {
|
|
const severityScore = alertSeverityCompare(alertA, alertB);
|
|
if (severityScore !== 0) {
|
|
return severityScore;
|
|
}
|
|
|
|
if (!alertA.entities && alertA.entities.length > 0) {
|
|
return 1;
|
|
}
|
|
if (!alertB.entities && alertB.entities.length > 0) {
|
|
return -1;
|
|
}
|
|
const aEntitiesSorted = [...alertA.entities].sort(entityCompare);
|
|
const bEntitiesSorted = [...alertB.entities].sort(entityCompare);
|
|
const bestEntitiesCompared = entityCompare(
|
|
aEntitiesSorted[0],
|
|
bEntitiesSorted[0],
|
|
);
|
|
return bestEntitiesCompared === 0
|
|
? alertA.effectiveStartDate - alertB.effectiveStartDate
|
|
: bestEntitiesCompared;
|
|
};
|
|
|
|
/**
|
|
* Checks whether the alert to be displayed has meaningful data (header or description) or not.
|
|
* Used to decide whether to render the box for the alert or not.
|
|
* @param {*} alerts list of alerts.
|
|
*/
|
|
export const hasMeaningfulData = alerts => {
|
|
if (!isValidArray(alerts)) {
|
|
return false;
|
|
}
|
|
const alertForDisplaying = [...alerts].sort(alertSeverityCompare)[0];
|
|
const header = alertForDisplaying.alertHeaderText;
|
|
const description = alertForDisplaying.alertDescriptionText;
|
|
if (
|
|
(header && header.length > 0) ||
|
|
(description && description.length > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
export const mapAlertSource = (config, lang, feedName) => {
|
|
if (config.sourceForAlertsAndDisruptions?.[feedName]) {
|
|
return config.sourceForAlertsAndDisruptions[feedName][lang].concat(': ');
|
|
}
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Returns entities of only the given entity type.
|
|
*
|
|
* @param {*} entities the entities to filter.
|
|
* @param {String} entityType the entity type.
|
|
*/
|
|
export const getEntitiesOfType = (entities, entityType) =>
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
entities?.filter(entity => entity.__typename === entityType);
|
|
|
|
/**
|
|
* Returns entities of only the given entity type from an alert.
|
|
*
|
|
* @param {*} alert the alert which can contain entities.
|
|
* @param {String} entityType the entity type.
|
|
*/
|
|
export const getEntitiesOfTypeFromAlert = (alert, entityType) =>
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
alert?.entities?.filter(entity => entity.__typename === entityType);
|
|
|
|
/**
|
|
* Checks if the alert has at least one entity of the given entity type.
|
|
*
|
|
* @param {*} alert the alert which can contain entities.
|
|
* @param {String} entityType the entity type.
|
|
*/
|
|
export const hasEntitiesOfType = (alert, entityType) =>
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
alert?.entities?.some(entity => entity.__typename === entityType);
|
|
|
|
/**
|
|
* Checks if the alert has at least one entity of the given entity type.
|
|
*
|
|
* @param {*} alert the alert which can contain entities.
|
|
* @param {Array.<String>} entityTypes the entity type of which at least one
|
|
* should exist in the alert.
|
|
*/
|
|
export const hasEntitiesOfTypes = (alert, entityTypes) =>
|
|
alert?.entities?.some(entity =>
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
entityTypes.includes(entity.__typename),
|
|
);
|
|
|
|
/**
|
|
* Sets the given entity to be the only entity on an alert.
|
|
*
|
|
* @param {*} alert the alert which can already contain entities which are removed.
|
|
* @param {*} entity the entity that will be the only entity on the returned alert.
|
|
*/
|
|
export const setEntityForAlert = (alert, entity) => {
|
|
return { ...alert, entities: [entity] };
|
|
};
|
|
|
|
/**
|
|
* Returns an array of currently active alerts for the legs' route and origin/destination stops
|
|
*
|
|
* @param {*} leg the itinerary leg to check.
|
|
* @param {*} legStartTime the reference unix time stamp (in seconds).
|
|
*/
|
|
export const getActiveLegAlerts = (leg, legStartTime) => {
|
|
if (!leg) {
|
|
return undefined;
|
|
}
|
|
|
|
const { route } = leg;
|
|
|
|
const serviceAlerts = [
|
|
...getAlertsForObject(route).map(alert => {
|
|
return {
|
|
...alert,
|
|
entities: getEntitiesOfTypeFromAlert(
|
|
alert,
|
|
AlertEntityType.Route,
|
|
entity => entity.gtfsId === route.gtfsId,
|
|
),
|
|
};
|
|
}),
|
|
...getAlertsForObject(leg?.from.stop).map(alert => {
|
|
return {
|
|
...alert,
|
|
entities: getEntitiesOfTypeFromAlert(
|
|
alert,
|
|
AlertEntityType.Stop,
|
|
entity => entity.gtfsId === leg?.from.stop.gtfsId,
|
|
),
|
|
};
|
|
}),
|
|
...getAlertsForObject(leg?.to.stop).map(alert => {
|
|
return {
|
|
...alert,
|
|
entities: getEntitiesOfTypeFromAlert(
|
|
alert,
|
|
AlertEntityType.Stop,
|
|
entity => entity.gtfsId === leg?.to.stop.gtfsId,
|
|
),
|
|
};
|
|
}),
|
|
].filter(alert => isAlertActive(legStartTime, alert));
|
|
|
|
return serviceAlerts;
|
|
};
|