Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Coach Sequence feature #65

Merged
merged 29 commits into from
Feb 16, 2025
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40c7877
frontend: pass VITE_BACKEND_BASE_URL via docker
schmolldechse Feb 14, 2025
b04ce91
backend: standardized redis password to match with users.acl
schmolldechse Feb 14, 2025
d95c33d
add default nginx config
schmolldechse Feb 14, 2025
0bb28fb
frontend: made goto async
schmolldechse Feb 14, 2025
97e6887
frontend: pass VITE_BACKEND_DOCKER_BASE_URL via docker (for SvelteKit…
schmolldechse Feb 14, 2025
de48571
backend: pass redis env via docker
schmolldechse Feb 14, 2025
318921f
frontend: fixes Filter being not at the bottom
schmolldechse Feb 14, 2025
2d0e0e5
backend: add documentation
schmolldechse Feb 14, 2025
b19bcfe
backend: updated documentation url path for better NGINX integration
schmolldechse Feb 14, 2025
e147813
backend: fixes evaNumber being not an integer
schmolldechse Feb 15, 2025
5ba5e11
backend: fixes export, better error message handling
schmolldechse Feb 15, 2025
5b9f323
backend: add SequenceController
schmolldechse Feb 15, 2025
a40dc78
backend: add model descriptions to Sequence
schmolldechse Feb 15, 2025
b3640b6
frontend: add journey/coach-sequence route
schmolldechse Feb 15, 2025
c43b05b
frontend: really fixed Filter being at the bottom this time
schmolldechse Feb 16, 2025
36d8967
frontend: fixed size of SVGs
schmolldechse Feb 16, 2025
6dd76f6
frontend: added Controlcars to Sequence
schmolldechse Feb 16, 2025
18ecca2
frontend: added first class decoration to coaches
schmolldechse Feb 16, 2025
609bc14
backend: changed fahrtNr type to number
schmolldechse Feb 16, 2025
0d9241a
frontend: added infos about trip
schmolldechse Feb 16, 2025
6545182
frontend: added Line Reference in MetaTags
schmolldechse Feb 16, 2025
7483ba0
backend: add BISTRO equipment
schmolldechse Feb 16, 2025
abce53c
frontend: changed directory of icons
schmolldechse Feb 16, 2025
1510ca1
backend: added POWERCAR & PASSENGERCARRIAGE in VehicleType
schmolldechse Feb 16, 2025
08279e4
frontend: added VehicleInfo by clicking on a Vehicle
schmolldechse Feb 16, 2025
a91991e
ci: lint only on PR's
schmolldechse Feb 16, 2025
dbfccf8
backend: add possibility to download swagger.json
schmolldechse Feb 16, 2025
5cf4e33
frontend & backend: use svelte env goofy ah
schmolldechse Feb 16, 2025
3b2574e
chore: linting
schmolldechse Feb 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Specify the backend URL which is used in the frontend. You can not adjust this to the backend container_name as it is not reachable from the frontend!
# Do not add a "/" at the end, as the frontend will add the path to the API endpoint itself.
VITE_BACKEND_BASE_URL=http://localhost:8000

# Specify the backend URL which is used in the frontend for SvelteKit SSR. Running in a docker container, you should use the container_name of the backend service.
# Do not add a "/" at the end, as the frontend will add the path to the API endpoint itself.
VITE_BACKEND_DOCKER_BASE_URL=http://navigator-backend:8000

# Redis configuration
REDIS_HOST=navigator-redis
REDIS_PORT=6379
REDIS_DB=0
REDIS_USER=navigator-stations
REDIS_PASSWORD=password
2 changes: 0 additions & 2 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Linting Checks

on:
push:
branches: ["*"]
pull_request:
branches: ["*"]
workflow_dispatch:
Expand Down
17 changes: 7 additions & 10 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# Use the official Bun image as the base image
FROM oven/bun:latest

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files first to leverage Docker layer caching
COPY package.json bun.lockb* ./

# Install dependencies
RUN bun install

# Copy the rest of your application code (all files and folders)
COPY . .

ENV REDIS_HOST=navigator-redis
ENV REDIS_PORT=6379
ENV REDIS_DB=0
ENV REDIS_USER=navigator-stations
ENV REDIS_PASSWORD=password

RUN bun tsoa

# Expose port 8000 (the port your app listens on)
EXPOSE 8000

# Start the application (adjust the entry point file if necessary)
CMD ["bun", "run", "server.ts"]
13 changes: 7 additions & 6 deletions backend/controllers/StationsController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type Station } from "../models/station.ts";
import { mapToEnum, Products } from "../models/products.ts";
import { Controller, Get, Path, Query, Res, Route, type TsoaResponse } from "tsoa";
import { Controller, Get, Path, Query, Res, Route, Tags, type TsoaResponse } from "tsoa";
import { getRedisClient } from "../lib/redis.ts";

