@golemio/pid
Version:
Golemio PID Module
306 lines • 18.1 kB
JavaScript
;
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