mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-07-05 16:30:37 +02:00
490 lines
14 KiB
JavaScript
490 lines
14 KiB
JavaScript
import { DateTime } from 'luxon';
|
|
import isEqual from 'lodash/isEqual';
|
|
import {
|
|
getTransitModes,
|
|
isTransportModeAvailable,
|
|
networkIsActive,
|
|
} from './modeUtils';
|
|
import { otpToLocation, getIntermediatePlaces } from './otpStrings';
|
|
import { getAllNetworksOfType, getDefaultNetworks } from './vehicleRentalUtils';
|
|
import { getCustomizedSettings } from '../store/localStorage';
|
|
import { estimateItineraryDistance } from './geo-utils';
|
|
import { TransportMode } from '../constants';
|
|
|
|
export const PLANTYPE = {
|
|
WALK: 'WALK',
|
|
BIKE: 'BICYCLE',
|
|
CAR: 'CAR',
|
|
CARTRANSIT: 'CARTRANSIT',
|
|
TRANSIT: 'TRANSIT',
|
|
BIKEPARK: 'BIKEPARK',
|
|
BIKETRANSIT: 'BIKETRANSIT',
|
|
PARKANDRIDE: 'PARKANDRIDE',
|
|
SCOOTERTRANSIT: 'SCOOTERTRANSIT',
|
|
FLEXTRANSIT: 'FLEXTRANSIT',
|
|
};
|
|
|
|
const directModes = [PLANTYPE.WALK, PLANTYPE.BIKE, PLANTYPE.CAR];
|
|
const SHORT_TRIP_METERS = 2000;
|
|
|
|
/**
|
|
* Find an option nearest to the value
|
|
*
|
|
* @param value a number
|
|
* @param options array of numbers
|
|
* @returns on option from options that is closest to the provided value
|
|
*/
|
|
export function findNearestOption(value, options) {
|
|
let currNearest = options[0];
|
|
let diff = Math.abs(value - currNearest);
|
|
for (let i = 0; i < options.length; i++) {
|
|
const newdiff = Math.abs(value - options[i]);
|
|
if (newdiff < diff) {
|
|
diff = newdiff;
|
|
currNearest = options[i];
|
|
}
|
|
}
|
|
return currNearest;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the default settings from the configuration.
|
|
*
|
|
* @param {*} config UI configuration
|
|
*/
|
|
export function getDefaultSettings(config) {
|
|
if (!config) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
...config.defaultSettings,
|
|
modes: getTransitModes(config),
|
|
allowedBikeRentalNetworks: config.transportModes?.citybike?.defaultValue
|
|
? getDefaultNetworks(config)
|
|
: [],
|
|
scooterNetworks: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether user has customized any settings that are visible and make a difference.
|
|
* @param {*} config the configuration for the software installation
|
|
*/
|
|
export function hasCustomizedSettings(config) {
|
|
const defaultSettings = getDefaultSettings(config);
|
|
const customizedSettings = getCustomizedSettings();
|
|
if (Object.keys(customizedSettings).length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return Object.keys(customizedSettings).some(key => {
|
|
if (key === 'allowedBikeRentalNetworks') {
|
|
return customizedSettings.allowedBikeRentalNetworks.some(network =>
|
|
networkIsActive(config.vehicleRental.networks[network]),
|
|
);
|
|
}
|
|
if (
|
|
Array.isArray(customizedSettings[key]) &&
|
|
Array.isArray(defaultSettings[key])
|
|
) {
|
|
return customizedSettings[key].length !== defaultSettings[key].length;
|
|
}
|
|
return customizedSettings[key] !== defaultSettings[key];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves the current (customized) settings kept in local store
|
|
* Missing setting gets a default value
|
|
* @param {*} config the configuration for the software installation
|
|
*/
|
|
export function getSettings(config) {
|
|
const defaultSettings = getDefaultSettings(config);
|
|
const userSettings = getCustomizedSettings();
|
|
const allNetworks = getDefaultNetworks(config);
|
|
const allScooterNetworks = getAllNetworksOfType(
|
|
config,
|
|
TransportMode.Scooter,
|
|
);
|
|
|
|
const settings = {
|
|
...defaultSettings,
|
|
...userSettings,
|
|
modes: userSettings?.modes // filter modes to configured allowed values
|
|
? [
|
|
...userSettings.modes.filter(mode =>
|
|
isTransportModeAvailable(config, mode),
|
|
),
|
|
].sort()
|
|
: defaultSettings.modes,
|
|
// filter networks to configured allowed values
|
|
allowedBikeRentalNetworks:
|
|
userSettings.allowedBikeRentalNetworks?.length > 0
|
|
? userSettings.allowedBikeRentalNetworks.filter(network =>
|
|
allNetworks.includes(network),
|
|
)
|
|
: defaultSettings.allowedBikeRentalNetworks,
|
|
scooterNetworks:
|
|
userSettings.scooterNetworks?.length > 0
|
|
? userSettings.scooterNetworks.filter(network =>
|
|
allScooterNetworks.includes(network),
|
|
)
|
|
: defaultSettings.scooterNetworks,
|
|
};
|
|
const { defaultOptions } = config;
|
|
return {
|
|
...settings,
|
|
walkSpeed: findNearestOption(settings.walkSpeed, defaultOptions.walkSpeed),
|
|
bikeSpeed: findNearestOption(settings.bikeSpeed, defaultOptions.bikeSpeed),
|
|
};
|
|
}
|
|
|
|
function filterTransitModes(modes, planType, config) {
|
|
if (planType === PLANTYPE.BIKETRANSIT) {
|
|
if (config.bikeBoardingModes) {
|
|
return modes.filter(m => config.bikeBoardingModes[m]);
|
|
}
|
|
return [];
|
|
}
|
|
if (planType === PLANTYPE.CARTRANSIT) {
|
|
if (config.carBoardingModes) {
|
|
return modes.filter(m => config.carBoardingModes[m]);
|
|
}
|
|
return [];
|
|
}
|
|
return modes;
|
|
}
|
|
|
|
export function planQueryNeeded(
|
|
config,
|
|
{
|
|
params: { from, to },
|
|
location: {
|
|
query: { intermediatePlaces, arriveBy, time },
|
|
},
|
|
},
|
|
planType,
|
|
relaxSettings = false,
|
|
) {
|
|
if (!from || !to || from === '-' || to === '-') {
|
|
return false;
|
|
}
|
|
const fromLocation = otpToLocation(from);
|
|
const toLocation = otpToLocation(to);
|
|
const intermediateLocations = getIntermediatePlaces({
|
|
intermediatePlaces,
|
|
});
|
|
|
|
// not needed if origin is destination an no via points
|
|
if (isEqual(fromLocation, toLocation) && !intermediateLocations.length) {
|
|
return false;
|
|
}
|
|
|
|
const defaultSettings = getDefaultSettings(config);
|
|
const settings = getSettings(config);
|
|
const transitModes = filterTransitModes(
|
|
relaxSettings ? defaultSettings.modes : settings.modes,
|
|
planType,
|
|
config,
|
|
);
|
|
const wheelchair = !!settings.accessibilityOption;
|
|
const distance = estimateItineraryDistance(
|
|
fromLocation,
|
|
toLocation,
|
|
intermediateLocations,
|
|
);
|
|
|
|
switch (planType) {
|
|
case PLANTYPE.WALK:
|
|
return (
|
|
!wheelchair &&
|
|
distance < config.suggestWalkMaxDistance &&
|
|
!config.hideWalkOption
|
|
);
|
|
|
|
case PLANTYPE.BIKE:
|
|
return (
|
|
!wheelchair &&
|
|
distance < config.suggestBikeMaxDistance &&
|
|
settings.includeBikeSuggestions
|
|
);
|
|
|
|
case PLANTYPE.CAR:
|
|
return (
|
|
config.showCO2InItinerarySummary ||
|
|
(distance > config.suggestCarMinDistance &&
|
|
settings.includeCarSuggestions)
|
|
);
|
|
|
|
case PLANTYPE.BIKEPARK:
|
|
return (
|
|
transitModes.length > 0 &&
|
|
!wheelchair &&
|
|
config.showBikeAndParkItineraries &&
|
|
settings.showBikeAndParkItineraries
|
|
);
|
|
|
|
case PLANTYPE.BIKETRANSIT:
|
|
return (
|
|
transitModes.length > 0 &&
|
|
!wheelchair &&
|
|
settings.includeBikeSuggestions
|
|
);
|
|
|
|
case PLANTYPE.CARTRANSIT:
|
|
return transitModes.length > 0 && settings.includeCarSuggestions;
|
|
|
|
case PLANTYPE.SCOOTERTRANSIT:
|
|
/* special logic: relaxed scooter query is made only if no networks allowed */
|
|
/* special logic: scooter queries are not made if the search is n minutes or more into the future or if arrival time is too late */
|
|
return (
|
|
config.transportModes.scooter.availableForSelection &&
|
|
transitModes.length > 0 &&
|
|
!wheelchair &&
|
|
(relaxSettings
|
|
? settings.scooterNetworks.length === 0
|
|
: settings.scooterNetworks.length > 0) &&
|
|
((!arriveBy &&
|
|
(time - Date.now() / 1000) / 60 <
|
|
config.vehicleRental.maxMinutesToRentalJourneyStart) ||
|
|
(arriveBy &&
|
|
(time - Date.now() / 1000) / 60 <
|
|
config.vehicleRental.maxMinutesToRentalJourneyEnd))
|
|
);
|
|
case PLANTYPE.PARKANDRIDE:
|
|
return (
|
|
transitModes.length > 0 &&
|
|
distance > config.suggestCarMinDistance &&
|
|
settings.includeParkAndRideSuggestions
|
|
);
|
|
/* special logic: relaxed flex query is made only if taxis are not allowed */
|
|
case PLANTYPE.FLEXTRANSIT:
|
|
return (
|
|
config.experimental?.allowFlexJourneys &&
|
|
(transitModes.length > 0 ||
|
|
config.experimental?.allowDirectFlexJourneys) &&
|
|
settings.includeTaxiSuggestions !== relaxSettings
|
|
);
|
|
|
|
case PLANTYPE.TRANSIT:
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function getLocation(str) {
|
|
const loc = otpToLocation(str);
|
|
if (loc.gtfsId) {
|
|
return {
|
|
location: {
|
|
stopLocation: { stopLocationId: loc.gtfsId },
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
location: {
|
|
coordinate: {
|
|
latitude: loc.lat,
|
|
longitude: loc.lon,
|
|
},
|
|
},
|
|
label: loc.address,
|
|
};
|
|
}
|
|
|
|
export function getPlanParams(
|
|
config,
|
|
{
|
|
params: { from, to },
|
|
location: {
|
|
query: { arriveBy, time, intermediatePlaces },
|
|
},
|
|
},
|
|
planType,
|
|
relaxSettings = false,
|
|
) {
|
|
const fromPlace = getLocation(from);
|
|
const toPlace = getLocation(to);
|
|
const useLatestArrival = arriveBy === 'true';
|
|
// estimate distance for search iteration heuristics
|
|
const fromLocation = otpToLocation(from);
|
|
const toLocation = otpToLocation(to);
|
|
const intermediateLocations = getIntermediatePlaces({
|
|
intermediatePlaces,
|
|
});
|
|
const via = intermediateLocations.map(loc => ({
|
|
passThrough: {
|
|
stopLocationIds: [loc.gtfsId],
|
|
},
|
|
}));
|
|
const distance = estimateItineraryDistance(
|
|
fromLocation,
|
|
toLocation,
|
|
intermediateLocations,
|
|
);
|
|
const shortTrip = distance < SHORT_TRIP_METERS;
|
|
|
|
const defaultSettings = getDefaultSettings(config);
|
|
const settings = getSettings(config);
|
|
|
|
if (settings.allowedBikeRentalNetworks.length === 0) {
|
|
settings.allowedBikeRentalNetworks = null;
|
|
}
|
|
|
|
const transitModes = filterTransitModes(
|
|
relaxSettings ? defaultSettings.modes : settings.modes,
|
|
planType,
|
|
config,
|
|
);
|
|
|
|
let otpModes = transitModes.map(mode => {
|
|
return { mode };
|
|
});
|
|
if (config.customWeights) {
|
|
otpModes.forEach(m => {
|
|
if (config.customWeights[m.mode]) {
|
|
// eslint-disable-next-line
|
|
m.cost = { reluctance: config.customWeights[m.mode] };
|
|
}
|
|
});
|
|
}
|
|
|
|
// non-direct for testing purposes on planners that only allow direct
|
|
const directFlexOnly =
|
|
config.experimental?.allowDirectFlexJourneys &&
|
|
!window.localStorage.getItem('favouriteStore')?.includes('Flextestaus2025');
|
|
const directOnly = directModes.includes(planType) || otpModes.length === 0;
|
|
let transitOnly = !!relaxSettings;
|
|
const wheelchair = !!settings.accessibilityOption;
|
|
const cityBike =
|
|
!wheelchair && settings.allowedBikeRentalNetworks?.length > 0;
|
|
// set defaults
|
|
let access = cityBike ? ['WALK', 'BICYCLE_RENTAL'] : ['WALK'];
|
|
let egress = access;
|
|
let transfer = ['WALK'];
|
|
let direct = null;
|
|
let numItineraries = directOnly ? 1 : 5;
|
|
let carReluctance = null;
|
|
let noIterationsForShortTrips = false;
|
|
// A null value uses the default amount of maximum iterations.
|
|
let maxQueryIterations = null;
|
|
|
|
switch (planType) {
|
|
case PLANTYPE.BIKEPARK:
|
|
access = ['BICYCLE_PARKING'];
|
|
transitOnly = true;
|
|
noIterationsForShortTrips = shortTrip;
|
|
break;
|
|
case PLANTYPE.BIKETRANSIT:
|
|
access = ['BICYCLE'];
|
|
egress = ['BICYCLE'];
|
|
transfer = ['BICYCLE'];
|
|
transitOnly = true;
|
|
noIterationsForShortTrips = shortTrip;
|
|
break;
|
|
case PLANTYPE.CARTRANSIT:
|
|
// For cars the direct mode is included to allow OTP to filter out unwanted routes.
|
|
direct = ['CAR'];
|
|
access = ['CAR'];
|
|
egress = ['CAR'];
|
|
transfer = ['CAR'];
|
|
transitOnly = false;
|
|
numItineraries = 6;
|
|
carReluctance = 1.75;
|
|
// This is done to enable more cache hits. New cache entries are generated for different speeds otherwise.
|
|
settings.walkSpeed = null;
|
|
settings.bikeSpeed = null;
|
|
settings.walkReluctance = null;
|
|
settings.bikeReluctance = null;
|
|
// As of writing this comment, iterating (paging) does not support filtering of bad car transit itineraries.
|
|
maxQueryIterations = 1;
|
|
break;
|
|
case PLANTYPE.PARKANDRIDE:
|
|
access = ['CAR_PARKING'];
|
|
transitOnly = true;
|
|
break;
|
|
case PLANTYPE.TRANSIT:
|
|
direct = access;
|
|
break;
|
|
case PLANTYPE.SCOOTERTRANSIT:
|
|
access = ['WALK', 'SCOOTER_RENTAL'];
|
|
egress = access;
|
|
direct = access;
|
|
break;
|
|
case PLANTYPE.FLEXTRANSIT:
|
|
access = directFlexOnly ? null : ['WALK', 'FLEX'];
|
|
egress = access;
|
|
direct = directFlexOnly ? ['WALK', 'FLEX'] : null;
|
|
transitOnly = false;
|
|
break;
|
|
default: // direct modes
|
|
direct = [planType];
|
|
break;
|
|
}
|
|
// reset unused arrays
|
|
if (directOnly) {
|
|
access = null;
|
|
egress = null;
|
|
transfer = null;
|
|
otpModes = [];
|
|
}
|
|
if (!access?.includes('BICYCLE_RENTAL')) {
|
|
settings.allowedBikeRentalNetworks = null;
|
|
}
|
|
if (!access?.includes('SCOOTER_RENTAL')) {
|
|
settings.scooterNetworks = null;
|
|
}
|
|
|
|
const modes = {
|
|
directOnly,
|
|
transitOnly,
|
|
direct,
|
|
transit: {
|
|
access,
|
|
transfer,
|
|
egress,
|
|
transit: otpModes.length === 0 ? null : otpModes,
|
|
},
|
|
};
|
|
|
|
const walkReluctance = relaxSettings
|
|
? defaultSettings.walkReluctance
|
|
: settings.walkReluctance;
|
|
const walkBoardCost = relaxSettings
|
|
? defaultSettings.walkBoardCost
|
|
: settings.walkBoardCost;
|
|
const transferPenalty = relaxSettings
|
|
? defaultSettings.transferPenalty
|
|
: settings.transferPenalty;
|
|
const timeStr = (time ? DateTime.fromSeconds(+time) : DateTime.now()).toISO({
|
|
suppressMilliseconds: true,
|
|
});
|
|
const datetime = useLatestArrival
|
|
? { latestArrival: timeStr }
|
|
: { earliestDeparture: timeStr };
|
|
|
|
return {
|
|
...settings,
|
|
allowedRentalNetworks:
|
|
planType === PLANTYPE.SCOOTERTRANSIT
|
|
? settings.scooterNetworks
|
|
: settings.allowedBikeRentalNetworks,
|
|
fromPlace,
|
|
toPlace,
|
|
datetime,
|
|
minTransferTime: `PT${settings.minTransferTime}S`,
|
|
first: numItineraries, // used in actual query
|
|
numItineraries, // backup original value for convenient paging
|
|
wheelchair,
|
|
walkReluctance,
|
|
walkBoardCost,
|
|
transferPenalty,
|
|
modes,
|
|
planType,
|
|
noIterationsForShortTrips,
|
|
via,
|
|
carReluctance,
|
|
maxQueryIterations,
|
|
};
|
|
}
|