@Route("stations")
@Tags("Stations")
export class StationController extends Controller {
@Get()
async queryStations(
Expand All @@ -19,11 +20,11 @@ export class StationController extends Controller {

@Get("/{evaNumber}")
async getStationByEvaNumber(
@Path() evaNumber: number,
@Path() evaNumber: string,
@Res() badRequestResponse: TsoaResponse<400, { reason: string }>
): Promise<Station> {
if (!Number.isInteger(evaNumber)) {
return badRequestResponse(400, { reason: "EvaNumber is not an integer" });
if (!/^\d+$/.test(evaNumber)) {
return badRequestResponse(400, { reason: "evaNumber is not an integer" });
}

const cachedStation = await getCachedStation(evaNumber.toString());
Expand Down Expand Up @@ -52,12 +53,12 @@ const fetchStation = async (searchTerm: string): Promise<Station[]> => {
});

if (!request.ok) {
throw new Error("HTTP Request error occurred");
throw new Error(`Failed to fetch stations for ${searchTerm}`);
}

const response = await request.json();
if (!response || !Array.isArray(response)) {
throw new Error("Invalid response format");
throw new Error(`Response was expected to be an array, but got ${typeof response}`);
}

return response.map((data: any) => ({
Expand Down
50 changes: 50 additions & 0 deletions backend/controllers/journey/SequenceController.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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<any> => {
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());
};
3 changes: 2 additions & 1 deletion backend/controllers/timetable/BahnhofController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type RequestType, retrieveBahnhofJourneys } from "./requests.ts";
import { Controller, Get, Queries, Route } from "tsoa";
import { Controller, Get, Queries, Route, Tags } from "tsoa";
import type { Journey } from "../../models/connection.ts";

class BahnhofQuery {
Expand All @@ -10,6 +10,7 @@ class BahnhofQuery {
}

@Route("timetable/bahnhof")
@Tags("Bahnhof")
export class BahnhofController extends Controller {
@Get()
async getBahnhofJourneys(@Queries() query: BahnhofQuery): Promise<Journey[]> {
Expand Down
3 changes: 2 additions & 1 deletion backend/controllers/timetable/CombinedController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RequestType, retrieveBahnhofJourneys, retrieveCombinedConnections } fro
import type { Connection, Journey } from "../../models/connection.ts";
import { isMatching } from "../../lib/merge.ts";
import calculateDuration from "../../lib/time.ts";
import { Controller, Get, Queries, Res, Route, type TsoaResponse } from "tsoa";
import { Controller, Get, Queries, Res, Route, Tags, type TsoaResponse } from "tsoa";

class CombinedQuery {
evaNumber!: string;
Expand All @@ -15,6 +15,7 @@ class CombinedQuery {
}

@Route("timetable/combined")
@Tags("Combined")
export class CombinedController extends Controller {
@Get()
async getCombinedJourneys(
Expand Down
3 changes: 2 additions & 1 deletion backend/controllers/timetable/VendoController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DateTime } from "luxon";
import { Profile, RequestType, retrieveCombinedConnections, retrieveConnections } from "./requests.ts";
import { Controller, Get, Queries, Res, Route, type TsoaResponse } from "tsoa";
import { Controller, Get, Queries, Res, Route, Tags, type TsoaResponse } from "tsoa";
import type { Journey } from "../../models/connection.ts";

class VendoQuery {
Expand All @@ -13,6 +13,7 @@ class VendoQuery {
}

@Route("timetable/vendo")
@Tags("Vendo")
export class VendoController extends Controller {
@Get()
async getVendoJourneys(
Expand Down
2 changes: 1 addition & 1 deletion backend/controllers/timetable/requests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Connection, Journey } from "../../models/connection.ts";
import mapConnection from "../../lib/mapping.ts";
import { DateTime } from "luxon";
import { mergeConnections } from "../../lib/merge.ts";
import { mapConnection } from "../../lib/mapping.ts";

export enum RequestType {
DEPARTURES = "departures",
Expand Down
53 changes: 52 additions & 1 deletion backend/lib/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -96,4 +97,54 @@ const mapMessages = (
};
};

export default 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 };
85 changes: 85 additions & 0 deletions backend/models/sequence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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: number;
}

export interface Vehicle {
vehicleType: VehicleType;
status: string;
orientation: string;
positionOnTrack: PositionOnTrack;
equipment: Equipment[];
orderNumber?: number; // coach number
}

/**
* category might be:
*
* POWERCAR
* LOCOMOTIVE
* DOUBLEDECK_*
* PASSENGERCARRIAGE_*
*/
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
* ZONE_MULTI_PURPOSE
* BISTRO
*/
export interface Equipment {
type: string;
status: string;
}
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "backend",
"name": "Navigator Backend",
"module": "server.ts",
"type": "module",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ app.use(express.json());
app.use(cors({ origin: "*" }));

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.get("/api-spec", (req, res) => res.download("./build/swagger.json"));
app.get("/api", (req, res) => res.redirect("/api-docs"));

RegisterRoutes(app);

Expand Down
Loading
Loading