From 5b9f323ca8df6d9b1519a92d05aea4ba4de77ac9 Mon Sep 17 00:00:00 2001 From: Christian Knapp Date: Sat, 15 Feb 2025 10:59:42 +0100 Subject: [PATCH] backend: add SequenceController - fetches the coach sequence of a connection --- .../controllers/journey/SequenceController.ts | 50 +++++++++++++ backend/lib/mapping.ts | 53 ++++++++++++- backend/models/sequence.ts | 75 +++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 backend/controllers/journey/SequenceController.ts create mode 100644 backend/models/sequence.ts diff --git a/backend/controllers/journey/SequenceController.ts b/backend/controllers/journey/SequenceController.ts new file mode 100644 index 0000000..ba9e77a --- /dev/null +++ b/backend/controllers/journey/SequenceController.ts @@ -0,0 +1,50 @@ +import { Controller, Get, Queries, Res, Route, Tags, type TsoaResponse } from "tsoa"; +import { DateTime } from "luxon"; +import { mapCoachSequence } from "../../lib/mapping.ts"; + +class SequenceQuery { + lineDetails!: string; + evaNumber!: string; + date!: string; +} + +@Route("journey/sequence") +@Tags("Coach Sequences") +export class SequenceController extends Controller { + @Get() + async getSequenceById( + @Queries() query: SequenceQuery, + @Res() badRequestResponse: TsoaResponse<400, { reason: string }> + ): Promise { + if (query.lineDetails === "") { + return badRequestResponse(400, { reason: "lineDetails must not be empty" }); + } + if (!/^\d+$/.test(query.evaNumber)) { + return badRequestResponse(400, { reason: "evaNumber is not an integer" }); + } + + const dateValidation = DateTime.fromFormat(query.date, "yyyyMMdd"); + if (!dateValidation.isValid) { + return badRequestResponse(400, { reason: `${dateValidation.invalidExplanation}` }); + } + + return fetchCoachSequence(query); + } +} + +const fetchCoachSequence = async (query: SequenceQuery): Promise => { + const request = await fetch(`https://app.vendo.noncd.db.de/mob/zuglaeufe/${query.lineDetails}/halte/by-abfahrt/${query.evaNumber}_${query.date}/wagenreihung`, { + method: "GET", + headers: { + Accept: "application/x.db.vendo.mob.wagenreihung.v3+json", + "Content-Type": "application/x.db.vendo.mob.wagenreihung.v3+json", + "X-Correlation-ID": crypto.randomUUID() + "_" + crypto.randomUUID() + } + }); + + if (!request.ok) { + throw new Error("Failed to fetch coach sequence"); + } + + return mapCoachSequence(await request.json()); +}; \ No newline at end of file diff --git a/backend/lib/mapping.ts b/backend/lib/mapping.ts index 63c2095..1a99274 100644 --- a/backend/lib/mapping.ts +++ b/backend/lib/mapping.ts @@ -4,6 +4,7 @@ import { DateTime } from "luxon"; import type { Stop } from "../models/station.ts"; import type { Message } from "../models/message.ts"; import { mapToProduct } from "../models/products.ts"; +import type { Sequence } from "../models/sequence.ts"; const mapConnection = (entry: any, type: "departures" | "arrivals", profile: "db" | "dbnav"): Connection => { const delay: number = calculateDuration( @@ -96,4 +97,54 @@ const mapMessages = ( }; }; -export { mapConnection }; +const mapCoachSequence = (entry: any): Sequence => { + return { + track: { + start: { position: entry?.gleis?.start?.position }, + end: { position: entry?.gleis?.ende?.position }, + sections: entry?.gleis?.sektoren?.map((rawSection: any) => ({ + name: rawSection?.bezeichnung, + start: { position: rawSection?.start?.position }, + end: { position: rawSection?.ende?.position }, + gleisabschnittswuerfelPosition: rawSection?.gleisabschnittswuerfelPosition, + firstClass: rawSection?.ersteKlasse + })), + name: entry?.gleis?.bezeichnung + }, + vehicleGroup: entry?.fahrzeuggruppen?.map((rawGroup: any) => ({ + vehicles: rawGroup?.fahrzeuge?.map((rawVehicle: any) => ({ + vehicleType: { + category: rawVehicle?.fahrzeugtyp?.fahrzeugkategorie, + model: rawVehicle?.fahrzeugtyp?.baureihe, + firstClass: rawVehicle?.fahrzeugtyp?.ersteKlasse, + secondClass: rawVehicle?.fahrzeugtyp?.zweiteKlasse + }, + status: rawVehicle?.status, + orientation: rawVehicle?.orientierung, + positionOnTrack: { + start: { position: rawVehicle?.positionAmGleis?.start?.position }, + end: { position: rawVehicle?.positionAmGleis?.ende?.position }, + section: rawVehicle?.positionAmGleis?.sektor + }, + equipment: rawVehicle?.ausstattungsmerkmale?.map((rawEquipment: any) => ({ + type: rawEquipment?.art, + status: rawEquipment?.status + })), + orderNumber: rawVehicle?.ordnungsnummer + })), + tripReference: { + type: rawGroup?.fahrtreferenz?.typ, + line: rawGroup?.fahrtreferenz?.linie, + destination: { name: rawGroup?.fahrtreferenz?.ziel.bezeichnung }, + category: rawGroup?.fahrtreferenz?.gattung, + fahrtNr: rawGroup?.fahrtreferenz?.fahrtnummer + }, + designation: rawGroup?.bezeichnung + })), + direction: entry?.fahrtrichtung === "RECHTS" ? "RIGHT" : "LEFT", + plannedTrack: entry?.gleisSoll, + actualTrack: entry?.gleisVorschau + } +} + +export { mapConnection, mapCoachSequence }; diff --git a/backend/models/sequence.ts b/backend/models/sequence.ts new file mode 100644 index 0000000..57ed9e9 --- /dev/null +++ b/backend/models/sequence.ts @@ -0,0 +1,75 @@ +export interface Sequence { + track: Track; + vehicleGroup?: VehicleGroup[]; + direction?: "RIGHT" | "LEFT"; + plannedTrack?: string; + actualTrack?: string; +} + +export interface Track { + start: { position: number }; + end: { position: number }; + sections: Section[] | []; + name: string; // name of the track +} + +export interface Section { + name: string; + start: { position: number }; + end: { position: number }; + gleisabschnittswuerfelPosition: number; // ?????????????????????? + firstClass: boolean; +} + +export interface VehicleGroup { + vehicles: Vehicle[]; + tripReference: TripReference; + designation: string; +} + +export interface TripReference { + type: string; + line: string; + destination: { name: string; }; + category: string; + fahrtNr: string; +} + +export interface Vehicle { + vehicleType: VehicleType; + status: string; + orientation: string; + positionOnTrack: PositionOnTrack; + equipment: Equipment[]; + orderNumber?: number; // ?? +} + +export interface VehicleType { + category: string; + model: string; + firstClass: boolean; + secondClass: boolean; +} + +export interface PositionOnTrack { + start: { position: number }; + end: { position: number }; + section: string; +} + +/** + * SEATS_SEVERELY_DISABLED + * ZONE_FAMILY + * ZONE_QUIET + * INFO + * CABIN_INFANT + * AIR_CONDITION + * TOILET_WHEELCHAIR + * WHEELCHAIR_SPACE + * SEATS_BAHN_COMFORT + * BIKE_SPACE + */ +export interface Equipment { + type: string; + status: string; +} \ No newline at end of file