UNPKG

@golemio/pid

Version:
531 lines 28.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var TransferFacade_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransferFacade = void 0; const DateTimeUtils_1 = require("../../../../helpers/DateTimeUtils"); const GtfsStopParser_1 = require("../../../../helpers/GtfsStopParser"); const RouteTypeEnums_1 = require("../../../../helpers/RouteTypeEnums"); const data_access_1 = require("../../data-access"); const DepartureFilter_1 = require("../../helpers/enums/DepartureFilter"); const DepartureMode_1 = require("../../helpers/enums/DepartureMode"); const DepartureOrder_1 = require("../../helpers/enums/DepartureOrder"); const DepartureSkip_1 = require("../../helpers/enums/DepartureSkip"); const TransferBoardFilter_1 = require("../../helpers/TransferBoardFilter"); const OgPidToken_1 = require("../../ioc/OgPidToken"); const PIDDepartureModel_1 = __importDefault(require("../../models/helpers/PIDDepartureModel")); const GtfsTripStopsRepository_1 = require("../../../ropid-gtfs/data-access/redis/GtfsTripStopsRepository"); const DelayComputationRepository_1 = require("../../../public/data-access/redis/DelayComputationRepository"); const PublicGtfsDepartureRepository_1 = require("../../../public/data-access/redis/PublicGtfsDepartureRepository"); const PublicStopTimeRepository_1 = require("../../../public/data-access/redis/PublicStopTimeRepository"); const PublicVehiclePositionsRepository_1 = require("../../../public/data-access/redis/PublicVehiclePositionsRepository"); const shared_1 = require("../../../shared"); const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken"); const monitoring_1 = require("@golemio/core/dist/monitoring"); const golemio_errors_1 = require("@golemio/core/dist/shared/golemio-errors"); const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe"); const const_1 = require("../../../../const"); const TransferDepartureTransformation_1 = require("../transformations/TransferDepartureTransformation"); const DUMMY_EMPTY_STRING_FOR_FILTERING = ""; let TransferFacade = TransferFacade_1 = class TransferFacade { constructor(log, publicDepartureRepository, departuresRepository, departureTransformation, tripRepository, stopTimeRepository, delayComputationRepository, gtfsTripStopsRepository) { this.log = log; this.publicDepartureRepository = publicDepartureRepository; this.departuresRepository = departuresRepository; this.departureTransformation = departureTransformation; this.tripRepository = tripRepository; this.stopTimeRepository = stopTimeRepository; this.delayComputationRepository = delayComputationRepository; this.gtfsTripStopsRepository = gtfsTripStopsRepository; } async getTransferDepartures(stopIds, tripNumber, currentMoment, minutesOffset, timeZone = shared_1.RopidRouterUtils.TIMEZONE) { let departureEntities = []; try { departureEntities = await this.departuresRepository.getTransferDepartures({ stopsIds: stopIds, currentMoment, minutesOffset, }); } catch (error) { if (error instanceof golemio_errors_1.AbstractGolemioError) { throw error; } throw new golemio_errors_1.GeneralError("Failed to retrieve transfer departures", this.constructor.name, error, 500); } if (departureEntities.length === 0) { return []; } let transferDepartures = []; try { const processedDepartures = new PIDDepartureModel_1.default(departureEntities, { ...this.defaultOptions, timezone: timeZone, tripNumber, }).processAndReturnTransfers(); transferDepartures = this.departureTransformation.transformArray(processedDepartures); } catch (error) { if (error instanceof golemio_errors_1.AbstractGolemioError) { throw error; } throw new golemio_errors_1.GeneralError("Failed to process transfer departures", this.constructor.name, error, 500); } return transferDepartures; } static sortByDepartureTime(a, b) { if (a.departure_datetime < b.departure_datetime) { return -1; } if (a.departure_datetime > b.departure_datetime) { return 1; } return 0; } static isDifferentLine(departure, currentArrival) { if (!departure.trip_id || departure.trip_id === currentArrival.trip_id) { return false; // leftover data OR same trip = remove itself } const isSameLine = departure.route_short_name === currentArrival.route_short_name && departure.route_type === currentArrival.route_type; if (isSameLine && departure.direction_id != null && departure.direction_id !== currentArrival.direction_id) { return false; // same line, opposite direction = not a relevant transfer } return true; } static allowedTransfers(departures, currentStopId, stopIds) { const shouldKeepDeparturesWithPossiblyGuaranteedTransfer = true; return TransferBoardFilter_1.TransferBoardFilter.sameStopNameTransfer(departures.map((departure) => { return { departure, }; }), currentStopId, stopIds, null, shouldKeepDeparturesWithPossiblyGuaranteedTransfer).map((d) => d.departure); } // platform_code not available in gtfsTripStopsCache and not used by line-direction filters static jsonPathDataToLineFilterStop(stopInfo) { return { stop_id: GtfsStopParser_1.GtfsStopParser.normalizedNodeId(stopInfo[0]), stop_name: stopInfo[1], platform_code: DUMMY_EMPTY_STRING_FOR_FILTERING, }; } calculateTimeFrom(currentArrival, currentPosition, delayedTimeFrom, plannedTimeFrom) { const arrivalDate = new Date(currentArrival.arrival_datetime); // arrival before expected time (negative number) is taken as 0 delay // arrival after expected time (positive number) is taken as is const delayOnArrivalMs = Math.max(currentPosition.delay ?? 0, 0) * 1000; delayedTimeFrom.setTime(arrivalDate.getTime() + delayOnArrivalMs); plannedTimeFrom.setTime(arrivalDate.getTime()); } async getTransferCache(stopId, stopIds, limit, reqNumber, vehicleType, timeFrom, sameNameStopIds) { const span = (0, monitoring_1.createChildSpan)(`Departures.getTransferCache`); span?.setAttributes({ stopIds, limit, reqNumber, vehicleType }); let delayedTimeFrom = new Date(timeFrom.getTime()); let plannedTimeFrom = new Date(timeFrom.getTime()); let currentArrival = null; let currentStop = null; let currentTripId = null; try { // 1. find the vehicle const currentPosition = await this.getDetailedVehiclePositionWithFallback(vehicleType, reqNumber); if (currentPosition) { // vehicle is on the map // 2. find its arrival to the stop-id currentArrival = await this.findVehicleArrival(currentPosition, stopIds, stopId); } if (currentPosition && currentArrival) { currentStop = { stop_id: currentArrival.stop_id, stop_name: currentArrival.stop_name, platform_code: currentArrival.platform_code || DUMMY_EMPTY_STRING_FOR_FILTERING, stop_icons: currentArrival.stop_icons, }; currentTripId = currentPosition.gtfs_trip_id; this.calculateTimeFrom(currentArrival, currentPosition, delayedTimeFrom, plannedTimeFrom); } else { // Vehicle does not ride into such stop-ids, or vehicle does not have Position return { transfers: [], delayedTimeFrom, plannedTimeFrom, currentStop, currentTripId, }; } // timeFrom in query can be too early, minimum is the real arrival of the vehicle if (timeFrom.getTime() > delayedTimeFrom.getTime()) { delayedTimeFrom = new Date(timeFrom); plannedTimeFrom = new Date(timeFrom); } const delayMinutes = Math.abs(Math.floor((delayedTimeFrom.getTime() - plannedTimeFrom.getTime()) / TransferFacade_1.MINUTE_AS_MS)); let departureEntities = []; try { // there are stops with max. 120 departures in 2h const optimizedLimit = Math.max(TransferFacade_1.DEPARTURES_LIMIT_LOWER, Math.min(TransferFacade_1.DEPARTURES_LIMIT_UPPER, limit * 10)); // 3. find departures from all stop_ids in Node // look forward 1 hour (add delay + open minutes), see JIS Transferboards Design Document departureEntities = await this.publicDepartureRepository.getPublicGtfsDepartureCache(stopIds, 60 + TransferFacade_1.OPEN_MINUTES_IN_PAST + delayMinutes, new Date(plannedTimeFrom.getTime() - TransferFacade_1.OPEN_MINUTES_IN_PAST_MS), optimizedLimit); } catch (error) { span?.recordException(error); if (error instanceof golemio_errors_1.AbstractGolemioError) { throw error; } throw new golemio_errors_1.GeneralError("Failed to publicGtfsDepartureCache", this.constructor.name, error, 500); } // Remove current line (and trip) from departureEntities departureEntities = departureEntities.filter((departure) => TransferFacade_1.isDifferentLine(departure, currentArrival)); // Remove departures of disallowed route-types on stops with different name(s) departureEntities = TransferFacade_1.allowedTransfers(departureEntities, currentStop.stop_id, sameNameStopIds); departureEntities.sort(TransferFacade_1.sortByDepartureTime); const uniqueDeparturesTripIds = Array.from(new Set(departureEntities.map((d) => d.trip_id))); // to be able to load future trips of Before_Track positioned vehicle const omitFutureTrips = false; const positionsByTrip = await this.tripRepository.getAllVehiclePositionsForMultipleTrips(uniqueDeparturesTripIds, omitFutureTrips); let transfers = []; const moreDepartures = await this.processMultiplePositionsForDeparturesAtOnce(departureEntities, positionsByTrip); transfers.push(...moreDepartures); return { transfers, delayedTimeFrom, plannedTimeFrom, currentStop, currentTripId, }; } catch (error) { span?.recordException(error); if (error instanceof golemio_errors_1.AbstractGolemioError) { throw error; } throw new golemio_errors_1.GeneralError("Error in getTransferCache", this.constructor.name, error, 500); } finally { span?.end(); } } static findStopTimeByStopId(stopTimes, stopIds) { return stopTimes.find((stopTime) => stopIds.includes(stopTime.stop_id)) || null; } /** * Find position from cache for a vehicle of given type and registration number. If no position was found and the vehicle type * given is bus, attempt fallback to trolleybus. */ async getDetailedVehiclePositionWithFallback(vehicleType, reqNumber) { const position = await this.tripRepository.getDetailedVehiclePosition(`service-${vehicleType}-${reqNumber}`); if (!position && vehicleType === RouteTypeEnums_1.GTFSRouteTypeEnum.BUS) { this.log.info(`${this.constructor.name}: Fallback to trolleybus for registration number ${reqNumber}`); return await this.tripRepository.getDetailedVehiclePosition(`service-${RouteTypeEnums_1.GTFSRouteTypeEnum.TROLLEYBUS}-${reqNumber}`); } return position; } async findVehicleArrival(currentPosition, stopIds, stopId) { const stopTimes = (await this.delayComputationRepository.getDelayComputationCacheProperty(currentPosition.gtfs_trip_id, "stop_times")); if (!stopTimes?.stop_times?.length) { return null; } // It is possible (based on incoming stop_ids), we won't find departure // for this vehicle directly on this stopId... let departureAtStopId = TransferFacade_1.findStopTimeByStopId(stopTimes.stop_times, [stopId]); // But it can be in the whole Node = iterate through all stop_ids then. departureAtStopId ??= TransferFacade_1.findStopTimeByStopId(stopTimes.stop_times, stopIds); if (departureAtStopId) { const arrivalDate = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForTripOrigin(departureAtStopId.arrival_time_seconds, currentPosition.detailed_info.origin_timestamp); const departureDate = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForTripOrigin(departureAtStopId.departure_time_seconds, currentPosition.detailed_info.origin_timestamp); return { stop_name: departureAtStopId.stop.stop_name, stop_id: departureAtStopId.stop_id, departure_datetime: departureDate.toISOString(), arrival_datetime: arrivalDate.toISOString(), route_short_name: currentPosition.gtfs_route_short_name, route_type: currentPosition.route_type, trip_id: currentPosition.gtfs_trip_id, stop_sequence: departureAtStopId.stop_sequence, platform_code: null, stop_icons: departureAtStopId.stop_icons, direction_id: currentPosition.detailed_info.direction_id, }; } return null; } async processMultiplePositionsForDeparturesAtOnce(departures, positionsByTrip) { const outputGroup = []; const trainIdTripIdTuples = []; const trainDepartures = new Map(); for (const departure of departures) { // for each departure, use its trip_id + multiple position const positions = positionsByTrip.get(departure.trip_id) || []; if (!positions.length) { outputGroup.push({ departure, position: null, stopTime: null, }); continue; } const validPositions = positions.filter((pos) => pos.state_position !== const_1.StatePositionEnum.CANCELED); for (const pos of validPositions) { if (departure.platform_code == null || departure.route_type === RouteTypeEnums_1.GTFSRouteTypeEnum.TRAIN) { trainDepartures.set(departure.trip_id, departure); trainIdTripIdTuples.push([pos.vehicle_id, departure.trip_id]); continue; } // we have platform_code, thus we do not need to load stopTime.cis_platform_code outputGroup.push({ departure, // no stop_name position: pos, // no stop_name either stopTime: null, }); } } const stopTimeCachesByTripId = !trainIdTripIdTuples.length ? new Map() : await this.stopTimeRepository.getManyPublicStopTimeCaches(trainIdTripIdTuples); for (const [tripId, stopTimeCache] of Array.from(stopTimeCachesByTripId.entries())) { const departure = trainDepartures.get(tripId); const position = positionsByTrip.get(tripId)[0]; const stopTime = stopTimeCache.find((stopTime) => departure.stop_id === stopTime.stop_id); outputGroup.push({ departure, position: position || null, stopTime: stopTime || null, }); } return outputGroup; } static findPreviousStop(line) { return line.stops[Math.max(0, line.currentStopIndex - 1)]; } static isOtherEndNearCurrent(otherStops, currentStop) { const next = otherStops[0]; const last = otherStops[otherStops.length - 1]; return next.stop_id === last.stop_id && next.stop_id === currentStop.stop_id; } static isOtherSubsetOfCurrent(otherStops, currentStops) { let oIndex = 0, oLen = otherStops.length, cLen = currentStops.length; if (oLen > cLen) { // other line is longer, thus is not a subset return false; } for (let cIndex = 0; cIndex < cLen; cIndex++) { if (currentStops[cIndex].stop_id === otherStops[oIndex].stop_id) { oIndex++; if (oIndex === oLen) { return true; } } } return false; } static isOppositeDirection(previousStop, otherFutureStops, options) { return (otherFutureStops.length > 0 && previousStop.stop_id === otherFutureStops[0].stop_id && (options.isOppositeDirectionFilterStopNameStrict ? previousStop.stop_name === otherFutureStops[0].stop_name : true)); } static async filterOutForwardSubgroupOfLine(params) { const { currentLine } = params.data; if (!params.data.otherLinesMap.size) return params; const currentFutureStops = currentLine.stops.slice(currentLine.currentStopIndex + 1); const currentFutureStopIdsSet = new Set(currentFutureStops.map((stop) => stop.stop_id)); const previousStop = TransferFacade_1.findPreviousStop(currentLine); for (const [tripId, otherLine] of params.data.otherLinesMap.entries()) { if (params.data.tripsToKeep.has(tripId)) { // already valid trip from different filter impl. continue; } const otherFutureStops = otherLine.stops.slice(otherLine.currentStopIndex + 1); if (!otherFutureStops.length) { // remove it, no future stops on other line params.data.otherLinesMap.delete(tripId); continue; } // Filter 1.0: Check if next other stop is same as current stop or the line ends there if (TransferFacade_1.isOtherEndNearCurrent(otherFutureStops, params.data.currentStop)) { // remove it, transfer for 1-2 stops is not worth it params.data.otherLinesMap.delete(tripId); continue; } // Filter 1.1: Opposite direction is handled elsewhere, will skip all other Filters 1.x below const isOppositeDirection = TransferFacade_1.isOppositeDirection(previousStop, otherFutureStops, params.options); if (isOppositeDirection) { // leave it for other filter function, this one solves same direction only continue; } // Filter 1.2: Remove trip if other future stops are a subset of current route const isSubsetOfCurrentFuture = TransferFacade_1.isOtherSubsetOfCurrent(otherFutureStops, currentFutureStops); if (isSubsetOfCurrentFuture) { // remove from possible transfers, not relevant transfer at all params.data.otherLinesMap.delete(tripId); continue; } // Filter 1.3: If other line has something unique to my current trip, keep it const hasOtherFutureStopUniqueToCurrent = otherFutureStops.some(({ stop_id }) => !currentFutureStopIdsSet.has(stop_id)); if (hasOtherFutureStopUniqueToCurrent) { // keep this other-stop transfer line params.data.tripsToKeep.add(tripId); } } return params; } static async filterOutLinesWithBacktrackingConflict(params) { const { currentLine } = params.data; const previousCountMax = params.options.previousCountMax; if (!params.data.otherLinesMap.size) return params; // Cap at currentStopIndex — can't look back more stops than exist before current position const previousCount = Math.min(previousCountMax, currentLine.currentStopIndex); // Get previous N stops before currentStop, most recent first const currentLinePastStops = currentLine.stops .slice(0, currentLine.currentStopIndex) // all stops before current .reverse() .slice(0, previousCount + 1); // take N+1 most recent (if one would be different, line should pass) for (const [tripId, otherLine] of params.data.otherLinesMap.entries()) { if (params.data.tripsToKeep.has(tripId)) { continue; } const otherStopIndex = otherLine.currentStopIndex + 1; // Get next N+1 stops after currentStop in the other line const otherFutureStopsToCheck = otherLine.stops.slice(otherStopIndex, otherStopIndex + previousCount + 1); // Filter 2.0: Future stops of other line are subset (or same) as past stop (limit to previousCount) of current line. // The order of the current previous stops and other future stops is checked const isSubsetOfCurrentPast = TransferFacade_1.isOtherSubsetOfCurrent(otherFutureStopsToCheck, currentLinePastStops); if (isSubsetOfCurrentPast) { // remove from possible transfers, not relevant transfer at all params.data.otherLinesMap.delete(tripId); params.data.tripsToKeep.delete(tripId); continue; } params.data.tripsToKeep.add(tripId); } return params; } static async applyFiltersSequentially(params, filters) { return filters.reduce((promiseChain, filterFn) => promiseChain.then(filterFn), Promise.resolve(params)); } static rawTripLinesEntriesToTransfersMap(currentStopInput, currentTripId, rawLinesMap) { const otherLinesMap = new Map(); // apply same transformations as later on the other trips, so we have same format of stop_id / stop_name const currentStop = TransferFacade_1.jsonPathDataToLineFilterStop([ currentStopInput.stop_id, currentStopInput.stop_name, currentStopInput.platform_code, ]); for (const [tripId, rawLine] of rawLinesMap.entries()) { if (!rawLine?.length) { continue; } const stops = rawLine.map(TransferFacade_1.jsonPathDataToLineFilterStop); let index = null; if (tripId === currentTripId) { index = rawLine.findIndex((rawStop) => { return rawStop[0] === currentStopInput.stop_id; // gtfs stop id format }); } otherLinesMap.set(tripId, { stops, currentStopIndex: index ?? stops.findIndex((stop) => stop.stop_id === currentStop.stop_id && stop.stop_name === currentStop.stop_name), }); } return otherLinesMap; } async findRelevantTripIdsFromLines(tripIds, currentStopInput, currentTripId, previousCountMax = 1, isOppositeDirectionFilterStopNameStrict = true) { if (!tripIds.size) { return []; } const allTripIds = new Set(tripIds); allTripIds.add(currentTripId); const gtfsTripStopsMap = await this.gtfsTripStopsRepository.getMultipleTripStops([...allTripIds]); const rawLinesMap = new Map(); for (const [tripId, dto] of gtfsTripStopsMap.entries()) { if (dto?.stops?.length) { rawLinesMap.set(tripId, dto.stops); } } const otherLinesMap = TransferFacade_1.rawTripLinesEntriesToTransfersMap(currentStopInput, currentTripId, rawLinesMap); const currentLine = otherLinesMap.get(currentTripId) || null; if (!currentLine?.stops.length) { throw new golemio_errors_1.GeneralError(`Cannot find data in delay cache for current trip '${currentTripId}'`, this.constructor.name); } // remove current trip from the lines map otherLinesMap.delete(currentTripId); if (!otherLinesMap.size) { return []; } const filters = [ TransferFacade_1.filterOutForwardSubgroupOfLine, // keep transfers with unique stops to my current stop TransferFacade_1.filterOutLinesWithBacktrackingConflict, // keep oncoming traffic going to my previous stop ]; const data = { currentStop: currentLine.stops[currentLine.currentStopIndex], currentLine, otherLinesMap, tripsToKeep: new Set(), }; const options = { previousCountMax, isOppositeDirectionFilterStopNameStrict, }; const params = { data, options }; const results = await TransferFacade_1.applyFiltersSequentially(params, filters); const tripIdsToKeep = results.data.tripsToKeep; return Array.from(tripIdsToKeep.values()); } /** * Fallback options as defined by ROPID */ get defaultOptions() { return { limit: 16, total: 16, offset: 0, mode: DepartureMode_1.DepartureMode.DEPARTURES, order: DepartureOrder_1.DepartureOrder.REAL, filter: DepartureFilter_1.DepartureFilter.ROUTE_HEADING_ONCE_NOGAP_FILL, skip: [DepartureSkip_1.DepartureSkip.CANCELED], departuresDirections: [], runScheduleMap: null, // not used in this context untrackedTrips: null, // not used in this context }; } }; exports.TransferFacade = TransferFacade; TransferFacade.DEPARTURES_LIMIT_LOWER = 80; TransferFacade.DEPARTURES_LIMIT_UPPER = 120; TransferFacade.MINUTE_AS_MS = 60 * 1000; TransferFacade.OPEN_MINUTES_IN_PAST = 61; TransferFacade.OPEN_MINUTES_IN_PAST_MS = TransferFacade_1.OPEN_MINUTES_IN_PAST * TransferFacade_1.MINUTE_AS_MS; exports.TransferFacade = TransferFacade = TransferFacade_1 = __decorate([ (0, tsyringe_1.injectable)(), __param(0, (0, tsyringe_1.inject)(CoreToken_1.CoreToken.Logger)), __param(1, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicGtfsDepartureRepository)), __param(2, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.DeparturesRepository)), __param(3, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.TransferDepartureTransformation)), __param(4, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicVehiclePositionsRepository)), __param(5, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicStopTimeRepository)), __param(6, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.DelayComputationRepository)), __param(7, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.GtfsTripStopsRepository)), __metadata("design:paramtypes", [Object, PublicGtfsDepartureRepository_1.PublicGtfsDepartureRepository, data_access_1.DeparturesRepository, TransferDepartureTransformation_1.TransferDepartureTransformation, PublicVehiclePositionsRepository_1.PublicVehiclePositionsRepository, PublicStopTimeRepository_1.PublicStopTimeRepository, DelayComputationRepository_1.DelayComputationRepository, GtfsTripStopsRepository_1.GtfsTripStopsRepository]) ], TransferFacade); //# sourceMappingURL=TransferFacade.js.map