Created
August 11, 2025 16:11
-
-
Save DomeQdev/4964f0cd20f7c1f9d6630a1c676b3f67 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Stop } from "@/typings"; | |
| import { proxyRequest } from "@/modules/Proxy"; | |
| import db, { stopTable } from "@/db"; | |
| import { eq } from "drizzle-orm"; | |
| import ensureRealtimeData, { | |
| ERealtimeItineraryStop, | |
| EStopTime, | |
| RealtimeCityData, | |
| RealtimeRoute, | |
| RealtimeTrip, | |
| } from "./GTFSRealtime/tools/ensureRealtimeData"; | |
| import { distance } from "fastest-levenshtein"; | |
| import { calculateDistance, slugify } from "@/util/tools"; | |
| import GTFSRealtime from "./GTFSRealtime"; | |
| /** | |
| * Ten kod to coś strasznego i jestem tego świadomy. | |
| * Uruchomienie tego bez pozostałej cześci (lepiej napisanego) backendu zbiorkomu __nie jest możliwe__ i nie będę pomagał w jego uruchomieniu/przepisaniu. | |
| */ | |
| type Options = { | |
| city: string; | |
| agency: string; | |
| matchStops: (kiedyPrzyjedzieStop: KiedyPrzyjedzieStop, stop: Stop) => boolean; | |
| }; | |
| export enum EKiedyPrzyjedzieStop { | |
| id, | |
| designator, | |
| name, | |
| lng, | |
| lat, | |
| onlyDisembarking, | |
| isStation, | |
| } | |
| export type KiedyPrzyjedzieStop = [ | |
| id: string, | |
| designator: number, | |
| name: string, | |
| lng: number, | |
| lat: number, | |
| onlyDisembarking: 0 | 1, | |
| isStation: 0 | 1, | |
| ]; | |
| type Trip = { | |
| executionId: string; | |
| vehicleId: string; | |
| block: string; | |
| blockIndex: number; | |
| hasEnded: boolean; | |
| zbiorkomTripId: string; | |
| }; | |
| type Execution = { | |
| trip: { | |
| times: { | |
| stop_name: string; | |
| external: boolean; | |
| designator: number; | |
| place_id: string; | |
| departure_time: string; | |
| index: number; | |
| }[]; | |
| direction: string; | |
| current_station_id: number; | |
| line: { | |
| name: string; | |
| show_name: boolean; | |
| }; | |
| }; | |
| vehicle: { | |
| lon: number; | |
| lat: number; | |
| }; | |
| next_departure_index: number; | |
| }; | |
| const cache = {} as Record< | |
| string, | |
| { | |
| tripsCache: Record<string, Trip>; | |
| stopsIdsDictionary: Record<string, [number, boolean]>; | |
| } | |
| >; | |
| export default async (options: Options) => { | |
| let cityCache = cache[options.agency]; | |
| if (!cityCache) { | |
| cache[options.agency] = { | |
| tripsCache: {}, | |
| stopsIdsDictionary: {}, | |
| }; | |
| cityCache = cache[options.agency]; | |
| } | |
| await Promise.all([ensureStops(options), ensureRealtimeData(options.city)]); | |
| const realtimeCityData = global.realtimeCityData[options.city]; | |
| await fetchNewTrips(options, realtimeCityData); | |
| const realtime = new GTFSRealtime(options.city, "indexString"); | |
| const seenVehicles = new Set<string>(); | |
| realtime.addPositions( | |
| await Promise.all( | |
| Object.entries(cityCache.tripsCache) | |
| .sort( | |
| ([, tripA], [, tripB]) => | |
| realtimeCityData.trips[tripA.zbiorkomTripId].stopTimes[0][0] - | |
| realtimeCityData.trips[tripB.zbiorkomTripId].stopTimes[0][0] | |
| ) | |
| .filter(([, trip]) => { | |
| if (trip.hasEnded) return false; | |
| if (seenVehicles.has(trip.vehicleId)) return false; | |
| seenVehicles.add(trip.vehicleId); | |
| return true; | |
| }) | |
| .map(async ([tripId, trip]) => { | |
| const egzekucja_ale_kogo = await proxyRequest({ | |
| url: `https://${options.agency.toLowerCase()}.kiedyprzyjedzie.pl/api/trip_execution/${trip.executionId}/0`, | |
| }).then((res) => res.data as Execution); | |
| if (egzekucja_ale_kogo.next_departure_index === egzekucja_ale_kogo.trip.times.length) { | |
| cityCache.tripsCache[tripId].hasEnded = true; | |
| } | |
| if (!egzekucja_ale_kogo.vehicle) { | |
| return null as any; | |
| } | |
| return { | |
| id: trip.vehicleId, | |
| location: [egzekucja_ale_kogo.vehicle.lon, egzekucja_ale_kogo.vehicle.lat] as [ | |
| number, | |
| number, | |
| ], | |
| indexString: [tripId, ""], | |
| route: options.agency + slugify(egzekucja_ale_kogo.trip.line.name), | |
| }; | |
| }) | |
| ).then((positions) => positions.filter((x) => x !== null)) | |
| ); | |
| await realtime.process(); | |
| }; | |
| const ensureStops = async (options: Options) => { | |
| let cityCache = cache[options.agency]; | |
| if (!cityCache) { | |
| cache[options.agency] = { | |
| tripsCache: {}, | |
| stopsIdsDictionary: {}, | |
| }; | |
| cityCache = cache[options.agency]; | |
| } | |
| if (Object.keys(cityCache.stopsIdsDictionary).length) return; | |
| const kiedyPrzyjedzieStops = await proxyRequest({ | |
| url: `https://${options.agency.toLowerCase()}.kiedyprzyjedzie.pl/stops`, | |
| }).then((res) => res.data as { stops: KiedyPrzyjedzieStop[] }); | |
| const zbiorkomStops = await db.query.stopTable.findMany({ where: eq(stopTable.city, options.city) }); | |
| for (const stop of kiedyPrzyjedzieStops.stops) { | |
| const candidates: { | |
| stop: KiedyPrzyjedzieStop; | |
| zbiorkomStop: Stop; | |
| distance: number; | |
| }[] = []; | |
| for (const zbiorkomStop of zbiorkomStops) { | |
| if (options.matchStops(stop, zbiorkomStop)) { | |
| candidates.push({ | |
| stop, | |
| zbiorkomStop, | |
| distance: calculateDistance( | |
| [stop[EKiedyPrzyjedzieStop.lng] / 1e6, stop[EKiedyPrzyjedzieStop.lat] / 1e6], | |
| zbiorkomStop.location | |
| ), | |
| }); | |
| } | |
| } | |
| let bestCandidate: Stop | undefined; | |
| let bestCandidateDistance = Infinity; | |
| for (const candidate of candidates) { | |
| if (candidate.distance < bestCandidateDistance) { | |
| bestCandidate = candidate.zbiorkomStop; | |
| bestCandidateDistance = candidate.distance; | |
| } | |
| } | |
| if (!bestCandidate) continue; | |
| cityCache.stopsIdsDictionary[bestCandidate.id] = [ | |
| stop[EKiedyPrzyjedzieStop.designator], | |
| stop[EKiedyPrzyjedzieStop.isStation] === 1, | |
| ]; | |
| } | |
| }; | |
| type StopDeparture = { | |
| static_time: string; // x min | |
| direction_id: string; // headsign | |
| vehicle_id: number; | |
| line_name: string; | |
| trip_execution_id: string; | |
| block_code: string; | |
| }; | |
| const fetchNewTrips = async (options: Options, realtimeCityData: RealtimeCityData) => { | |
| let cityCache = cache[options.agency]; | |
| if (!cityCache) { | |
| cache[options.agency] = { | |
| tripsCache: {}, | |
| stopsIdsDictionary: {}, | |
| }; | |
| cityCache = cache[options.agency]; | |
| } | |
| const activeTrips = Object.values(realtimeCityData.trips).filter((trip) => { | |
| if ( | |
| cityCache.tripsCache[trip.gtfsId] || | |
| realtimeCityData.routes[trip.route].agency !== options.agency | |
| ) { | |
| return false; | |
| } | |
| const start = trip.stopTimes[0][EStopTime.arrival]; | |
| return start > Date.now() && start < Date.now() + 30 * 60 * 1000; | |
| }); | |
| if (!activeTrips.length) return; | |
| const stopsDeparturesCache: Record<string, StopDeparture[]> = {}; | |
| for (const trip of activeTrips) { | |
| if (cityCache.tripsCache[trip.gtfsId]) continue; | |
| const [designator, isStation] = | |
| cityCache.stopsIdsDictionary[ | |
| realtimeCityData.itineraries[trip.itinerary].stops[0][ERealtimeItineraryStop.id] | |
| ] ?? []; | |
| if (!designator) continue; | |
| let departures: StopDeparture[] = stopsDeparturesCache[designator]; | |
| if (!departures) { | |
| departures = await proxyRequest({ | |
| url: `https://kiedyprzyjedzie.pl/admin/${options.agency.toLowerCase()}/fleet/departures?designator=${designator}&is_station=${isStation ? 1 : 0}`, | |
| }).then((res) => { | |
| for (const row of res.data.rows) { | |
| row.direction_id = res.data.directions[row.direction_id]; | |
| } | |
| return res.data.rows as StopDeparture[]; | |
| }); | |
| stopsDeparturesCache[designator] = departures; | |
| } | |
| const tripData = matchDepartureToTrip(trip, realtimeCityData.routes[trip.route], departures); | |
| if (!tripData) continue; | |
| cityCache.tripsCache[trip.gtfsId] = tripData; | |
| } | |
| }; | |
| const matchDepartureToTrip = ( | |
| trip: RealtimeTrip, | |
| route: RealtimeRoute, | |
| departures: StopDeparture[] | |
| ): Trip | undefined => { | |
| const departure = trip.stopTimes[0][EStopTime.departure]; | |
| const minutesToDeparture = Math.round((departure - Date.now()) / 1000 / 60); | |
| const minMinutes = minutesToDeparture - 1; | |
| const maxMinutes = minutesToDeparture + 1; | |
| const candidates: StopDeparture[] = []; | |
| for (const dep of departures) { | |
| const minutesToDep = dep.static_time ? +dep.static_time.split(" ")[0] : 0; | |
| if ( | |
| dep.line_name === route.name && | |
| ((minutesToDep === 0 && minutesToDeparture < 0) || | |
| (minutesToDep >= minMinutes && minutesToDep <= maxMinutes)) | |
| ) { | |
| candidates.push(dep); | |
| } | |
| } | |
| let bestCandidate: StopDeparture | undefined; | |
| let bestCandidateScore = Infinity; | |
| for (const candidate of candidates) { | |
| const score = distance(candidate.direction_id, trip.headsign); | |
| if (score < bestCandidateScore) { | |
| bestCandidateScore = score; | |
| bestCandidate = candidate; | |
| } | |
| } | |
| if (!bestCandidate) return; | |
| return { | |
| executionId: btoa(bestCandidate.trip_execution_id), | |
| block: bestCandidate.block_code, | |
| vehicleId: `3/${bestCandidate.vehicle_id}`, | |
| blockIndex: +bestCandidate.trip_execution_id.split(":").last(), | |
| hasEnded: false, | |
| zbiorkomTripId: trip.id, | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment