mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-06-16 05:00:40 +02:00
618 lines
17 KiB
JavaScript
618 lines
17 KiB
JavaScript
import debounce from 'lodash/debounce';
|
|
import flatten from 'lodash/flatten';
|
|
import take from 'lodash/take';
|
|
import {
|
|
sortSearchResults,
|
|
isStop,
|
|
} from '@digitransit-search-util/digitransit-search-util-helpers';
|
|
import filterMatchingToInput from '@digitransit-search-util/digitransit-search-util-filter-matching-to-input';
|
|
import getGeocodingResults from '@digitransit-search-util/digitransit-search-util-get-geocoding-results';
|
|
import getJson from '@digitransit-search-util/digitransit-search-util-get-json';
|
|
|
|
function getStopsFromGeocoding(stops, URL_PELIAS_PLACE) {
|
|
if (!stops || stops.length < 1) {
|
|
return Promise.resolve([]);
|
|
}
|
|
let gids = '';
|
|
const stopsWithGids = stops.map(stop => {
|
|
const type = stop.type === 'stop' ? 'stop' : 'station';
|
|
let gid = `gtfs${stop.gtfsId.split(':')[0].toLowerCase()}:${type}:GTFS:${
|
|
stop.gtfsId
|
|
}`;
|
|
if (stop.code) {
|
|
gid += `#${stop.code}`;
|
|
}
|
|
gids += `${gid},`;
|
|
return { ...stop, gid };
|
|
});
|
|
const stopStationMap = stopsWithGids.reduce(function redu(
|
|
map,
|
|
stopOrStation,
|
|
) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
map[stopOrStation.gid] = stopOrStation;
|
|
return map;
|
|
}, {});
|
|
return getJson(URL_PELIAS_PLACE, {
|
|
ids: gids.slice(0, -1),
|
|
// lang: context.getStore('PreferencesStore').getLanguage(), TODO enable this when OTP supports translations
|
|
}).then(res => {
|
|
return res.features.map(stop => {
|
|
const favourite = {
|
|
type: 'FavouriteStop',
|
|
properties: {
|
|
addendum: stop.properties.addendum,
|
|
gid: stopStationMap[stop.properties.gid].gid,
|
|
code: stopStationMap[stop.properties.gid].code,
|
|
gtfsId: stopStationMap[stop.properties.gid].gtfsId,
|
|
lastUpdated: stopStationMap[stop.properties.gid].lastUpdated,
|
|
favouriteId: stopStationMap[stop.properties.gid].favouriteId,
|
|
address: stop.properties.label,
|
|
layer: isStop(stop.properties) ? 'favouriteStop' : 'favouriteStation',
|
|
},
|
|
geometry: stop.geometry,
|
|
};
|
|
return favourite;
|
|
});
|
|
});
|
|
}
|
|
function filterFavouriteLocations(favourites, input) {
|
|
return Promise.resolve(
|
|
filterMatchingToInput(favourites, input, ['address', 'name']).map(item => ({
|
|
type: 'FavouritePlace',
|
|
properties: {
|
|
...item,
|
|
label: item.name,
|
|
layer: 'favouritePlace',
|
|
},
|
|
geometry: { type: 'Point', coordinates: [item.lon, item.lat] },
|
|
})),
|
|
);
|
|
}
|
|
function selectPositionFomMap(input) {
|
|
if (typeof input !== 'string' || input.length === 0) {
|
|
return Promise.resolve([
|
|
{
|
|
type: 'SelectFromMap',
|
|
address: 'SelectFromMap',
|
|
lat: null,
|
|
lon: null,
|
|
properties: {
|
|
labelId: 'select-from-map',
|
|
layer: 'selectFromMap',
|
|
address: 'SelectFromMap',
|
|
lat: null,
|
|
lon: null,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [],
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
function getCurrentPositionIfEmpty(input, position) {
|
|
if (typeof input !== 'string' || input.length === 0) {
|
|
return Promise.resolve([
|
|
{
|
|
type: 'CurrentLocation',
|
|
address: position.address,
|
|
lat: position.lat,
|
|
lon: position.lon,
|
|
properties: {
|
|
labelId: 'use-own-position',
|
|
layer: 'currentPosition',
|
|
address: position.address,
|
|
lat: position.lat,
|
|
lon: position.lon,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [position.lon, position.lat],
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
function selectFromOwnLocations(input) {
|
|
if (typeof input !== 'string' || input.length === 0) {
|
|
return Promise.resolve([
|
|
{
|
|
type: 'SelectFromOwnLocations',
|
|
address: 'SelectFromOwnLocations',
|
|
lat: null,
|
|
lon: null,
|
|
properties: {
|
|
labelId: 'select-from-own-locations',
|
|
layer: 'ownLocations',
|
|
address: 'selectFromOwnLocations',
|
|
lat: null,
|
|
lon: null,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [],
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
function getBackSuggestion() {
|
|
return Promise.resolve([
|
|
{
|
|
type: 'back',
|
|
address: 'back',
|
|
lat: null,
|
|
lon: null,
|
|
properties: {
|
|
labelId: 'back',
|
|
layer: 'back',
|
|
address: 'back',
|
|
lat: null,
|
|
lon: null,
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [],
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
|
|
function filterFavouriteStops(stopsAndStations, input, useStops, useStations) {
|
|
return stopsAndStations.then(stopsStations => {
|
|
const candidates = stopsStations.filter(s =>
|
|
s.type === 'stop' ? useStops : useStations,
|
|
);
|
|
return filterMatchingToInput(candidates, input, [
|
|
'properties.name',
|
|
'properties.name',
|
|
'properties.address',
|
|
]);
|
|
});
|
|
}
|
|
|
|
function filterOldSearches(oldSearches, input, dropLayers) {
|
|
let matchingOldSearches = filterMatchingToInput(oldSearches, input, [
|
|
'properties.name',
|
|
'properties.label',
|
|
'properties.address',
|
|
'properties.shortName',
|
|
'properties.longName',
|
|
]);
|
|
if (dropLayers) {
|
|
// don't want these
|
|
matchingOldSearches = matchingOldSearches.filter(
|
|
item => !dropLayers.includes(item.properties.layer),
|
|
);
|
|
}
|
|
return Promise.resolve(
|
|
take(matchingOldSearches, 10).map(item => {
|
|
const newItem = {
|
|
...item,
|
|
type: 'OldSearch',
|
|
timetableClicked: false, // reset latest selection action
|
|
arrowClicked: false,
|
|
};
|
|
delete newItem.properties.confidence;
|
|
return newItem;
|
|
}),
|
|
);
|
|
}
|
|
|
|
function hasFavourites(searchContext) {
|
|
const favouriteLocations = searchContext.getFavouriteLocations(
|
|
searchContext.context,
|
|
);
|
|
if (favouriteLocations?.length > 0) {
|
|
return true;
|
|
}
|
|
|
|
const favouriteVehicleRentalStations =
|
|
searchContext.getFavouriteVehicleRentalStations(searchContext.context);
|
|
if (favouriteVehicleRentalStations?.length > 0) {
|
|
return true;
|
|
}
|
|
const favouriteStops = searchContext.getFavouriteStops(searchContext.context);
|
|
return favouriteStops?.length > 0;
|
|
}
|
|
|
|
const routeLayers = [
|
|
'route-TRAM',
|
|
'route-BUS',
|
|
'route-RAIL',
|
|
'route-FERRY',
|
|
'route-SUBWAY',
|
|
'route-AIRPLANE',
|
|
'route-FUNICULAR',
|
|
];
|
|
const locationLayers = ['venue', 'address', 'street'];
|
|
const parkingLayers = ['carpark', 'bikepark'];
|
|
const stopLayers = ['stop', 'station'];
|
|
|
|
/**
|
|
* Executes the search
|
|
*
|
|
*/
|
|
export function getSearchResults(
|
|
targets,
|
|
sources,
|
|
transportMode,
|
|
searchContext,
|
|
filterResults,
|
|
geocodingSize,
|
|
{ input },
|
|
callback,
|
|
pathOpts,
|
|
refPoint,
|
|
) {
|
|
const {
|
|
getPositions,
|
|
getFavouriteLocations,
|
|
getOldSearches,
|
|
getFavouriteStops,
|
|
parkingAreaSources,
|
|
getLanguage,
|
|
getStopAndStationsQuery,
|
|
getFavouriteVehicleRentalStationsQuery,
|
|
getFavouriteVehicleRentalStations,
|
|
getFavouriteRoutesQuery,
|
|
getFavouriteRoutes,
|
|
getRoutesQuery,
|
|
context,
|
|
isPeliasLocationAware: locationAware,
|
|
minimalRegexp,
|
|
lineRegexp,
|
|
URL_PELIAS,
|
|
URL_PELIAS_PLACE,
|
|
feedIDs,
|
|
geocodingSearchParams,
|
|
geocodingSources,
|
|
getFutureRoutes,
|
|
cityBikeNetworks,
|
|
} = searchContext;
|
|
// if no targets are provided, search them all.
|
|
const allTargets = !targets || targets.length === 0;
|
|
// if no sources are provided, use them all.
|
|
const allSources = !sources || sources.length === 0;
|
|
const position = getPositions(context);
|
|
const searchComponents = [];
|
|
const searches = { type: 'all', term: input, results: [] };
|
|
const language = getLanguage(context);
|
|
const focusPoint =
|
|
locationAware && refPoint?.lat && refPoint?.lon
|
|
? {
|
|
// Round coordinates to approx 1 km, in order to improve caching
|
|
'focus.point.lat': refPoint.lat.toFixed(3),
|
|
'focus.point.lon': refPoint.lon.toFixed(3),
|
|
}
|
|
: {};
|
|
const mode = transportMode ? transportMode.split('-')[1] : undefined;
|
|
if (
|
|
targets.includes('CurrentPosition') &&
|
|
position.status !== 'geolocation-not-supported'
|
|
) {
|
|
searchComponents.push(getCurrentPositionIfEmpty(input, position));
|
|
}
|
|
if (allTargets || targets.includes('MapPosition')) {
|
|
searchComponents.push(selectPositionFomMap(input));
|
|
}
|
|
if (targets.includes('FutureRoutes')) {
|
|
const items = getFutureRoutes(context);
|
|
searchComponents.push(take(items, 3));
|
|
}
|
|
if (
|
|
targets.includes('SelectFromOwnLocations') &&
|
|
hasFavourites(searchContext)
|
|
) {
|
|
searchComponents.push(selectFromOwnLocations(input));
|
|
}
|
|
if (allTargets || targets.includes('Locations')) {
|
|
// eslint-disable-next-line prefer-destructuring
|
|
const searchParams = geocodingSearchParams;
|
|
if (sources.includes('Favourite')) {
|
|
const favouriteLocations = getFavouriteLocations(context);
|
|
searchComponents.push(
|
|
filterFavouriteLocations(favouriteLocations, input),
|
|
);
|
|
if (sources.includes('Back')) {
|
|
searchComponents.push(getBackSuggestion());
|
|
}
|
|
}
|
|
|
|
if (allSources || sources.includes('Datasource')) {
|
|
const geocodingLayers = ['venue', 'address', 'street'];
|
|
if (targets.includes('Stations')) {
|
|
geocodingLayers.push('station'); // search stations from OSM
|
|
}
|
|
searchComponents.push(
|
|
getGeocodingResults(
|
|
input,
|
|
searchParams,
|
|
language,
|
|
focusPoint,
|
|
geocodingSources.join(','),
|
|
URL_PELIAS,
|
|
minimalRegexp,
|
|
geocodingLayers,
|
|
),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('History')) {
|
|
const locationHistory = getOldSearches(context, 'endpoint');
|
|
const dropLayers = ['bikestation'];
|
|
dropLayers.push(...stopLayers);
|
|
dropLayers.push(...routeLayers);
|
|
dropLayers.push(...parkingLayers);
|
|
searchComponents.push(
|
|
filterOldSearches(locationHistory, input, dropLayers),
|
|
);
|
|
}
|
|
}
|
|
if (allTargets || targets.includes('ParkingAreas')) {
|
|
if (allSources || sources.includes('Datasource')) {
|
|
const searchParams = geocodingSearchParams;
|
|
if (geocodingSize && geocodingSize !== 10) {
|
|
searchParams.size = geocodingSize;
|
|
}
|
|
const geocodingLayers = ['carpark', 'bikepark'];
|
|
const feedIds = parkingAreaSources ? parkingAreaSources.join(',') : null;
|
|
searchComponents.push(
|
|
getGeocodingResults(
|
|
input,
|
|
searchParams,
|
|
language,
|
|
focusPoint,
|
|
feedIds,
|
|
URL_PELIAS,
|
|
minimalRegexp,
|
|
geocodingLayers,
|
|
).then(results => {
|
|
if (filterResults) {
|
|
return filterResults(results, mode);
|
|
}
|
|
return results;
|
|
}),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('History')) {
|
|
const history = getOldSearches(context);
|
|
const dropLayers = ['bikestation'];
|
|
dropLayers.push(...stopLayers);
|
|
dropLayers.push(...routeLayers);
|
|
dropLayers.push(...locationLayers);
|
|
searchComponents.push(filterOldSearches(history, input, dropLayers));
|
|
}
|
|
}
|
|
|
|
const useStops = targets.includes('Stops');
|
|
const useStations = targets.includes('Stations');
|
|
|
|
if (allTargets || useStops || useStations) {
|
|
if (sources.includes('Favourite')) {
|
|
const favouriteStops = getFavouriteStops(context);
|
|
let stopsAndStations;
|
|
if (favouriteStops.every(stop => stop.type === 'station')) {
|
|
stopsAndStations = getStopsFromGeocoding(
|
|
favouriteStops,
|
|
URL_PELIAS_PLACE,
|
|
).then(results => {
|
|
if (filterResults) {
|
|
return filterResults(results, mode);
|
|
}
|
|
return results;
|
|
});
|
|
} else {
|
|
stopsAndStations = getStopAndStationsQuery(favouriteStops)
|
|
.then(favourites =>
|
|
getStopsFromGeocoding(favourites, URL_PELIAS_PLACE),
|
|
)
|
|
.then(results => {
|
|
if (filterResults) {
|
|
return filterResults(results, mode);
|
|
}
|
|
return results;
|
|
});
|
|
}
|
|
searchComponents.push(
|
|
filterFavouriteStops(stopsAndStations, input, useStops, useStations),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('Datasource')) {
|
|
const geocodingLayers = [];
|
|
if (useStops) {
|
|
geocodingLayers.push('stop');
|
|
}
|
|
if (useStations) {
|
|
geocodingLayers.push('station');
|
|
}
|
|
const searchParams =
|
|
geocodingSize && geocodingSize !== 10 ? { size: geocodingSize } : {};
|
|
if (geocodingSearchParams && geocodingSearchParams['boundary.country']) {
|
|
searchParams['boundary.country'] =
|
|
geocodingSearchParams['boundary.country'];
|
|
}
|
|
// a little hack: when searching location data, automatically dedupe stops
|
|
// this could be a new explicit prop
|
|
if (allTargets || targets.includes('Locations')) {
|
|
searchParams.dedupestops = 1;
|
|
}
|
|
const feeds = feedIDs.map(v => `gtfs${v}`).join(',');
|
|
searchComponents.push(
|
|
getGeocodingResults(
|
|
input,
|
|
searchParams,
|
|
language,
|
|
focusPoint,
|
|
feeds,
|
|
URL_PELIAS,
|
|
minimalRegexp,
|
|
geocodingLayers,
|
|
).then(results => {
|
|
if (filterResults) {
|
|
return filterResults(results, mode);
|
|
}
|
|
return results;
|
|
}),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('History')) {
|
|
const stopHistory = getOldSearches(context).filter(item => {
|
|
if (item.properties.gid) {
|
|
return item.properties.gid.includes('GTFS:');
|
|
}
|
|
return true;
|
|
});
|
|
const dropLayers = ['bikestation'];
|
|
dropLayers.push(...routeLayers);
|
|
dropLayers.push(...locationLayers);
|
|
dropLayers.push(...parkingLayers);
|
|
if (!useStops) {
|
|
dropLayers.push('stop');
|
|
}
|
|
if (!useStations) {
|
|
dropLayers.push('station');
|
|
}
|
|
if (transportMode) {
|
|
searchComponents.push(
|
|
filterOldSearches(stopHistory, input, dropLayers).then(result =>
|
|
filterResults ? filterResults(result, mode) : result,
|
|
),
|
|
);
|
|
} else {
|
|
searchComponents.push(
|
|
filterOldSearches(stopHistory, input, dropLayers),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (allTargets || targets.includes('Routes')) {
|
|
if (sources.includes('Favourite')) {
|
|
const favouriteRoutes = getFavouriteRoutes(context);
|
|
searchComponents.push(
|
|
getFavouriteRoutesQuery(favouriteRoutes, input, mode, pathOpts).then(
|
|
result =>
|
|
filterResults ? filterResults(result, mode, 'Routes') : result,
|
|
),
|
|
);
|
|
}
|
|
searchComponents.push(
|
|
getRoutesQuery(input, feedIDs, transportMode, pathOpts).then(result =>
|
|
filterResults ? filterResults(result, mode, 'Routes') : result,
|
|
),
|
|
);
|
|
if (allSources || sources.includes('History')) {
|
|
const routeHistory = getOldSearches(context);
|
|
const dropLayers = ['bikestation'];
|
|
if (transportMode) {
|
|
dropLayers.push(...routeLayers.filter(i => !(i === transportMode)));
|
|
}
|
|
dropLayers.push(...stopLayers);
|
|
dropLayers.push(...locationLayers);
|
|
dropLayers.push(...parkingLayers);
|
|
|
|
searchComponents.push(
|
|
filterOldSearches(routeHistory, input, dropLayers).then(results =>
|
|
filterResults ? filterResults(results, mode, 'Routes') : results,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
if (allTargets || targets.includes('VehicleRentalStations')) {
|
|
if (sources.includes('Favourite')) {
|
|
const favouriteVehicleRentalStation =
|
|
getFavouriteVehicleRentalStations(context);
|
|
searchComponents.push(
|
|
getFavouriteVehicleRentalStationsQuery(
|
|
favouriteVehicleRentalStation,
|
|
input,
|
|
),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('Datasource')) {
|
|
const geocodingLayers = ['bikestation'];
|
|
const searchParams =
|
|
geocodingSize && geocodingSize !== 10 ? { size: geocodingSize } : {};
|
|
searchComponents.push(
|
|
getGeocodingResults(
|
|
input,
|
|
searchParams,
|
|
language,
|
|
focusPoint,
|
|
cityBikeNetworks.join(','),
|
|
URL_PELIAS,
|
|
minimalRegexp,
|
|
geocodingLayers,
|
|
).then(results => {
|
|
if (filterResults) {
|
|
return filterResults(results, mode, 'VehicleRentalStations');
|
|
}
|
|
return results;
|
|
}),
|
|
);
|
|
}
|
|
if (allSources || sources.includes('History')) {
|
|
const history = getOldSearches(context);
|
|
const dropLayers = [...stopLayers];
|
|
dropLayers.push(...routeLayers);
|
|
dropLayers.push(...locationLayers);
|
|
dropLayers.push(...parkingLayers);
|
|
|
|
searchComponents.push(filterOldSearches(history, input, dropLayers));
|
|
}
|
|
}
|
|
|
|
const searchResultsPromise = Promise.all(searchComponents)
|
|
.then(flatten)
|
|
.then(results => {
|
|
searches.results = results;
|
|
})
|
|
.catch(err => {
|
|
searches.error = err;
|
|
});
|
|
searchResultsPromise.then(() => {
|
|
callback({
|
|
...searches,
|
|
results: sortSearchResults(lineRegexp, searches.results, input),
|
|
});
|
|
});
|
|
}
|
|
|
|
const debouncedSearch = debounce(getSearchResults, 300, {
|
|
leading: true,
|
|
});
|
|
|
|
export const executeSearch = (
|
|
targets,
|
|
sources,
|
|
transportMode,
|
|
searchContext,
|
|
filterResults,
|
|
geocodingSize,
|
|
data,
|
|
callback,
|
|
pathOpts,
|
|
refPoint,
|
|
) => {
|
|
callback(null); // This means 'we are searching'
|
|
debouncedSearch(
|
|
targets,
|
|
sources,
|
|
transportMode,
|
|
searchContext,
|
|
filterResults,
|
|
geocodingSize,
|
|
data,
|
|
callback,
|
|
pathOpts,
|
|
refPoint,
|
|
);
|
|
};
|