diff options
author | 2025-07-21 22:53:11 +0200 | |
---|---|---|
committer | 2025-07-21 22:53:11 +0200 | |
commit | a723e56b4ce392d2b11d28f2745279aa825a2ee1 (patch) | |
tree | dc69ea161c9b52e84c3969fbb9562b780f74586b /src/lib | |
download | fahrplan-a723e56b4ce392d2b11d28f2745279aa825a2ee1.tar.gz fahrplan-a723e56b4ce392d2b11d28f2745279aa825a2ee1.tar.bz2 fahrplan-a723e56b4ce392d2b11d28f2745279aa825a2ee1.tar.lz fahrplan-a723e56b4ce392d2b11d28f2745279aa825a2ee1.zip |
feat: initial commit
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/Page.svelte | 32 | ||||
-rw-r--r-- | src/lib/Timetable.svelte | 424 | ||||
-rw-r--r-- | src/lib/Titlebar.svelte | 55 | ||||
-rw-r--r-- | src/lib/aliases.ts | 36 | ||||
-rw-r--r-- | src/lib/assets/LineGlyph.svelte | 193 | ||||
-rw-r--r-- | src/lib/assets/LineGlyphSrc.svg | 233 | ||||
-rw-r--r-- | src/lib/assets/Pictogram.svelte | 11 | ||||
-rw-r--r-- | src/lib/index.ts | 51 | ||||
-rw-r--r-- | src/lib/motis-api.ts | 57 | ||||
-rw-r--r-- | src/lib/motis-types.ts | 496 |
10 files changed, 1588 insertions, 0 deletions
diff --git a/src/lib/Page.svelte b/src/lib/Page.svelte new file mode 100644 index 0000000..92cd92a --- /dev/null +++ b/src/lib/Page.svelte @@ -0,0 +1,32 @@ +<script lang="ts"> + import { S } from '$lib'; + import type { Snippet } from 'svelte'; + + let { + children, + sidepanel, + title, + description = 'No Description Specified', + class: classes, + }: { + children: Snippet; + sidepanel?: Snippet; + title: string; + description?: string; + class?: string; + } = $props(); +</script> + +<svelte:head> + <title>{title}</title> + <meta name="description" content={description} /> +</svelte:head> + +<div class="{S.window} {S.shadow_large} relative max-w-[100%]"> + {#if sidepanel} + <div class={S.window_sidepanel}> + {@render sidepanel()} + </div> + {/if} + {@render children()} +</div> diff --git a/src/lib/Timetable.svelte b/src/lib/Timetable.svelte new file mode 100644 index 0000000..4a17ca7 --- /dev/null +++ b/src/lib/Timetable.svelte @@ -0,0 +1,424 @@ +<script lang="ts"> + import { browser, dev } from '$app/environment'; + import { page } from '$app/state'; + import { S } from '$lib'; + import { operators } from './aliases'; + import LineGlyph from './assets/LineGlyph.svelte'; + import Pictogram from './assets/Pictogram.svelte'; + import { Mode, type StoptimesResponse } from './motis-types'; + import { m } from './paraglide/messages'; + + let relativeSecondPrecision = $derived( + ['seconds', 'second', 'sec', 'secs', 's', '2'].includes( + page.url.searchParams.get('relative')! + ) + ); + let isRelativeTime = $derived( + relativeSecondPrecision || + ['1', 'true'].includes(page.url.searchParams.get('relative')!) + ); + /** only if isRelativeTime is true */ + let now = $state(0); + const updateNow = () => { + now = Date.now(); + setTimeout(updateNow, 33); + }; + $effect(() => (isRelativeTime ? updateNow() : void 0)); + + let { + stopTimes, + isArrivals = false, + placeName = '', + placeId, + isResultsPage = true, + setSearch = (q) => void 0, + }: { + stopTimes: null | StoptimesResponse; + isArrivals?: boolean; + placeName?: string; + isResultsPage?: boolean; + placeId: string | null; + setSearch: (query: string) => void; + } = $props(); + const timeRelative = (ms: number, relativeTo = now) => { + const _totalMillis = ms - relativeTo; + const totalMillis = Math.abs(_totalMillis); + const totalSeconds = totalMillis / 1000; + const totalMinutes = totalSeconds / 60; + const totalHours = totalMinutes / 60; + const hours = Math.floor(totalHours); + const minutes = Math.floor(totalMinutes) - hours * 60; + const seconds = Math.floor(totalSeconds) - hours * 60 * 60 - minutes * 60; + const results = [] as string[]; + if (hours) results.push(`${hours} ${hours !== 1 ? m.hours() : m.hour()}`); + if (hours || minutes) + results.push(`${minutes} ${minutes !== 1 ? m.minutes() : m.minute()}`); + if (seconds && relativeSecondPrecision) + results.push(`${seconds} ${seconds !== 1 ? m.seconds() : m.second()}`); + if (results.length > 1) { + const [r, l] = [results.pop()!, results.pop()!]; + results.push( + m.timeJoiner({ + l, + r, + }) + ); + } + const relTime = + results.length === 1 + ? results[0] + : results.length === 0 + ? '' + : results.reduce((pv, cv) => m.timeJoiner2({ l: pv, r: cv })); + if (relTime === '') + return m.timeImmediate({ + isPastTense: _totalMillis < 0 ? 'true' : 'false', + }); + if (_totalMillis < 0) + return m.timeInPast({ + relTime, + }); + else + return m.timeInFuture({ + relTime, + }); + }; + const localTime = async ( + time: Date, + forceAbsolute = false, + relativeTo = now + ) => { + const offset = parseFloat( + browser + ? // Browsers like Librewolf love patching time to be a fixed timezone, breaking us. + (localStorage.getItem( + isRelativeTime && !forceAbsolute + ? 'relative-offset-time' + : 'offset-time' + ) ?? '0') + : ((await import('$env/dynamic/private')).env[ + isRelativeTime && !forceAbsolute + ? 'PRIVATE_RELATIVE_TIME_OFFSET' + : 'PRIVATE_SERVER_TIMEZONE' + ] ?? '0') + ); + if (isRelativeTime && !forceAbsolute) { + return timeRelative(time.getTime() - offset * 60 * 60 * 1000, relativeTo); + } else { + const hours = time.getHours() + Math.floor(offset); + const minutes = time.getMinutes() + Math.floor((offset % 1) * 60); + const seconds = + time.getSeconds() + Math.floor(((offset % 1) * 60) % 1) * 60; + return ( + hours.toString().padStart(2, '0') + + ':' + + minutes.toString().padStart(2, '0') + + (seconds !== 0 ? `:${seconds.toString().padStart(2, '0')}` : '') + ); + } + }; +</script> + +{#snippet renderLocalTime( + time: Date, + forceAbsolute = false +)}{#await localTime(time, forceAbsolute, now)}{isRelativeTime && !forceAbsolute + ? timeRelative(time.getTime(), now) + : time.getUTCHours() + + ':' + + time.getUTCMinutes() + + (time.getUTCSeconds() !== 0 ? `:${time.getUTCSeconds()}` : '') + + ' UTC'}{:then t}{t}{/await}{/snippet} + +{#if stopTimes} + {#each stopTimes.stopTimes + // garbage data + .filter((v) => v.agencyUrl !== 'http://www.rta.ae') as departure} + {@const expectedTime = + new Date( + (isArrivals + ? departure.place.scheduledArrival + : departure.place.scheduledDeparture) ?? '0' + ).getTime() / + 1000 / + 60} + {@const receivedTime = + new Date( + (isArrivals ? departure.place.arrival : departure.place.departure) ?? + '0' + ).getTime() / + 1000 / + 60} + {@const delayMinutes = receivedTime - expectedTime} + {@const avoidGlyph = departure.routeShortName.startsWith('FlixTrain')} + {@const routeShortName = (() => { + let n = departure.routeShortName; + if (n.startsWith('EC ')) n = n.replace('EC ', 'EC'); + if (departure.mode === 'TRAM' && !isNaN(parseInt(n))) n = 'T ' + n; + if (departure.mode === 'BUS' && !isNaN(parseInt(n))) n = 'B ' + n; + if ( + departure.routeShortName === 'European Sleeper' && + departure.agencyName === 'Eu Sleeper' + ) + n = 'EN'; // TODO: validate these are real euronights + if (n === '?') n = ''; + if (n.startsWith('FlixTrain ')) n = n.substring(10); + // Note: may also catch ECs/ICs/EXTs operated by DB + if ( + departure.agencyId === '12681' && + departure.agencyName === 'DB Fernverkehr AG' && + departure.mode === 'HIGHSPEED_RAIL' && + departure.source.startsWith('de_DELFI.gtfs.zip/') && + !isNaN(parseInt(n)) + ) + n = `ICE ${n}`; + return n; + })()} + {@const pictogram = (() => { + switch (true) { + case departure.mode === Mode.Bike: + return 'Velo_l'; + case departure.mode === Mode.ODM: + case departure.mode === Mode.Rental: + return 'Taxi_l'; + case departure.mode === Mode.Car: + case departure.mode === Mode.CarDropoff: + case departure.mode === Mode.CarParking: + return 'Auto_l'; + + // Transit // + case departure.mode === Mode.Airplane: + return 'Abflug_l'; + case departure.mode === Mode.LongDistanceRail: + case departure.mode === Mode.RegionalFastRail: + case departure.mode === Mode.RegionalRail: + case departure.mode === Mode.Metro: + case departure.mode === Mode.HighspeedRail: + return 'Zug_l'; + case departure.mode === Mode.NightRail: + return 'Schlafwagen'; + case departure.mode === Mode.Subway: + return 'Metro_l_' + (m.lang_short() === 'en' ? 'de' : m.lang_short()); + case departure.mode === Mode.Bus: + return 'Bus_l'; + case departure.mode === Mode.Coach: + return 'Fernbus_l'; + case departure.mode === Mode.Tram: + case departure.mode === Mode.CableTram: + return 'Tram_l'; + case departure.mode === Mode.Funicular: + return 'Zahnradbahn_l'; + case departure.mode === Mode.AerialLift: + // return 'Gondelbahn_l'; + return 'Luftseilbahn_l'; + case departure.mode === Mode.Ferry: + return 'Schiff_l'; + case departure.mode === Mode.Other: + default: + return 'Licht'; + } + })()} + {@const notices = (() => { + let notices = [] as [pictogram: string[], content: string][]; + if (departure.cancelled) + notices.push([ + ['Cancellation', 'Attention'], + m.brief_north_otter_cherish(), + ]); + if (delayMinutes < -0.5) { + notices.push([ + ['Hint'], + m.vehicle_is_early({ + minutes: -delayMinutes, + arrival: isArrivals.toString(), + }), + ]); + } else if (delayMinutes >= 1) { + notices.push([ + delayMinutes >= 5 + ? ['Delay', 'Attention'] + : delayMinutes >= 3 + ? ['Delay', 'Hint'] + : ['Hint'], + m.next_long_lark_bask({ + minutes: delayMinutes.toFixed(0), + }), + ]); + } + return notices; + })()} + {@const situationIsBad = notices.find((v) => v[0].includes('Attention'))} + <div + class={{ + 'p-4 pr-3 sm:pr-4 md:p-6 md:pr-6 rounded-xl': true, + 'bg-[#28282C]': !situationIsBad, + 'bg-[#452525]': situationIsBad, + }} + data-data={dev ? JSON.stringify(departure, null, 2) : undefined} + > + <div class="flex gap-1 md:items-center md:flex-row flex-col flex-wrap"> + <div class="pictoline flex gap-1 md:items-center flex-r"> + {#if pictogram} + <Pictogram which={pictogram} /> + {/if} + {#if ([Mode.NightRail || Mode.HighspeedRail || Mode.LongDistanceRail || Mode.RegionalFastRail || Mode.RegionalRail].includes(departure.mode) || (departure.mode === 'BUS' && routeShortName.startsWith('EV')) || (departure.mode === 'METRO' && departure.routeShortName.startsWith('S'))) && !avoidGlyph} + <LineGlyph currentColor="#fff" kind={routeShortName} /> + {:else} + <span + class="ml-1 -mr-0.5 md:mt-0.5 font-sbb-typo text-nowrap font-bold" + > + {routeShortName} + </span> + {/if} + <span class="ml-1 -mr-0.5 md:mt-0.5 font-sbb-typo"> + <!-- {isArrivals ? m.from() : m.to()} --> + {m.to()} + <span class="font-semibold">{departure.headsign}</span> + </span> + </div> + <div class="flex-1"></div> + {#if departure.place.scheduledTrack && departure.place.track} + <span + class={{ + 'ml-1 mt-0.5 font-[SBB,Inter,system-ui,sans-serif]': true, + 'text-red-400': + departure.place.scheduledTrack !== departure.place.track, + }} + > + {#if departure.place.name !== placeName}{`${ + departure.place.name === placeName + ', Bahnhof' + ? placeName + ', Busbahnhof' + : departure.place.name + }, `} + {/if}<span class="font-semibold" + >{m.station_location({ + track: departure.place.track, + mode: departure.mode, + })}</span + > + </span> + {:else if departure.place.name !== placeName}{departure.place.name === + placeName + ', Bahnhof' + ? placeName + ', Busbahnhof' + : departure.place.name} + {/if} + </div> + <div class="flex gap-1 items-center"> + {departure.cancelled + ? m.antsy_weird_cowfish_wish() + ' ' + : ''}{isRelativeTime && Math.abs(expectedTime - receivedTime) < 1 + ? isArrivals + ? m.hour_tidy_hawk_explore() + : m.free_knotty_ray_soar() + : isArrivals + ? m.polite_lucky_angelfish_amaze() + : m.home_flaky_jurgen_ascend()} + {#if Math.abs(expectedTime - receivedTime) < 1} + <span class="font-bold"> + {@render renderLocalTime(new Date(receivedTime * 60 * 1000))} + </span> + {:else} + <span class="line-through"> + {@render renderLocalTime(new Date(expectedTime * 60 * 1000), true)} + </span> + <span class="font-bold"> + {@render renderLocalTime(new Date(receivedTime * 60 * 1000), true)} + </span> + {#if isRelativeTime} + <span class="opacity"> + ({@render renderLocalTime(new Date(receivedTime * 60 * 1000))}) + </span> + {/if} + {/if} + </div> + {#if notices.length !== 0} + <div class="notices pt-2 flex flex-col gap-1"> + {#each notices as notice} + <div class="flex items-center gap-2"> + {#each notice[0] as pictogram}<Pictogram + which={pictogram} + />{/each} + <span class="ml-0.5"> + {notice[1]} + </span> + </div> + {/each} + </div> + {/if} + <!-- <pre>{JSON.stringify(departure, null, 2)}</pre> --> + {#if departure.agencyName} + <small class="-mb-1 mt-2 block opacity-70" + >{m.operated_by({ + operator: operators.has(departure.agencyName) + ? operators.get(departure.agencyName)! + : departure.agencyName, + })}{#if departure.agencyName === 'DB Fernverkehr AG'} + {' '} + <b>·</b> + {m.line_number_accuracy()}{/if}</small + > + {/if} + </div> + {/each} +{:else} + <div class="flex items-center justify-center"> + <div class="results"> + {#if (placeName || placeId) && isResultsPage} + <h2 class="text-2xl opacity-90">No results</h2> + <p> + No results have been found for the station <b + >{placeName ?? placeId}</b + >.<br /> + Please try again. + </p> + {:else if placeId} + <h2 class="text-2xl opacity-90">No results</h2> + <p> + No results have been found for the station <b + >{placeName ?? placeId}</b + >.<br /> + Please try again. + </p> + {:else} + <h2 class="text-2xl opacity-90">No Station</h2> + <p class="pb-1"> + Please input a station in the search field above and select a search + result. + </p> + <p class="py-1"> + Examples: + <span class="pt-2 flex flex-wrap gap-2"> + <button + class="{S.button('secondary').replace( + 'not-disabled:bg-[#0000]', + 'not-disabled:bg-[#2E2E3299]' + )} flex-1 text-nowrap p-3" + onclick={() => setSearch('Zürich HB')}>Zürich HB</button + > + <button + class="{S.button('secondary').replace( + 'not-disabled:bg-[#0000]', + 'not-disabled:bg-[#2E2E3299]' + )} flex-1 text-nowrap p-3" + onclick={() => setSearch('Berlin Hbf')}>Berlin Hbf</button + > + <button + class="{S.button('secondary').replace( + 'not-disabled:bg-[#0000]', + 'not-disabled:bg-[#2E2E3299]' + )} flex-1 text-nowrap p-3" + onclick={() => setSearch('Hamburg Hbf')}>Hamburg Hbf</button + > + <button + class="{S.button('secondary').replace( + 'not-disabled:bg-[#0000]', + 'not-disabled:bg-[#2E2E3299]' + )} flex-1 text-nowrap p-3" + onclick={() => setSearch('Bern')}>Bern</button + > + </span> + </p> + {/if} + </div> + </div> +{/if} diff --git a/src/lib/Titlebar.svelte b/src/lib/Titlebar.svelte new file mode 100644 index 0000000..35cfd0d --- /dev/null +++ b/src/lib/Titlebar.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + import { S } from '$lib'; + import type { Snippet } from 'svelte'; + let { children }: { children?: Snippet } = $props(); +</script> + +<div class="rounded-tr-xl {S.window_topbar}"> + <!-- <div class="flex gap-2 w-max"> + <button + onclick={() => history.back()} + onkeypress={(e) => + e.key === 'Return' || e.key === 'Enter' + ? e.currentTarget.click() + : void 0} + aria-label="Back" + class={S.button('secondary')} + ><svg + height="16px" + viewBox="0 0 16 16" + width="16px" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="m 12 2 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 l -6 6 c -0.1875 0.1875 -0.292969 0.441406 -0.292969 0.707031 s 0.105469 0.519531 0.292969 0.707031 l 6 6 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 c 0.1875 -0.1875 0.292969 -0.441406 0.292969 -0.707031 s -0.105469 -0.519531 -0.292969 -0.707031 l -5.292969 -5.292969 l 5.292969 -5.292969 c 0.1875 -0.1875 0.292969 -0.441406 0.292969 -0.707031 z m 0 0" + fill="currentColor" + /> + </svg></button + > + <button + onclick={() => history.forward()} + onkeypress={(e) => + e.key === 'Return' || e.key === 'Enter' + ? e.currentTarget.click() + : void 0} + aria-label="Forward" + class={S.button('secondary')} + ><svg + height="16px" + viewBox="0 0 16 16" + width="16px" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="m 4 2 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 s 0.105469 -0.519531 0.292969 -0.707031 l 5.292969 -5.292969 l -5.292969 -5.292969 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" + fill="currentColor" + /> + </svg></button + > + </div> --> + <div + class="flex-1 rounded-tl-xl min-h-8 flex items-center justify-center gap-2" + > + {@render children?.()} + </div> +</div> diff --git a/src/lib/aliases.ts b/src/lib/aliases.ts new file mode 100644 index 0000000..4fc08ec --- /dev/null +++ b/src/lib/aliases.ts @@ -0,0 +1,36 @@ +import { m } from './paraglide/messages'; + +export const placeNameMap = new Map<string, string>(); +for (const [v1, v2] of [ + ['Freiburg(Brsg)', 'Freiburg(Breisgau) Hbf'], + ['Freiburg(Breisgau)', 'Freiburg(Breisgau) Hbf'], + ['Freiburg(Breisgau) Hbf', 'Freiburg(Breisgau) Hbf'], + ['Freiburg (Breisgau)', 'Freiburg(Breisgau) Hbf'], + ['Freiburg (Breisgau) Hbf', 'Freiburg(Breisgau) Hbf'], + ['Freiburg im Breisgau', 'Freiburg(Breisgau) Hbf'], + ['Freiburg im Breisgau Hbf', 'Freiburg(Breisgau) Hbf'], + ['Freiburg im Breisgau Hauptbahnhof', 'Freiburg(Breisgau) Hbf'], + ['Freiburg (D)', 'Freiburg(Breisgau) Hbf'], + ['Freiburg (D), Busbahnhof', 'Freiburg(Breisgau) Hbf'], + ['Freiburg Hauptbahnhof', 'Freiburg(Breisgau) Hbf'], + ['S+U Berlin Hauptbahnhof', 'Berlin Hbf'], + ['Berlin Hauptbahnhof', 'Berlin Hbf'], +]) + placeNameMap.set(v1.toLowerCase(), v2); +export const operators = new Map<string, string>(); +operators.set('Schweizerische Bundesbahnen SBB', m.operator_sbb()); +operators.set('SBB', m.operator_sbb()); + +operators.set('SZU', m.operator_szu()); +operators.set('Sihltal-Zürich-Uetliberg-Bahn', m.operator_szu()); + +operators.set('Verkehrsbetriebe Zürich', m.operator_vbz()); // buses +operators.set('Verkehrsbetriebe Zürich INFO+', m.operator_vbz()); // trams + +operators.set('BLS AG (bls)', m.operator_bls()); + +operators.set('Städtische Verkehrsbetriebe Bern', m.operator_bernmobil()); + +operators.set('Regionalverkehr Bern-Solothurn', m.operator_rbs()); + +operators.set('Verkehrsbetriebe Glattal', m.operator_vbg()); diff --git a/src/lib/assets/LineGlyph.svelte b/src/lib/assets/LineGlyph.svelte new file mode 100644 index 0000000..f5df523 --- /dev/null +++ b/src/lib/assets/LineGlyph.svelte @@ -0,0 +1,193 @@ +<script lang="ts"> + import { dev } from '$app/environment'; + + const fernverkehrIconMap = { + IR: 'M 2.43946,1.11125 H 3.51261 L 2.18493,4.18041 H 1.10728 Z m 4.43442,2.16058 c 0.097,-3.3e-4 0.18477,-0.0576 0.2241,-0.14631 L 7.96899,1.11125 H 4.2291 L 2.90142,4.18041 H 3.96981 L 4.97523,1.88118 H 6.56088 L 6.24311,2.6154 4.8604,2.6104 5.97615,4.18043 H 7.19244 L 6.36588,3.27185 Z', + IC: 'M 2.4406,1.11203 H 3.51401 L 2.18633,4.1812 H 1.10869 Z m 1.78011,0 H 8.02939 L 7.63516,2.02352 H 4.91525 L 4.37497,3.26971 H 7.09621 L 6.70198,4.1812 H 2.89304 Z', + EC: 'm 2.1507979,4.1626896 a 0.238125,0.238125 0 0 0 0.094721,0.017727 H 5.5890583 L 4.5100875,3.2525229 H 2.4299333 l 0.4741334,-0.4699 h 2.0984104 l 0.4743979,-0.4699 H 3.3779354 L 3.8401625,1.8370021 h 2.098675 L 6.6558583,1.11125 H 3.3485667 A 0.43127083,0.43127083 0 0 0 3.1231417,1.164696 L 3.0644047,1.212321 2.9929667,1.271852 1.1911542,3.0860999 a 0.21166667,0.21166667 0 0 0 -0.083344,0.1664229 0.16933333,0.16933333 0 0 0 0.065617,0.1367896 l 0.1180041,0.1010708 0.6757459,0.5413375 0.1068917,0.089165 c 0.020373,0.02196 0.047625,0.036513 0.076994,0.041804 m 4.0433625,0.00582 0.088371,0.011906 H 9.44245 L 10.367169,3.2525229 H 6.5315042 L 7.9245354,1.8370021 H 11.291623 L 12.003087,1.11125 H 7.2255062 l -1.920875,1.9629437 q -0.054504,0.050006 -0.097631,0.1098021 a 0.14816667,0.14816667 0 0 0 -0.015081,0.074348 0.17197917,0.17197917 0 0 0 0.04736,0.125148 l 0.089429,0.071438 0.6402917,0.5529791 0.082815,0.077258 q 0.044979,0.033338 0.094456,0.059531 z', + TGV: 'M 6.1364817,1.762125 A 1.8520833,1.8520833 0 0 0 5.9634437,1.669785 1.8520833,1.8520833 0 0 0 5.7247897,1.588558 1.5610417,1.5610417 0 0 0 5.3085997,1.54252 q -0.543718,0 -0.889264,0.3603625 -0.346075,0.3603625 -0.346075,0.9186334 0,0.434975 0.245269,0.681302 0.245268,0.2460625 0.683418,0.2460625 c 0.0799,0.0037 0.160073,-0.00688 0.236538,-0.030692 l 0.170656,-0.8133288 h -0.648229 l 0.100542,-0.4833937 h 1.239837 l -0.350308,1.6748125 q -0.08784,0.030427 -0.157692,0.050271 -0.07038,0.019844 -0.236537,0.052917 a 2.38125,2.38125 0 0 1 -0.442384,0.032808 q -0.723106,0 -1.101989,-0.3537479 -0.378884,-0.3537479 -0.378884,-1.0218209 0,-0.8262937 0.50165,-1.3123333 0.50165,-0.4855104 1.351492,-0.4855104 0.26035,-0.00132 0.516731,0.043921 a 3.4395833,3.4395833 0 0 1 0.486304,0.127529 z M 1.2125854,1.1115146 H 3.5737267 L 3.4731857,1.5951729 H 2.60985 l -0.5564187,2.58445 H 1.4141979 L 1.9703521,1.5954375 H 1.1075458 Z m 6.1198123,0 h -0.613304 l 0.402961,3.0681083 h 0.731572 l 1.708415,-3.0681083 h -0.665956 l -1.278996,2.3868062 z', + ICE: 'M 4.9887187,1.7227021 A 1.1641667,1.1641667 0 0 0 4.6950312,1.5909396 1.3758333,1.3758333 0 0 0 4.3053,1.5425208 q -0.5167312,0 -0.8453438,0.3386667 -0.3286125,0.3381375 -0.3286125,0.8744479 0,0.4397375 0.2452688,0.7164917 a 0.809625,0.809625 0 0 0 0.635,0.2770187 c 0.1008062,5.292e-4 0.2010833,-0.0127 0.2981854,-0.039687 0.117475,-0.037042 0.2307167,-0.087048 0.3373438,-0.149225 l -0.096573,0.5889625 q -0.061383,0.013229 -0.1706562,0.03519 -0.1098021,0.02196 -0.2169583,0.034925 a 2.38125,2.38125 0 0 1 -0.2868084,0.013229 q -0.6482291,0 -1.0162646,-0.3823229 -0.3680354,-0.382323 -0.3680354,-1.063625 0,-0.7871354 0.4815417,-1.2573 Q 3.4554583,1.0591271 4.270375,1.0591271 a 2.4870833,2.4870833 0 0 1 0.4489979,0.032808 q 0.1730375,0.033073 0.2365375,0.052917 0.0635,0.019844 0.1860021,0.0635 z M 1.7515417,1.1117792 H 2.3910396 L 1.7470437,4.1798875 H 1.1075458 Z m 5.6816625,0 H 5.6327146 L 4.9887187,4.1798875 h 1.82245 L 6.9162083,3.6962292 H 5.7070625 L 5.8869792,2.8347458 H 6.9598646 L 7.0564375,2.3513521 H 5.9875208 L 6.1452125,1.5954375 h 1.1959167 z', + // RJ: 'M 6.093,15.802 H 3.703 L 6.133,4.2 h 5.152 q 1.329,0 2.07,0.273 0.741,0.273 1.194,1.001 0.456,0.729 0.456,1.765 0,1.48 -0.887,2.442 -0.885,0.961 -2.683,1.19 0.46,0.412 0.863,1.085 0.8,1.361 1.78,3.846 H 11.514 Q 11.206,14.821 10.304,12.739 9.812,11.615 9.259,11.228 8.919,10.998 8.072,10.998 H 7.098 Z m 1.37,-6.545 h 1.266 q 1.923,0 2.552,-0.23 0.63,-0.228 0.985,-0.72 0.357,-0.49 0.356,-1.028 0,-0.635 -0.514,-0.95 -0.317,-0.19 -1.37,-0.19 H 8.112 Z M 22.375,4.2 h 2.342 l -1.464,7.028 q -0.585,2.793 -1.535,3.783 -0.95,0.99 -2.92,0.989 -1.67,0 -2.501,-0.768 -0.831,-0.767 -0.831,-2.09 0,-0.276 0.04,-0.616 l 2.207,-0.238 a 4.4,4.4 0 0 0 -0.047,0.578 q 0,0.555 0.316,0.847 0.317,0.293 0.974,0.293 0.918,0 1.29,-0.539 0.276,-0.411 0.673,-2.318 z', + // RJX: '', + // RX: '', + // NJ: '', + // OGV: '', + // PE: '', + IRE: '', + BEX: '', + GEX: '', + EN: '', + EXT: '', + CNL: '', + ICN: '', + }; + let { + kind: routeShortName, + type: _type, + line: _line, + currentColor: _currentColor, + nightIsFilled = true, + }: { + /** i.e. RE69, TER, etc... */ + kind: string; + type?: keyof typeof fernverkehrIconMap | string | null; + line?: string; + nightIsFilled?: boolean; + } & { + currentColor?: string; + } = $props(); + + let shortNumberIndex = $derived( + routeShortName.split('').findIndex((char) => char.match(/[0-9]/)) + ); + let line = $derived( + _line ?? + (_line === undefined + ? shortNumberIndex !== -1 + ? routeShortName.substring(shortNumberIndex).trim() + : '' + : _line) + ); + let lineType = $derived( + _type ?? + (_type === undefined + ? shortNumberIndex !== -1 + ? routeShortName.substring(0, shortNumberIndex).trim() + : routeShortName + : _type) + ); + let isRE = $derived( + typeof routeShortName === 'string' && + routeShortName.toUpperCase().startsWith('RE') && + routeShortName.charAt(2).toUpperCase() !== 'N' + ), + isFernverkehr = $derived( + (lineType ?? '').toUpperCase() in fernverkehrIconMap + ), + isNightRendering = + routeShortName.toUpperCase().startsWith('SN') || + routeShortName.toUpperCase().startsWith('REN') || + routeShortName.startsWith('IRN'); + let currentColor = $derived( + _currentColor ?? (isRE ? '#eb0000' : 'currentColor') + ); +</script> + +<!-- svelte-ignore attribute_illegal_colon --> +<svg + width="15.61mm" + height="5.2919998mm" + viewBox="0 0 15.61 5.2919998" + version="1.1" + id="svg1" + data-kind={routeShortName} + data-type={lineType} + data-line={line} + data-debug={dev + ? JSON.stringify({ + shortNumberIndex, + line, + type: lineType, + kind: routeShortName, + _line: _line ?? [`undef, ${_line}`], + _type: _type ?? [`undef, ${_type}`], + }) + : undefined} +> + <defs id="defs1"> + <clipPath id="a"> + <path fill="#fff" d="M0 0h59v20H0z" id="path3" /> + </clipPath> + </defs> + {#if isFernverkehr} + <g id="layer4" data-inkscape-label="Fernverkehr" style="display:inline"> + <path + fill="#eb0000" + fill-rule="evenodd" + d="M 0.52917,0 A 0.52916667,0.52916667 0 0 0 0,0.52916 V 4.7625 A 0.52916667,0.52916667 0 0 0 0.52917,5.29166 H 15.08125 A 0.52916667,0.52916667 0 0 0 15.61042,4.7625 V 0.52916 A 0.52916667,0.52916667 0 0 0 15.08125,0 Z" + clip-rule="evenodd" + id="path1" + style="display:inline;stroke-width:0.264583" + data-inkscape-label="Background" + /> + <g + data-inkscape-label="Glyphs" + data-inkscape-groupmode="layer" + id="layer1" + > + {#if lineType && lineType.toUpperCase() in fernverkehrIconMap} + <path + fill="#ffffff" + d={fernverkehrIconMap[ + lineType.toUpperCase() as keyof typeof fernverkehrIconMap + ]} + id="path2" + style="stroke-width:0.264583" + data-sodipodi-nodetypes="ccccccccccccccccccc" + data-inkscape-label="IR Glyph" + /> + {/if} + </g> + {#if line !== undefined && line !== ''} + <text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.23333px;line-height:0;font-family:SBB;-inkscape-font-specification:'SBB Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke-width:0.0264583" + x="9.5329876" + y="4.1801271" + id="text1-9" + data-inkscape-label="Number" + ><tspan + id="tspan1-8" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.28625px;line-height:0;font-family:SBB;-inkscape-font-specification:'SBB, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;text-anchor:start;fill:#ffffff;stroke-width:0.0264583" + x="9.5329876" + y="4.1801271" + data-sodipodi-role="line">{line}</tspan + ></text + > + {/if} + </g> + {:else} + <g + clip-path="url(#a)" + id="g2" + transform="scale(0.26458333)" + data-inkscape-label="Regioverkehr" + > + <title id="title1">A</title> + <path + stroke="#000000" + d="M 2,0.5 H 57 A 1.5,1.5 0 0 1 58.5,2 V 18 A 1.5,1.5 0 0 1 57,19.5 H 2 A 1.5,1.5 0 0 1 0.5,18 V 2 A 1.5,1.5 0 0 1 2,0.5 Z" + id="path1-8" + style="display:inline;fill:{nightIsFilled && isNightRendering + ? '#000' + : 'none'};stroke:{isNightRendering + ? nightIsFilled + ? '#000' + : '#FFDE15' + : currentColor};stroke-opacity:1" + data-inkscape-label="Background" + /> + <text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.32594px;line-height:0;font-family:SBB;-inkscape-font-specification:'SBB, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;direction:ltr;text-anchor:start;display:inline;fill:{isNightRendering + ? '#FFDE15' + : currentColor};fill-opacity:1;stroke:{/*isNightRendering + ? nightIsFilled + '#FFDE15' + : currentColor*/ 'none'};stroke-width:0.0264583;stroke-opacity:1" + x="0.80801982" + y="4.2193737" + id="text1" + data-inkscape-label="Kind" + transform="scale(3.7795276)" + ><tspan + data-sodipodi-role="line" + id="tspan1" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.32594px;font-family:SBB;-inkscape-font-specification:'SBB, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.0264583" + x="0.80801982" + y="4.2193737">{routeShortName}</tspan + ></text + > + </g> + {/if} +</svg> diff --git a/src/lib/assets/LineGlyphSrc.svg b/src/lib/assets/LineGlyphSrc.svg new file mode 100644 index 0000000..8d7bc59 --- /dev/null +++ b/src/lib/assets/LineGlyphSrc.svg @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="15.61mm" + height="5.2919998mm" + viewBox="0 0 15.61 5.2919998" + version="1.1" + id="svg1" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + sodipodi:docname="LineGlyphSrc.svg" + xml:space="preserve" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview + id="namedview1" + pagecolor="#505050" + bordercolor="#ffffff" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="1" + inkscape:deskcolor="#505050" + inkscape:document-units="mm" + inkscape:zoom="16.635742" + inkscape:cx="20.077253" + inkscape:cy="1.5929557" + inkscape:window-width="1920" + inkscape:window-height="1014" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" /><defs + id="defs1"><clipPath + id="a"><path + fill="#fff" + d="M0 0h59v20H0z" + id="path3" /></clipPath><clipPath + id="a-6"><path + fill="#fff" + d="M0 0h59v20H0z" + id="path3-8" /></clipPath></defs><g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="Fernverkehr" + style="display:inline" + transform="scale(0.99997309,1)"><path + fill="#eb0000" + fill-rule="evenodd" + d="M 0.52917,0 A 0.52916667,0.52916667 0 0 0 0,0 |