Skip to content

Instantly share code, notes, and snippets.

@DomeQdev
Created August 11, 2025 16:11
Show Gist options
  • Select an option

  • Save DomeQdev/4964f0cd20f7c1f9d6630a1c676b3f67 to your computer and use it in GitHub Desktop.

Select an option

Save DomeQdev/4964f0cd20f7c1f9d6630a1c676b3f67 to your computer and use it in GitHub Desktop.
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