commit d325d40aafa264af0d969146f90a2b19e15989a4
parent ff8d980e2e60752b16286f35863e7d99114bac5f
Author: Katja (ctucx) <git@ctu.cx>
Date: Thu, 30 Jan 2025 11:27:45 +0100
parent ff8d980e2e60752b16286f35863e7d99114bac5f
Author: Katja (ctucx) <git@ctu.cx>
Date: Thu, 30 Jan 2025 11:27:45 +0100
coach-sequence: refactor, add attribution for bahn-expert code
23 files changed, 779 insertions(+), 975 deletions(-)
A
|
199
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
211
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
109
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
181
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
D
|
220
-------------------------------------------------------------------------------
D
|
164
-------------------------------------------------------------------------------
D
|
203
-------------------------------------------------------------------------------
D
|
104
-------------------------------------------------------------------------------
D
|
176
-------------------------------------------------------------------------------
diff --git a/flake.nix b/flake.nix @@ -15,7 +15,7 @@ name = "trainsearch"; src = self; - npmDepsHash = "sha256-Ce0opId1cDrcO77kjCoUs7sIzTFQFlrkBRoaSTg7sIQ="; + npmDepsHash = "sha256-anEajLsr+iiZ1nYLBerc7VqJops5Ln2mn9EN+x1zVts="; makeCacheWritable = true; npmBuildScript = "build";
diff --git a/package-lock.json b/package-lock.json @@ -11,8 +11,6 @@ "dependencies": { "assert": "^2.1.0", "buffer": "^6.0.3", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", "db-vendo-client": "https://github.com/yuyuyureka/db-vendo-client#main", "hafas-client": "https://github.com/yu-re-ka/hafas-client#main", "idb": "^8.0.1", @@ -3916,25 +3914,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", - "license": "MIT", - "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" - } - }, "node_modules/db-hafas-stations": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/db-hafas-stations/-/db-hafas-stations-1.0.0.tgz",
diff --git a/package.json b/package.json @@ -12,25 +12,23 @@ "dependencies": { "assert": "^2.1.0", "buffer": "^6.0.3", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", "db-vendo-client": "https://github.com/yuyuyureka/db-vendo-client#main", "hafas-client": "https://github.com/yu-re-ka/hafas-client#main", "idb": "^8.0.1", "lit-html": "^3.2.1" }, "devDependencies": { - "webpack": "^5.97.1", - "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.0", + "@principalstudio/html-webpack-inject-preload": "^1.2.7", "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^7.0.0", "git-revision-webpack-plugin": "^5.0.0", "html-webpack-plugin": "^5.6.3", - "workbox-webpack-plugin": "^7.3.0", - "@principalstudio/html-webpack-inject-preload": "^1.2.7", - "style-loader": "^4.0.0", - "css-loader": "^7.1.2", "mini-css-extract-plugin": "^2.9.2", - "css-minimizer-webpack-plugin": "^7.0.0" + "style-loader": "^4.0.0", + "webpack": "^5.97.1", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.0", + "workbox-webpack-plugin": "^7.3.0" } }
diff --git a/src/canvas.js b/src/canvas.js @@ -2,7 +2,7 @@ import { moreJourneys } from './journeysView.js'; import { go } from './router.js'; import { padZeros } from './helpers.js'; import { formatTrainTypes, formatLineDisplayName } from './formatters.js' -import { cachedCoachSequence, coachSequenceCache, coachSequenceCacheKey } from './reihung/index.js'; +import { cachedCoachSequence, coachSequenceCache, coachSequenceCacheKey } from './coach-sequence/index.js'; const formatTime = (date) => { return `${padZeros(date.getHours())}:${padZeros(date.getMinutes())}`;
diff --git a/src/coach-sequence/DB/DBMapping.js b/src/coach-sequence/DB/DBMapping.js @@ -0,0 +1,199 @@ +// This code is mostly from marudor's great bahn.expert project. +// See here: https://github.com/marudor/bahn.expert/tree/main/src/server/coachSequence +// Since the source is MIT licensed, to following code is it too. +// + +import { enrichCoachSequence } from './commonMapping.js'; +import { getLineFromNumber } from './lineNumberMapping.js'; + +const mapSectors = (sectors, basePercent) => sectors?.map((s) => ({ + name: s.name, + position: { + startPercent: basePercent * s.start, + endPercent: basePercent * s.end, + }, +}) || []); + +const mapStop = (evaNumber, platform) => { + if (platform?.start === undefined || platform.end === undefined) { + return [undefined, 0]; + } + + const basePercent = 100 / (platform.end - platform.start); + + return [ + { + stopPlace: { + evaNumber, + name: '', + }, + sectors: mapSectors(platform.sectors, basePercent), + }, + basePercent, + ]; +}; + +const mapClass = (vehicleType) => { + switch (vehicleType.category) { + case 'LOCOMOTIVE': + case 'POWERCAR': + return 4; + case 'DININGCAR': + return 2; + } + + if (vehicleType.hasFirstClass && vehicleType.hasEconomyClass) return 3; + if (vehicleType.hasFirstClass) return 1; + if (vehicleType.hasEconomyClass) return 2; + + return 0; +} + +const diningCategories = new Set([ + 'DININGCAR', + 'HALFDININGCAR_ECONOMY_CLASS', + 'HALFDININGCAR_FIRST_CLASS', +]); + +const mapFeatures = (vehicle) => { + const features = {}; + + for (const a of vehicle.amenities) { + switch (a.type) { + case 'BIKE_SPACE': { + features.bike = true; + break; + } + case 'BISTRO': { + features.dining = true; + break; + } + case 'INFO': { + features.info = true; + break; + } + case 'SEATS_BAHN_COMFORT': { + features.comfort = true; + break; + } + case 'SEATS_SEVERELY_DISABLED': { + features.disabled = true; + break; + } + case 'WHEELCHAIR_SPACE': { + features.wheelchair = true; + break; + } + case 'WIFI': { + features.wifi = true; + break; + } + case 'ZONE_FAMILY': { + features.family = true; + break; + } + case 'ZONE_QUIET': { + features.quiet = true; + break; + } + case 'CABIN_INFANT': { + features.toddler = true; + break; + } + } + } + + if (!features.dining && diningCategories.has(vehicle.type.category)) { + features.dining = true; + } + + return features; +} + +const mapVehicle = (vehicle, basePercent) => { + + if (!vehicle.platformPosition) return undefined; + + return { + identificationNumber: vehicle.wagonIdentificationNumber?.toString(), + uic: vehicle.vehicleID, + type: vehicle.type.constructionType, + class: mapClass(vehicle.type), + vehicleCategory: vehicle.type.category, + closed: + vehicle.status === 'CLOSED' || + vehicle.type.category === 'LOCOMOTIVE' || + vehicle.type.category === 'POWERCAR', + position: { + startPercent: basePercent * vehicle.platformPosition.start, + endPercent: basePercent * vehicle.platformPosition.end, + }, + features: mapFeatures(vehicle), + }; +} + +const mapGroup = (group, basePercent) => { + const coaches = group.vehicles.map(vehicle => mapVehicle(vehicle, basePercent)); + + if (coaches.includes(undefined)) return undefined; + + return { + name: group.name, + destinationName: group.transport.destination.name, + originName: 'UNKNOWN', + number: group.transport.number.toString(), + coaches: coaches, + }; +} + +const mapDirection = coaches => { + const first = coaches[0]; + const last = coaches.at(-1); + + return last.position.startPercent > first.position.startPercent; +} + +const mapSequence = async (sequence, basePercent) => { + const groups = await Promise.all( + sequence.groups.map((g) => mapGroup(g, basePercent)), + ); + + if (groups.includes(undefined)) return undefined; + + return { + groups: groups, + }; +} + +export const mapInformation = async (upstreamSequence, trainCategory, trainNumber, evaNumber) => { + if (!upstreamSequence) return undefined; + + const [stop, basePercent] = mapStop(evaNumber, upstreamSequence.platform); + + if (!stop) return undefined; + + const sequence = await mapSequence(upstreamSequence, basePercent); + + if (!sequence) return undefined; + + const allCoaches = sequence.groups.flatMap((g) => g.coaches); + + const information = { + source: 'DB-bahnde', + product: { + number: trainNumber, + type: trainCategory, + }, + isRealtime: allCoaches.every( + (c) => c.uic || c.vehicleCategory === 'LOCOMOTIVE', + ), + stop, + sequence, + direction: mapDirection(allCoaches), + journeyId: upstreamSequence.journeyID, + }; + + enrichCoachSequence(information); + + return information; +};
diff --git a/src/coach-sequence/DB/baureihe.js b/src/coach-sequence/DB/baureihe.js @@ -0,0 +1,211 @@ +// +// This code is mostly from marudor's great bahn.expert project. +// See here: https://github.com/marudor/bahn.expert/tree/main/src/server/coachSequence +// Since the source is MIT licensed, to following code is it too. +// + +import notRedesigned from './notRedesigned.json'; + +export const nameMap = { + '401': 'ICE 1 (BR401)', + '401.9': 'ICE 1 Kurz (BR401)', + '401.LDV': 'ICE 1 Modernisiert (BR401)', + '402': 'ICE 2 (BR402)', + '403': 'ICE 3 (BR403)', + '403.S1': 'ICE 3 (BR403 1. Serie)', + '403.S2': 'ICE 3 (BR403 2. Serie)', + '403.R': 'ICE 3 (BR403)', + '406': 'ICE 3 (BR406)', + '406.R': 'ICE 3 (BR406 Redesign)', + '407': 'ICE 3 Velaro (BR407)', + '408': 'ICE 3neo (BR408)', + '410.1': 'ICE S (BR410.1)', + '411': 'ICE T (BR411)', + '411.S1': 'ICE T (BR411 1. Serie)', + '411.S2': 'ICE T (BR411 2. Serie)', + '412': 'ICE 4 (BR412)', + '412.7': 'ICE 4 Kurz (BR412)', + '412.13': 'ICE 4 Lang (BR412)', + '415': 'ICE T Kurz (BR415)', + 'IC2.TRE': 'IC 2 (TRE)', + '4110': 'IC 2 KISS (BR4110)', + '4010': 'IC 2 KISS (BR4010)', + MET: 'MET', + TGV: 'TGV', +}; + +const getATBR = (code, _serial, _coaches) => { + switch (code) { + case '4011': + return { + baureihe: '411', + identifier: '411.S1' + }; + } +}; + +const getDEBR = (code, uicOrdnungsnummer, coaches, tzn) => { + switch (code) { + case '0812': + case '1412': + case '1812': + case '2412': + case '2812': + case '3412': + case '4812': + case '5812': + case '6412': + case '6812': + case '7412': + case '7812': + case '8812': + case '9412': + case '9812': { + let identifier; + switch (coaches.length) { + case 13: { + identifier = '412.13'; + break; + } + case 7: { + identifier = '412.7'; + break; + } + } + return { + identifier, + baureihe: '412', + }; + } + case '5401': + case '5801': + case '5802': + case '5803': + case '5804': { + let identifier; + if (coaches.length === 11) { + identifier = + coaches.filter((f) => f.class === 1).length === 2 + ? '401.LDV' + : '401.9'; + } + return { + identifier, + baureihe: '401', + }; + } + case '5402': + case '5805': + case '5806': + case '5807': + case '5808': { + return { + baureihe: '402', + identifier: '402', + }; + } + case '5403': { + // const identifier: AvailableIdentifier = `403.S${ + // Number.parseInt(uicOrdnungsnummer.slice(1), 10) <= 37 ? '1' : '2' + // }`; + return { + baureihe: '403', + identifier: '403.R', + }; + } + case '5406': { + return { + baureihe: '406', + identifier: tzn?.endsWith('4651') ? '406.R' : '406', + }; + } + case '5407': { + return { + baureihe: '407', + identifier: '407', + }; + } + case '5410': { + return { + baureihe: '410.1', + identifier: '410.1', + }; + } + case '5408': { + return { + baureihe: '408', + identifier: '408', + }; + } + case '5411': { + return { + baureihe: '411', + identifier: `411.S${ + Number.parseInt(uicOrdnungsnummer, 10) <= 32 ? '1' : '2' + }`, + }; + } + case '5415': { + return { + baureihe: '415', + identifier: '415', + }; + } + case '5475': { + return { + identifier: 'TGV', + }; + } + } +}; + +export const getBaureiheByUIC = (uic, coaches, tzn) => { + const country = uic.slice(2, 4); + const code = uic.slice(4, 8); + const serial = uic.slice(8, 11); + let br; + switch (country) { + case '80': { + br = getDEBR(code, serial, coaches, tzn); + break; + } + case '81': { + br = getATBR(code, serial, coaches); + break; + } + } + if (!br) return undefined; + + return { + ...br, + name: nameMap[br.identifier || br.baureihe], + }; +}; + +export const getBaureiheByCoaches = coaches => { + let identifier; + for (const c of coaches) { + if (c.type === 'Apmbzf') { + identifier = 'MET'; + break; + } + if (c.type === 'DBpbzfa') { + identifier = 'IC2.TRE'; + break; + } + if (c.uic?.slice(4, 8) === '4110') { + identifier = '4110'; + break; + } + if (c.uic?.slice(4, 8) === '4010') { + identifier = '4010'; + break; + } + } + if (identifier) { + return { + identifier, + name: nameMap[identifier], + }; + } +};
diff --git a/src/coach-sequence/DB/commonMapping.js b/src/coach-sequence/DB/commonMapping.js @@ -0,0 +1,109 @@ +// This code is mostly from marudor's great bahn.expert project. +// See here: https://github.com/marudor/bahn.expert/tree/main/src/server/coachSequence +// Since the source is MIT licensed, to following code is it too. +// + +import { getBaureiheByCoaches, getBaureiheByUIC } from './baureihe.js'; +import { getSeatsForCoach } from './specialSeats.js'; +import TrainNames from './TrainNames.js'; + +const hasNonLokCoach = group => group.coaches.some(c => c.category !== 'LOK' && c.category !== 'TRIEBKOPF'); + +export function enrichCoachSequence(coachSequence) { + let prevGroup; + + for (const group of coachSequence.sequence.groups) { + enrichCoachSequenceGroup(group, coachSequence.product); + + if (!hasNonLokCoach(group)) { + continue; + } + + if (prevGroup && prevGroup.destinationName !== group.destinationName) { + coachSequence.multipleDestinations = true; + } + + if (prevGroup && prevGroup.number !== group.number) { + coachSequence.multipleTrainNumbers = true; + } + + prevGroup = group; + } +} +const allowedBR = ['IC', 'EC', 'ICE', 'ECE']; +const tznRegex = /(\d+)/; + +function enrichCoachSequenceGroup(group, product) { + // https://inside.bahn.de/entstehung-zugnummern/?dbkanal_006=L01_S01_D088_KTL0006_INSIDE-BAHN-2019_Zugnummern_LZ01 + const trainNumberAsNumber = Number.parseInt(group.number, 10); + + if (trainNumberAsNumber >= 9550 && trainNumberAsNumber <= 9599) { + group.coaches.forEach(c => { + if (c.features.comfort) { + c.features.comfort = false; + } + }); + } + + if (allowedBR.includes(product.type)) { + let tzn; + + if (group.name.startsWith('IC')) { + tzn = tznRegex.exec(group.name)?.[0]; + group.trainName = TrainNames(tzn); + } + + group.baureihe = calculateBR(group.coaches, tzn); + + + if (group.baureihe) { + if ( + group.baureihe.identifier === '401.LDV' || + group.baureihe.identifier === '401.9' + ) { + // Schwerbehindertenplätze/Vorrangplätze sind in Wagen 11, nicht 12 + for (const coach of group.coaches) { + if (coach.identificationNumber === '11') { + coach.features.disabled = true; + } + if (coach.identificationNumber === '12') { + coach.features.disabled = false; + } + } + } + if ( + group.baureihe.identifier === '4010' || + group.baureihe.identifier === '4110' + ) { + for (const c of group.coaches) { + switch (c.identificationNumber) { + case '6': { + c.features.disabled = true; + break; + } + case '5': { + c.features.disabled = true; + break; + } + case '4': { + c.features.disabled = false; + } + } + } + } + for (const c of group.coaches) { + //c.seats = getSeatsForCoach(c, group.baureihe.identifier); + } + } + } +} + +function calculateBR(coaches, tzn) { + for (const c of coaches) { + if (!c.uic) continue; + const br = getBaureiheByUIC(c.uic, coaches, tzn); + if (br) return br; + } + + return getBaureiheByCoaches(coaches); +}
diff --git a/src/coach-sequence/DB/lineNumberMapping.js b/src/coach-sequence/DB/lineNumberMapping.js @@ -0,0 +1,11 @@ +// This code is mostly from marudor's great bahn.expert project. +// See here: https://github.com/marudor/bahn.expert/tree/main/src/server/coachSequence +// Since the source is MIT licensed, to following code is it too. +// + +import lines from './lines.json'; + +export function getLineFromNumber(journeyNumber) { + if (!journeyNumber) return undefined; + return lines[journeyNumber]; +}
diff --git a/src/coach-sequence/DB/specialSeats.js b/src/coach-sequence/DB/specialSeats.js @@ -0,0 +1,180 @@ +// This code is mostly from marudor's great bahn.expert project. +// See here: https://github.com/marudor/bahn.expert/tree/main/src/server/coachSequence +// Since the source is MIT licensed, to following code is it too. +// + +export function getComfortSeats(identifier, klasse) { + switch (identifier) { + case '401': + return klasse === 1 ? '11-36' : '11-57'; + + case '401.9': + case '401.LDV': + return klasse === 1 ? '12-31' : '11-44'; + + case '402': + return klasse === 1 ? '11-16, 21, 22' : '81-108'; + + case '403': + case '403.S1': + case '403.S2': + case '406': + return klasse === 1 ? '12-26' : '11-38'; + + case '403.R': + case '406.R': + return klasse === 1 ? '12-26' : '11-37'; + + case '407': + return klasse === 1 ? '21-26, 31, 33, 35' : '31-55, 57'; + + case '411': + return klasse === 1 ? '46, 52-56' : '92, 94, 96, 98, 101-118'; + + case '412.7': + return klasse === 1 ? '41, 44-53' : '11-44'; + + case '412': + case '412.13': + return klasse === 1 ? '11-46' : '11-68'; + + case '415': + return klasse === 1 ? '52, 54, 56' : '81-88, 91-98'; + + case 'MET': + return klasse === 1 ? '61-66' : '91-106'; + + case 'IC2.TWIN': + return klasse === 1 ? '73, 75, 83-86' : '31-38, 41-45, 47'; + + case 'IC2.KISS': + return klasse === 3 ? '144, 145' : '55-68'; + } +} +export function getDisabledSeats(identifier, klasse, wagenordnungsnummer) { + switch (identifier) { + case '401': + return klasse === 1 ? '51, 52, 53, 55' : '111-116'; + + case '401.9': + case '401.LDV': + return klasse === 1 ? '11, 13, 15' : '11, 13, 111-116'; + + case '402': + return klasse === 1 ? '12, 21' : '81, 85-88'; + + case '403': + case '403.R': + case '403.S1': + case '403.S2': + case '406': + if (klasse === 1) return '64, 66'; + + if (wagenordnungsnummer === '25' || wagenordnungsnummer === '35') { + // redesign slighlty different + return ['403R', '403.S1R', '403.S2R'].includes(identifier) ? '61, 63, 65-67' : '61, 63, 65, 67'; + } + + return '106, 108'; + + case '407': + if (klasse === 1) return '13, 15'; + + if (wagenordnungsnummer === '11' || wagenordnungsnummer === '21') { + return '11-18'; + } + + return '28, 33-34'; + + case '411': + return klasse === 1 ? '21, 22' : '15-18'; + + case '412.7': + return klasse === 1 ? '12, 13' : '11-18'; + + case '412': + case '412.13': + if (klasse === 1) return wagenordnungsnummer === '10' ? '12, 13' : '11, 14, 21'; + + switch (wagenordnungsnummer) { + case '1': + return '11-24'; + + case '8': + return '11, 12'; + + case '9': + return '41, 45, 46'; + } + + break; + + case '415': + return klasse === 1 ? '21' : '15, 17'; + + case 'MET': + return klasse === 1 ? '16, 21' : '12, 14, 16'; + + case 'IC2.TWIN': + return klasse === 1 ? '21, 71' : '25, 101-105, 171-173'; + + case 'IC2.KISS': + return klasse === 3 ? '143' : '21-26'; + } +} +export function getFamilySeats(identifier) { + switch (identifier) { + case '401': + return '81-116'; + + case '401.9': + case '401.LDV': + return '91-116'; + + case '402': + return '61-78'; + + case '403': + case '403.R': + case '403.S1': + case '403.S2': + case '406': + case '406.R': + return '11-28'; + + case '407': + return '11-28'; + + case '411': + return '11-18, 31-38'; + + case '412.7': + return '61-78'; + + case '412': + case '412.13': + return '61-78'; + + case 'MET': + return '11-26'; + + case 'IC2.TWIN': + return '121, 123, 131-138'; + + case 'IC2.KISS': + return '42, 43, 45, 46, 52-56'; + } +} +export function getSeatsForCoach(coach, identifier) { + const family = coach.features.family ? getFamilySeats(identifier) : undefined; + const disabled = coach.features.disabled ? getDisabledSeats(identifier, coach.class, coach.identificationNumber) : undefined; + const comfort = coach.features.comfort ? getComfortSeats(identifier, coach.class) : undefined; + + if (family || disabled || comfort) { + return { + comfort, + disabled, + family + }; + } +}+ \ No newline at end of file
diff --git a/src/coach-sequence/index.js b/src/coach-sequence/index.js @@ -0,0 +1,55 @@ +import { padZeros, sleep } from '../helpers.js'; +import { mapInformation } from './DB/DBMapping.js'; + +const dbCoachSequenceTimeout = 1000; +export const coachSequenceCache = {}; + +const rawDBCoachSequence = async (category, number, evaNumber, date, retry = 2) => { + try { + const searchParams = new URLSearchParams(); + + searchParams.append("category", category); + searchParams.append("date", `${date.getFullYear()}-${padZeros(date.getMonth()+1)}-${padZeros(date.getDate())}`); + searchParams.append("time", `${date.getFullYear()}-${padZeros(date.getMonth()+1)}-${padZeros(date.getDate())}T${padZeros(date.getHours())}:${padZeros(date.getMinutes())}:${padZeros(date.getSeconds())}Z`); + searchParams.append("evaNumber", evaNumber); + searchParams.append("number", number); + + + return await fetch(`/db/vehicle-sequence?${searchParams}`).then(x => x.json()); + } catch (e) { + sleep(dbCoachSequenceTimeout); + if (retry) return rawDBCoachSequence(category, number, evaNumber, date, retry - 1); + } +} + +const DBCoachSequence = async (category, number, evaNumber, date) => { + const rawSequence = await rawDBCoachSequence(category, number, evaNumber, date); + + if (!rawSequence) return undefined; + + return mapInformation(rawSequence, category, number, evaNumber); +} + +export const coachSequenceCacheKey = (category, number, evaNumber, departure) => { + if (!category || !number || !evaNumber || !departure) return; + return `${category}-${number}-${evaNumber}-${departure.toISOString()}`; +}; + +export const cachedCoachSequence = (category, number, evaNumber, departure) => { + const key = coachSequenceCacheKey(category, number, evaNumber, departure); + + if (!key) return; + + if (coachSequenceCache[key] === undefined) { + coachSequenceCache[key] = (async () => { + try { + const info = await DBCoachSequence(category, number, evaNumber, departure); + coachSequenceCache[key] = info; + return info; + } catch (e) {} + coachSequenceCache[key] = null; + }) (); + } + + return coachSequenceCache[key]; +};
diff --git a/src/helpers.js b/src/helpers.js @@ -18,6 +18,8 @@ const loyaltyCardsReverse = { 'Symbol(General-Abonnement)': 'GENERALABONNEMENT', }; +export const sleep = delay => new Promise((resolve) => setTimeout(resolve, delay)); + export const ElementById = id => document.getElementById(id); export const showElement = element => element.classList.remove('hidden');
diff --git a/src/journeyView.js b/src/journeyView.js @@ -1,5 +1,5 @@ -import { cachedCoachSequence } from './reihung/index.js'; import { html, nothing, render } from 'lit-html'; +import { cachedCoachSequence } from './coach-sequence/index.js'; import { settings } from './settings.js'; import { remarksModalTemplate, platformTemplate, stopTemplate, timeTemplate } from './templates.js'; import { ElementById, setThemeColor, queryBackgroundColor } from './helpers.js';
diff --git a/src/reihung/DB/DBMapping.js b/src/reihung/DB/DBMapping.js @@ -1,220 +0,0 @@ -import { enrichCoachSequence } from '../commonMapping.js'; -import { getLineFromNumber } from '../lineNumberMapping.js'; - -function mapSectors( - sectors, - basePercent, -) { - return ( - sectors?.map((s) => ({ - name: s.name, - position: { - startPercent: basePercent * s.start, - endPercent: basePercent * s.end, - }, - })) || [] - ); -} - -function mapStop( - evaNumber, - platform, -) { - if (platform?.start === undefined || platform.end === undefined) { - return [undefined, 0]; - } - const basePercent = 100 / (platform.end - platform.start); - return [ - { - stopPlace: { - evaNumber, - name: '', - }, - sectors: mapSectors(platform.sectors, basePercent), - }, - basePercent, - ]; -} - -function mapClass(vehicleType) { - switch (vehicleType.category) { - case 'LOCOMOTIVE': - case 'POWERCAR': { - return 4; - } - case 'DININGCAR': { - return 2; - } - } - if (vehicleType.hasFirstClass && vehicleType.hasEconomyClass) { - return 3; - } - if (vehicleType.hasFirstClass) { - return 1; - } - if (vehicleType.hasEconomyClass) { - return 2; - } - return 0; -} - -const diningCategories = new Set([ - 'DININGCAR', - 'HALFDININGCAR_ECONOMY_CLASS', - 'HALFDININGCAR_FIRST_CLASS', -]); -function mapFeatures(vehicle) { - const features = {}; - - for (const a of vehicle.amenities) { - switch (a.type) { - case 'BIKE_SPACE': { - features.bike = true; - break; - } - case 'BISTRO': { - features.dining = true; - break; - } - case 'INFO': { - features.info = true; - break; - } - case 'SEATS_BAHN_COMFORT': { - features.comfort = true; - break; - } - case 'SEATS_SEVERELY_DISABLED': { - features.disabled = true; - break; - } - case 'WHEELCHAIR_SPACE': { - features.wheelchair = true; - break; - } - case 'WIFI': { - features.wifi = true; - break; - } - case 'ZONE_FAMILY': { - features.family = true; - break; - } - case 'ZONE_QUIET': { - features.quiet = true; - break; - } - case 'CABIN_INFANT': { - features.toddler = true; - break; - } - } - } - - if (!features.dining && diningCategories.has(vehicle.type.category)) { - features.dining = true; - //logger.debug('Manually set dining feature'); - } - return features; -} - -function mapVehicle( - vehicle, - basePercent, -) { - if (!vehicle.platformPosition) { - return undefined; - } - const r = { - identificationNumber: vehicle.wagonIdentificationNumber?.toString(), - uic: vehicle.vehicleID, - type: vehicle.type.constructionType, - class: mapClass(vehicle.type), - vehicleCategory: vehicle.type.category, - closed: - vehicle.status === 'CLOSED' || - vehicle.type.category === 'LOCOMOTIVE' || - vehicle.type.category === 'POWERCAR', - position: { - startPercent: basePercent * vehicle.platformPosition.start, - endPercent: basePercent * vehicle.platformPosition.end, - }, - features: mapFeatures(vehicle), - }; - return r; -} - -function mapGroup( - group, - basePercent, -) { - const coaches = group.vehicles.map(vehicle => mapVehicle(vehicle, basePercent)); - if (coaches.includes(undefined)) { - return undefined; - } - return { - name: group.name, - destinationName: group.transport.destination.name, - originName: 'UNKNOWN', - number: group.transport.number.toString(), - coaches: coaches, - }; -} - -function mapDirection(coaches) { - const first = coaches[0]; - const last = coaches.at(-1); - - return last.position.startPercent > first.position.startPercent; -} - -async function mapSequence( - sequence, - basePercent, -) { - const groups = await Promise.all( - sequence.groups.map((g) => mapGroup(g, basePercent)), - ); - if (groups.includes(undefined)) return undefined; - return { - groups: groups, - }; -} - -export const mapInformation = async ( - upstreamSequence, - trainCategory, - trainNumber, - evaNumber, -) => { - if (!upstreamSequence) { - return undefined; - } - const [stop, basePercent] = mapStop(evaNumber, upstreamSequence.platform); - if (!stop) { - return undefined; - } - const sequence = await mapSequence(upstreamSequence, basePercent); - if (!sequence) { - return undefined; - } - const allCoaches = sequence.groups.flatMap((g) => g.coaches); - - const information = { - source: 'DB-bahnde', - product: { - number: trainNumber, - type: trainCategory, - }, - isRealtime: allCoaches.every( - (c) => c.uic || c.vehicleCategory === 'LOCOMOTIVE', - ), - stop, - sequence, - direction: mapDirection(allCoaches), - journeyId: upstreamSequence.journeyID, - }; - enrichCoachSequence(information); - - return information; -};
diff --git a/src/reihung/DB/index.js b/src/reihung/DB/index.js @@ -1,40 +0,0 @@ -import { - addDays, - differenceInHours, - isWithinInterval, - subDays, - format, - formatISO, -} from 'date-fns'; - -import { mapInformation } from './DBMapping.js'; - -const dbCoachSequenceTimeout = process.env.NODE_ENV === 'production' ? 2500 : 10000; - -export const getDBCoachSequenceUrl = (category, number, evaNumber, date) => { - const searchParams = new URLSearchParams(); - searchParams.append("category", category); - searchParams.append("date", format(date, "yyyy-MM-dd")); - searchParams.append("time", formatISO(date)); - searchParams.append("evaNumber", evaNumber); - searchParams.append("number", number); - return `/db/vehicle-sequence?${searchParams}`; -}; - -async function coachSequence(category, number, evaNumber, date) { - const info = await fetch(getDBCoachSequenceUrl(category, number, evaNumber, date)).then(x => x.json()); - return info; -} - -export async function rawDBCoachSequence(category, number, evaNumber, date, retry = 2) { - try { - return coachSequence(category, number, evaNumber, date); - } catch (e) { - if (retry) return rawDBCoachSequence(category, number, evaNumber, date, retry - 1); - } -} -export async function DBCoachSequence(category, number, evaNumber, date) { - const rawSequence = await rawDBCoachSequence(category, number, evaNumber, date); - if (!rawSequence) return undefined; - return mapInformation(rawSequence, category, number, evaNumber); -}
diff --git a/src/reihung/DB/plannedSequence.js b/src/reihung/DB/plannedSequence.js @@ -1,164 +0,0 @@ -import { getSeatsForCoach } from '../specialSeats.js'; -import { nameMap } from '../baureihe.js'; -import Axios from 'axios'; -const apiUrl = process.env.PLANNED_API_URL; -const apiKey = process.env.PLANNED_API_KEY; -export const planSequenceAxios = Axios.create({ - baseURL: apiUrl, - headers: { - 'x-api-key': apiKey || '' - } -}); -export async function getPlannedSequence(trainNumber, initialDeparture, evaNumber) { - if (!apiKey || !apiUrl) { - return undefined; - } - - try { - const plannedSequence = (await planSequenceAxios.get(`/sequence/${trainNumber}/${initialDeparture.toISOString()}/${evaNumber}`)).data; - plannedSequence.sequence.groups.forEach(g => { - const br = getBRFromGroupName(g.name); - g.baureihe = br; - g.name = `${g.number}-planned`; - - if (br) { - g.coaches.forEach(coach => { - coach.seats = getSeatsForCoach(coach, br.identifier); - }); - } - }); - return plannedSequence; - } catch (e) { - return undefined; - } -} - -function getBRWithoutNameFromGroupName(groupName) { - // ICE - if (groupName.startsWith('401_11')) { - return { - identifier: '401.9', - baureihe: '401' - }; - } - - if (groupName.startsWith('401_14')) { - return { - identifier: '401', - baureihe: '401' - }; - } - - if (groupName.startsWith('402')) { - return { - identifier: '402', - baureihe: '402' - }; - } - - if (groupName.startsWith('403E')) { - return { - identifier: '403.R', - baureihe: '403' - }; - } - - if (groupName.startsWith('406')) { - return { - identifier: '406' - }; - } - - if (groupName.startsWith('403')) { - return { - identifier: '403', - baureihe: '403' - }; - } - - if (groupName.startsWith('406.01')) { - return { - identifier: '406', - baureihe: '406' - }; - } - - if (groupName.startsWith('406.02')) { - return { - identifier: '406.R', - baureihe: '406' - }; - } - - if (groupName.startsWith('412_12')) { - return { - identifier: '412', - baureihe: '412' - }; - } - - if (groupName.startsWith('407')) { - return { - identifier: '407', - baureihe: '407' - }; - } - - if (groupName.startsWith('411')) { - return { - identifier: '411', - baureihe: '411' - }; - } - - if (groupName.startsWith('412_13')) { - return { - identifier: '412.13', - baureihe: '412' - }; - } - - if (groupName.startsWith('412_07')) { - return { - identifier: '412.7', - baureihe: '412' - }; - } - - if (groupName.startsWith('406')) { - return { - identifier: '406', - baureihe: '406' - }; - } - - if (groupName.startsWith('415')) { - return { - identifier: '415', - baureihe: '415' - }; - } // IC - - - if (groupName.startsWith('KISS')) { - return { - identifier: 'IC2.KISS' - }; - } - - if (groupName.startsWith('Dosto')) { - return { - identifier: 'IC2.TWIN' - }; - } -} - -export function getBRFromGroupName(groupName) { - const brWithoutName = getBRWithoutNameFromGroupName(groupName); - - if (brWithoutName) { - return { ...brWithoutName, - name: nameMap[brWithoutName.identifier] - }; - } -}
diff --git a/src/reihung/baureihe.js b/src/reihung/baureihe.js @@ -1,203 +0,0 @@ -import notRedesigned from './notRedesigned.json'; -export const nameMap = { - '401': 'ICE 1 (BR401)', - '401.9': 'ICE 1 Kurz (BR401)', - '401.LDV': 'ICE 1 Modernisiert (BR401)', - '402': 'ICE 2 (BR402)', - '403': 'ICE 3 (BR403)', - '403.S1': 'ICE 3 (BR403 1. Serie)', - '403.S2': 'ICE 3 (BR403 2. Serie)', - '403.R': 'ICE 3 (BR403)', - '406': 'ICE 3 (BR406)', - '406.R': 'ICE 3 (BR406 Redesign)', - '407': 'ICE 3 Velaro (BR407)', - '408': 'ICE 3neo (BR408)', - '410.1': 'ICE S (BR410.1)', - '411': 'ICE T (BR411)', - '411.S1': 'ICE T (BR411 1. Serie)', - '411.S2': 'ICE T (BR411 2. Serie)', - '412': 'ICE 4 (BR412)', - '412.7': 'ICE 4 Kurz (BR412)', - '412.13': 'ICE 4 Lang (BR412)', - '415': 'ICE T Kurz (BR415)', - 'IC2.TRE': 'IC 2 (TRE)', - '4110': 'IC 2 KISS (BR4110)', - '4010': 'IC 2 KISS (BR4010)', - MET: 'MET', - TGV: 'TGV', -}; - -const getATBR = (code, _serial, _coaches) => { - switch (code) { - case '4011': - return { - baureihe: '411', - identifier: '411.S1' - }; - } -}; - -const getDEBR = (code, uicOrdnungsnummer, coaches, tzn) => { - switch (code) { - case '0812': - case '1412': - case '1812': - case '2412': - case '2812': - case '3412': - case '4812': - case '5812': - case '6412': - case '6812': - case '7412': - case '7812': - case '8812': - case '9412': - case '9812': { - let identifier; - switch (coaches.length) { - case 13: { - identifier = '412.13'; - break; - } - case 7: { - identifier = '412.7'; - break; - } - } - return { - identifier, - baureihe: '412', - }; - } - case '5401': - case '5801': - case '5802': - case '5803': - case '5804': { - let identifier; - if (coaches.length === 11) { - identifier = - coaches.filter((f) => f.class === 1).length === 2 - ? '401.LDV' - : '401.9'; - } - return { - identifier, - baureihe: '401', - }; - } - case '5402': - case '5805': - case '5806': - case '5807': - case '5808': { - return { - baureihe: '402', - identifier: '402', - }; - } - case '5403': { - // const identifier: AvailableIdentifier = `403.S${ - // Number.parseInt(uicOrdnungsnummer.slice(1), 10) <= 37 ? '1' : '2' - // }`; - return { - baureihe: '403', - identifier: '403.R', - }; - } - case '5406': { - return { - baureihe: '406', - identifier: tzn?.endsWith('4651') ? '406.R' : '406', - }; - } - case '5407': { - return { - baureihe: '407', - identifier: '407', - }; - } - case '5410': { - return { - baureihe: '410.1', - identifier: '410.1', - }; - } - case '5408': { - return { - baureihe: '408', - identifier: '408', - }; - } - case '5411': { - return { - baureihe: '411', - identifier: `411.S${ - Number.parseInt(uicOrdnungsnummer, 10) <= 32 ? '1' : '2' - }`, - }; - } - case '5415': { - return { - baureihe: '415', - identifier: '415', - }; - } - case '5475': { - return { - identifier: 'TGV', - }; - } - } -}; - -export const getBaureiheByUIC = (uic, coaches, tzn) => { - const country = uic.slice(2, 4); - const code = uic.slice(4, 8); - const serial = uic.slice(8, 11); - let br; - switch (country) { - case '80': { - br = getDEBR(code, serial, coaches, tzn); - break; - } - case '81': { - br = getATBR(code, serial, coaches); - break; - } - } - if (!br) return undefined; - - return { - ...br, - name: nameMap[br.identifier || br.baureihe], - }; -}; -export const getBaureiheByCoaches = coaches => { - let identifier; - for (const c of coaches) { - if (c.type === 'Apmbzf') { - identifier = 'MET'; - break; - } - if (c.type === 'DBpbzfa') { - identifier = 'IC2.TRE'; - break; - } - if (c.uic?.slice(4, 8) === '4110') { - identifier = '4110'; - break; - } - if (c.uic?.slice(4, 8) === '4010') { - identifier = '4010'; - break; - } - } - if (identifier) { - return { - identifier, - name: nameMap[identifier], - }; - } -};
diff --git a/src/reihung/commonMapping.js b/src/reihung/commonMapping.js @@ -1,104 +0,0 @@ -import { getBaureiheByCoaches, getBaureiheByUIC } from './baureihe.js'; -import { getSeatsForCoach } from './specialSeats.js'; -import TrainNames from './TrainNames.js'; - -const hasNonLokCoach = group => group.coaches.some(c => c.category !== 'LOK' && c.category !== 'TRIEBKOPF'); - -export function enrichCoachSequence(coachSequence) { - let prevGroup; - - for (const group of coachSequence.sequence.groups) { - enrichCoachSequenceGroup(group, coachSequence.product); - - if (!hasNonLokCoach(group)) { - continue; - } - - if (prevGroup && prevGroup.destinationName !== group.destinationName) { - coachSequence.multipleDestinations = true; - } - - if (prevGroup && prevGroup.number !== group.number) { - coachSequence.multipleTrainNumbers = true; - } - - prevGroup = group; - } -} -const allowedBR = ['IC', 'EC', 'ICE', 'ECE']; -const tznRegex = /(\d+)/; - -function enrichCoachSequenceGroup(group, product) { - // https://inside.bahn.de/entstehung-zugnummern/?dbkanal_006=L01_S01_D088_KTL0006_INSIDE-BAHN-2019_Zugnummern_LZ01 - const trainNumberAsNumber = Number.parseInt(group.number, 10); - - if (trainNumberAsNumber >= 9550 && trainNumberAsNumber <= 9599) { - group.coaches.forEach(c => { - if (c.features.comfort) { - c.features.comfort = false; - } - }); - } - - if (allowedBR.includes(product.type)) { - let tzn; - - if (group.name.startsWith('IC')) { - tzn = tznRegex.exec(group.name)?.[0]; - group.trainName = TrainNames(tzn); - } - - group.baureihe = calculateBR(group.coaches, tzn); - - - if (group.baureihe) { - if ( - group.baureihe.identifier === '401.LDV' || - group.baureihe.identifier === '401.9' - ) { - // Schwerbehindertenplätze/Vorrangplätze sind in Wagen 11, nicht 12 - for (const coach of group.coaches) { - if (coach.identificationNumber === '11') { - coach.features.disabled = true; - } - if (coach.identificationNumber === '12') { - coach.features.disabled = false; - } - } - } - if ( - group.baureihe.identifier === '4010' || - group.baureihe.identifier === '4110' - ) { - for (const c of group.coaches) { - switch (c.identificationNumber) { - case '6': { - c.features.disabled = true; - break; - } - case '5': { - c.features.disabled = true; - break; - } - case '4': { - c.features.disabled = false; - } - } - } - } - for (const c of group.coaches) { - //c.seats = getSeatsForCoach(c, group.baureihe.identifier); - } - } - } -} - -function calculateBR(coaches, tzn) { - for (const c of coaches) { - if (!c.uic) continue; - const br = getBaureiheByUIC(c.uic, coaches, tzn); - if (br) return br; - } - - return getBaureiheByCoaches(coaches); -}
diff --git a/src/reihung/index.js b/src/reihung/index.js @@ -1,28 +0,0 @@ -import { DBCoachSequence } from './DB/index.js'; - -export async function coachSequence(category, number, evaNumber, departure) { - return await DBCoachSequence(category, number, evaNumber, departure); -} - -export const coachSequenceCache = {}; - -export const coachSequenceCacheKey = (category, number, evaNumber, departure) => { - if (!category || !number || !evaNumber || !departure) return; - return `${category}-${number}-${evaNumber}-${departure.toISOString()}`; -}; - -export const cachedCoachSequence = (category, number, evaNumber, departure) => { - const key = coachSequenceCacheKey(category, number, evaNumber, departure); - if (!key) return; - if (coachSequenceCache[key] === undefined) { - coachSequenceCache[key] = (async () => { - try { - const info = await coachSequence(category, number, evaNumber, departure); - coachSequenceCache[key] = info; - return info; - } catch (e) {} - coachSequenceCache[key] = null; - }) (); - } - return coachSequenceCache[key]; -};
diff --git a/src/reihung/lineNumberMapping.js b/src/reihung/lineNumberMapping.js @@ -1,6 +0,0 @@ -import lines from './lines.json'; - -export function getLineFromNumber(journeyNumber) { - if (!journeyNumber) return undefined; - return lines[journeyNumber]; -}
diff --git a/src/reihung/specialSeats.js b/src/reihung/specialSeats.js @@ -1,175 +0,0 @@ -export function getComfortSeats(identifier, klasse) { - switch (identifier) { - case '401': - return klasse === 1 ? '11-36' : '11-57'; - - case '401.9': - case '401.LDV': - return klasse === 1 ? '12-31' : '11-44'; - - case '402': - return klasse === 1 ? '11-16, 21, 22' : '81-108'; - - case '403': - case '403.S1': - case '403.S2': - case '406': - return klasse === 1 ? '12-26' : '11-38'; - - case '403.R': - case '406.R': - return klasse === 1 ? '12-26' : '11-37'; - - case '407': - return klasse === 1 ? '21-26, 31, 33, 35' : '31-55, 57'; - - case '411': - return klasse === 1 ? '46, 52-56' : '92, 94, 96, 98, 101-118'; - - case '412.7': - return klasse === 1 ? '41, 44-53' : '11-44'; - - case '412': - case '412.13': - return klasse === 1 ? '11-46' : '11-68'; - - case '415': - return klasse === 1 ? '52, 54, 56' : '81-88, 91-98'; - - case 'MET': - return klasse === 1 ? '61-66' : '91-106'; - - case 'IC2.TWIN': - return klasse === 1 ? '73, 75, 83-86' : '31-38, 41-45, 47'; - - case 'IC2.KISS': - return klasse === 3 ? '144, 145' : '55-68'; - } -} -export function getDisabledSeats(identifier, klasse, wagenordnungsnummer) { - switch (identifier) { - case '401': - return klasse === 1 ? '51, 52, 53, 55' : '111-116'; - - case '401.9': - case '401.LDV': - return klasse === 1 ? '11, 13, 15' : '11, 13, 111-116'; - - case '402': - return klasse === 1 ? '12, 21' : '81, 85-88'; - - case '403': - case '403.R': - case '403.S1': - case '403.S2': - case '406': - if (klasse === 1) return '64, 66'; - - if (wagenordnungsnummer === '25' || wagenordnungsnummer === '35') { - // redesign slighlty different - return ['403R', '403.S1R', '403.S2R'].includes(identifier) ? '61, 63, 65-67' : '61, 63, 65, 67'; - } - - return '106, 108'; - - case '407': - if (klasse === 1) return '13, 15'; - - if (wagenordnungsnummer === '11' || wagenordnungsnummer === '21') { - return '11-18'; - } - - return '28, 33-34'; - - case '411': - return klasse === 1 ? '21, 22' : '15-18'; - - case '412.7': - return klasse === 1 ? '12, 13' : '11-18'; - - case '412': - case '412.13': - if (klasse === 1) return wagenordnungsnummer === '10' ? '12, 13' : '11, 14, 21'; - - switch (wagenordnungsnummer) { - case '1': - return '11-24'; - - case '8': - return '11, 12'; - - case '9': - return '41, 45, 46'; - } - - break; - - case '415': - return klasse === 1 ? '21' : '15, 17'; - - case 'MET': - return klasse === 1 ? '16, 21' : '12, 14, 16'; - - case 'IC2.TWIN': - return klasse === 1 ? '21, 71' : '25, 101-105, 171-173'; - - case 'IC2.KISS': - return klasse === 3 ? '143' : '21-26'; - } -} -export function getFamilySeats(identifier) { - switch (identifier) { - case '401': - return '81-116'; - - case '401.9': - case '401.LDV': - return '91-116'; - - case '402': - return '61-78'; - - case '403': - case '403.R': - case '403.S1': - case '403.S2': - case '406': - case '406.R': - return '11-28'; - - case '407': - return '11-28'; - - case '411': - return '11-18, 31-38'; - - case '412.7': - return '61-78'; - - case '412': - case '412.13': - return '61-78'; - - case 'MET': - return '11-26'; - - case 'IC2.TWIN': - return '121, 123, 131-138'; - - case 'IC2.KISS': - return '42, 43, 45, 46, 52-56'; - } -} -export function getSeatsForCoach(coach, identifier) { - const family = coach.features.family ? getFamilySeats(identifier) : undefined; - const disabled = coach.features.disabled ? getDisabledSeats(identifier, coach.class, coach.identificationNumber) : undefined; - const comfort = coach.features.comfort ? getComfortSeats(identifier, coach.class) : undefined; - - if (family || disabled || comfort) { - return { - comfort, - disabled, - family - }; - } -}- \ No newline at end of file