UNPKG

@golemio/pid

Version:
306 lines • 18.1 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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GenerateFilesTask = void 0; const RouteTypeEnums_1 = require("../../../../../helpers/RouteTypeEnums"); const ropid_vymi_1 = require("../../../../ropid-vymi"); const Di_1 = require("../../../ioc/Di"); const VPContainerToken_1 = require("../../../ioc/VPContainerToken"); const TripsRepository_1 = require("../../vehicle-positions/data-access/TripsRepository"); const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken"); const ioc_1 = require("@golemio/core/dist/integration-engine/ioc"); const workers_1 = require("@golemio/core/dist/integration-engine/workers"); const CloudflareCachePurgeWebhook_1 = require("@golemio/core/dist/integration-engine/workers/webhooks/CloudflareCachePurgeWebhook"); const WebhookDecorators_1 = require("@golemio/core/dist/integration-engine/workers/helpers/WebhookDecorators"); const golemio_errors_1 = require("@golemio/core/dist/shared/golemio-errors"); const ovapi_gtfs_realtime_bindings_1 = require("@golemio/ovapi-gtfs-realtime-bindings"); const const_1 = require("../../../../../const"); const GtfsRtRedisRepository_1 = require("../data-access/GtfsRtRedisRepository"); const AlertsGenerator_1 = require("../helpers/AlertsGenerator"); const VehicleDescriptor_1 = require("../helpers/VehicleDescriptor"); const const_2 = require("../helpers/const"); class GenerateFilesTask extends workers_1.AbstractEmptyTask { constructor(queuePrefix) { super(queuePrefix); this.queueName = "generateFiles"; this.queueTtl = 20 * 1000; // 20 seconds this.logger = Di_1.VPContainer.resolve(ioc_1.ContainerToken.Logger); this.stopTimeRepository = Di_1.VPContainer.resolve(VPContainerToken_1.VPContainerToken.PublicStopTimeCacheRepository); this.tripsRepository = new TripsRepository_1.TripsRepository(); this.gtfsRtRedisRepository = new GtfsRtRedisRepository_1.GtfsRtRedisRepository(); const vymiEventsRepository = new ropid_vymi_1.RopidVYMIEventsModel(); const vymiRoutesRepository = new ropid_vymi_1.RopidVYMIEventsRoutesModel(); this.alertsGenerator = new AlertsGenerator_1.AlertsGenerator(this.gtfsRtRedisRepository, vymiEventsRepository, vymiRoutesRepository); this.vehicleDescriptor = new VehicleDescriptor_1.VehicleDescriptor(); } async execute() { const startDateTime = new Date(); const tripData = await this.tripsRepository.findAllForGTFSRt(startDateTime); const feedHeader = this.createFeedHeader(); const updatesMessage = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedMessage.create({ header: feedHeader }); const positionsMessage = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedMessage.create({ header: feedHeader }); const pidFeedMessage = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedMessage.create({ header: feedHeader }); const stopTimesIds = []; for (const tripRecord of tripData) { const { id } = this.vehicleDescriptor.getVehicleDescriptor(tripRecord); if (typeof id === "string") { stopTimesIds.push(`${id}-${tripRecord.gtfs_trip_id}`); } } if (stopTimesIds.length === 0) { this.logger.warn("No stop time ids found for GTFS-RT generation. Most likely no vehicles are running."); return; } const stopTimes = await this.stopTimeRepository.getPublicStopTimeCache(stopTimesIds); const gtfsTripIdMap = {}; const entityTripStarts = []; for (const tripRecord of tripData) { let tripUpdateFeedEntity = undefined; let feedMessageEntity = undefined; const vehicleDescriptor = this.vehicleDescriptor.getVehicleDescriptor(tripRecord); const entityTimestamp = Math.round(new Date(tripRecord.last_position.origin_timestamp).getTime() / 1000); if (tripRecord.last_position.is_canceled && !gtfsTripIdMap[tripRecord.gtfs_trip_id]) { tripUpdateFeedEntity = this.handleCanceledTrip(tripRecord, entityTimestamp, updatesMessage); gtfsTripIdMap[tripRecord.gtfs_trip_id] = true; // For canceled trips we don't want to create positions because they have lng 0 and lat 0 } else { const tripDescriptor = { scheduleRelationship: ovapi_gtfs_realtime_bindings_1.transit_realtime.TripDescriptor.ScheduleRelationship.SCHEDULED, startDate: this.parseUTCDateFromISO(tripRecord.start_timestamp), startTime: tripRecord.start_time, tripId: tripRecord.gtfs_trip_id, routeId: tripRecord.gtfs_route_id, }; let stopTimeUpdates = []; if (typeof vehicleDescriptor.id === "string") { const stopTimesForVehicle = stopTimes.get(`${vehicleDescriptor.id}-${tripRecord.gtfs_trip_id}`); stopTimeUpdates = this.generateStopTimeUpdate(tripRecord.gtfs_route_type, stopTimesForVehicle, tripRecord.last_position); } else { this.logger.error(new golemio_errors_1.GeneralError(`Cannot get stopTimeUpdates for trip with unknown vehicleId and gtfsTripId \ '${tripRecord.gtfs_trip_id}'`, this.constructor.name, undefined, undefined, "pid")); } const shouldSkipTripUpdate = stopTimeUpdates.every((el) => el.scheduleRelationship === ovapi_gtfs_realtime_bindings_1.transit_realtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA || gtfsTripIdMap[tripRecord.gtfs_trip_id]); entityTripStarts.push(tripRecord.start_timestamp); const positionEntity = { id: tripRecord.id, vehicle: { currentStopSequence: tripRecord.last_position.last_stop_sequence, position: { bearing: tripRecord.last_position.bearing, latitude: tripRecord.last_position.lat, longitude: tripRecord.last_position.lng, speed: tripRecord.last_position.speed && parseFloat((tripRecord.last_position.speed / const_2.MPS_TO_KMH).toFixed(2)), }, timestamp: entityTimestamp, trip: tripDescriptor, vehicle: vehicleDescriptor, }, }; const positionsMessageEntity = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedEntity.fromObject(positionEntity); positionsMessage.entity.push(positionsMessageEntity); feedMessageEntity = structuredClone(positionsMessageEntity); if (!shouldSkipTripUpdate) { const tripUpdate = { stopTimeUpdate: stopTimeUpdates, timestamp: entityTimestamp, trip: tripDescriptor, vehicle: vehicleDescriptor, }; const updateEntity = { id: tripRecord.id, tripUpdate, }; const updatesMessageEntity = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedEntity.fromObject(updateEntity); updatesMessage.entity.push(updatesMessageEntity); gtfsTripIdMap[tripRecord.gtfs_trip_id] = true; feedMessageEntity.tripUpdate = tripUpdate; } } if (feedMessageEntity) { pidFeedMessage.entity.push(feedMessageEntity); } else if (tripUpdateFeedEntity) { // If the trip is canceled we want to update just the trip without positions pidFeedMessage.entity.push(tripUpdateFeedEntity); } } this.removeDuplicateVehicleDescriptors(positionsMessage, entityTripStarts); this.removeDuplicateVehicleDescriptors(pidFeedMessage, entityTripStarts, { shouldRemoveVehicleOnly: true }); this.validateAndSaveBuffer("trip_updates", updatesMessage); this.validateAndSaveBuffer("vehicle_positions", positionsMessage); this.validateAndSaveBuffer("pid_feed", pidFeedMessage); await this.alertsGenerator.generateAlerts(feedHeader); } /** * Get rid of vehicle descriptors with duplicate IDs from a given GTFS-RT message * * @param message The message to get rid of duplicate vehicle descriptors from * @param tripData The trip data to get start timestamps from * @param options Options for the duplicate removal process * @param options.removeVehicleOnly Whether to only remove the duplicate vehicle descriptor from the message entity, instead * of removing the whole entity. Defaults to `false`. Note that if an entity with a duplicate vehicle descriptor does not * contain any other valuable data, it will be removed regardless of this option. */ removeDuplicateVehicleDescriptors(message, entityTripStarts, options = { shouldRemoveVehicleOnly: false, }) { if (!message.entity) { return; } const entitiesWithoutVehicleId = []; const entitiesWithoutTheirDuplicateVehicle = []; const entityWithTripStartByVehicleId = new Map(); for (let i = 0; i < message.entity.length; i++) { const entity = message.entity[i]; const vehicleId = entity.vehicle?.vehicle?.id ?? null; const entityTripStart = new Date(entityTripStarts[i]); if (vehicleId === null) { entitiesWithoutVehicleId.push(entity); } else if (!entityWithTripStartByVehicleId.has(vehicleId)) { entityWithTripStartByVehicleId.set(vehicleId, { entity, entityTripStart }); } else { const unusedEntity = this.conditionallyUpdateEntityByVehicleId(entityWithTripStartByVehicleId, vehicleId, entity, entityTripStart); if (options.shouldRemoveVehicleOnly) { const { vehicle = null, ...entityWithoutVehicle } = unusedEntity; if (Object.keys(entityWithoutVehicle).find((key) => key !== "id")) { entitiesWithoutTheirDuplicateVehicle.push(entityWithoutVehicle); } } } } message.entity = entitiesWithoutVehicleId.concat(entitiesWithoutTheirDuplicateVehicle); for (const { entity } of entityWithTripStartByVehicleId.values()) message.entity.push(entity); } /** * Update a given `EntityWithTripStartByVehicleId` Map with given values if the new entity has a lower trip start date/time * and is not canceled. The entity that does not end up in the map (either the new entity if it does not meet the conditions * for the update or the original entity if the new entity does) is returned. * * @param entityWithTripStartByVehicleId The Map to update * @param vehicleId The vehicle ID to use as a key * @param entity The entity to use as part of the new value, if the conditions are met * @param entityTripStart The entity trip start date/time to use as part of the new value, if the conditions are met */ conditionallyUpdateEntityByVehicleId(entityWithTripStartByVehicleId, vehicleId, entity, entityTripStart) { const mapItem = entityWithTripStartByVehicleId.get(vehicleId); const entityTripScheduleRelationship = entity.vehicle?.trip?.scheduleRelationship ?? null; if (entityTripScheduleRelationship !== ovapi_gtfs_realtime_bindings_1.transit_realtime.TripDescriptor.ScheduleRelationship.CANCELED && entityTripStart < mapItem.entityTripStart) { entityWithTripStartByVehicleId.set(vehicleId, { entity, entityTripStart }); return mapItem.entity; } return entity; } generateStopTimeUpdate(routeType, stopTimes, vehiclePosition) { const stopTimeUpdates = []; // TODO this is a temporary solution, we need to fix the underlying issue in the source view // v_public_vehiclepositions_combined_stop_times, the delay prediction is seemingly not working properly // when the vehicle is before the track, see https://gitlab.com/operator-ict/golemio/code/modules/pid/-/issues/371 const shouldFallbackToScheduleTime = this.shouldFallbackToScheduleTime(vehiclePosition); for (const stopTime of stopTimes) { const scheduledTrack = stopTime.platform_code; let actualTrack = stopTime.platform_code; if (routeType === RouteTypeEnums_1.GTFSRouteTypeEnum.TRAIN) { actualTrack = stopTime.cis_stop_platform_code; } const arrivalDelay = shouldFallbackToScheduleTime ? 0 : stopTime.arr_delay ?? stopTime.dep_delay; const departureDelay = shouldFallbackToScheduleTime ? 0 : stopTime.dep_delay ?? stopTime.arr_delay; stopTimeUpdates.push({ arrival: { delay: arrivalDelay, }, departure: { delay: departureDelay, }, ...(arrivalDelay === null && departureDelay === null && { scheduleRelationship: ovapi_gtfs_realtime_bindings_1.transit_realtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA, }), stopSequence: stopTime.sequence, stopId: stopTime.stop_id, ".transit_realtime.ovapiStopTimeUpdate": { ...(scheduledTrack && { scheduledTrack }), ...(actualTrack && { actualTrack }), }, }); } return stopTimeUpdates; } createFeedHeader() { const header = { gtfsRealtimeVersion: "2.0", incrementality: ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedHeader.Incrementality.FULL_DATASET, timestamp: Math.round(new Date().getTime() / 1000), }; return ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedHeader.fromObject(header); } /** * Parse UTC date from ISO timestamp * @example 20230101 */ parseUTCDateFromISO(timestamp) { const startTimestamp = new Date(timestamp); const formattedDate = startTimestamp.getUTCFullYear() + ("0" + (startTimestamp.getUTCMonth() + 1)).slice(-2) + ("0" + startTimestamp.getUTCDate()).slice(-2); return formattedDate; } validateAndSaveBuffer(feedName, message) { const feedMessageVerificationResult = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedMessage.verify(message); if (feedMessageVerificationResult !== null) { this.logger.error(new golemio_errors_1.GeneralError(`Invalid GTFS-RT message for feed '${feedName}'`, this.constructor.name, feedMessageVerificationResult, undefined, "pid")); return; } const buffer = ovapi_gtfs_realtime_bindings_1.transit_realtime.FeedMessage.encode(message).finish(); this.gtfsRtRedisRepository.set(feedName + "_timestamp", message.header.timestamp); this.gtfsRtRedisRepository.hset(feedName + ".pb", buffer.toString("binary")); } shouldFallbackToScheduleTime(vehiclePosition) { return ((vehiclePosition.state_position === const_1.StatePositionEnum.BEFORE_TRACK || vehiclePosition.state_position === const_1.StatePositionEnum.BEFORE_TRACK_DELAYED) && (vehiclePosition.delay === 0 || vehiclePosition.delay === null)); } handleCanceledTrip(tripRecord, timestamp, updatesMessage) { const feedEntity = { id: tripRecord.id, tripUpdate: { stopTimeUpdate: null, timestamp, trip: { scheduleRelationship: ovapi_gtfs_realtime_bindings_1.transit_realtime.TripDescriptor.ScheduleRelationship.CANCELED, startDate: this.parseUTCDateFromISO(tripRecord.start_timestamp), startTime: tripRecord.start_time, tripId: tripRecord.gtfs_trip_id, routeId: tripRecord.gtfs_route_id, }, vehicle: null, }, }; updatesMessage.entity.push(feedEntity); return feedEntity; } } exports.GenerateFilesTask = GenerateFilesTask; GenerateFilesTask.config = Di_1.VPContainer.resolve(CoreToken_1.CoreToken.SimpleConfig); __decorate([ WebhookDecorators_1.WebhookDecorators.after(new CloudflareCachePurgeWebhook_1.CloudflareCachePurgeWebhook(Object.values(GenerateFilesTask.config.getValue("module.pid.cloudflare.cachedApiUrlPaths", {})))), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], GenerateFilesTask.prototype, "execute", null); //# sourceMappingURL=GenerateFilesTask.js.map