feat: add service window component and related functionality
- Introduced ServiceWindow component to display the last departure today and the first tomorrow. - Implemented cancellation notice parsing to handle route cancellations effectively. - Enhanced departure display logic to include cancellation alerts and status updates. - Created information blocks for displaying service information, weather, and QR codes. - Added tests for departure features, information handling, and service window functionality. - Updated fixtures for testing various scenarios including cancellations and long wait times.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { loadTimetable } from './timetable.mjs';
|
||||
import { parseCancellationNotice } from '../src/lib/cancellations.mjs';
|
||||
import { shiftDate } from '../src/lib/timetable.mjs';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(status = 502, code = 'data_unavailable') { super(code); this.status = status; this.code = code; }
|
||||
@@ -38,8 +40,11 @@ export function normalizeDepartures(raw, now, stopNames = new Map()) {
|
||||
const terminal = stopNames.get(Number(arrival.trip?.endStopId));
|
||||
const destination = terminal || route.name.split(/\s+[-–—]\s+/).at(-1) || route.name;
|
||||
const identity = [route.id, scheduledAt, arrival.trip?.departureTime || '', arrival.trip?.startStopId || '', arrival.trip?.endStopId || '', note].join(':');
|
||||
const originTime = scheduledInstant(arrival.trip?.departureTime, now)?.slice(11, 16);
|
||||
departures.push({ id: identity, routeId: route.id, routeNumber: route.number, routeName: route.name,
|
||||
destination, type: route.type, scheduledAt, scheduledTime: clean(arrival.scheduledTime).slice(0, 5), note,
|
||||
destination, type: route.type, scheduledAt, scheduledTime: scheduledAt.slice(11, 16), note,
|
||||
serviceDate: originTime && originTime > scheduledAt.slice(11, 16) ? shiftDate(localDate(now), -1) : localDate(now),
|
||||
originStopId: Number(arrival.trip?.startStopId) || undefined, originTime,
|
||||
source: 'schedule' });
|
||||
}
|
||||
}
|
||||
@@ -47,7 +52,7 @@ export function normalizeDepartures(raw, now, stopNames = new Map()) {
|
||||
routes: routes.sort((a, b) => a.number.localeCompare(b.number, 'ru', { numeric: true })), partial };
|
||||
}
|
||||
|
||||
export function normalizeAlerts(raw, routes, cityId, now) {
|
||||
export function normalizeAlerts(raw, routes, cityId, now, stops = []) {
|
||||
if (!Array.isArray(raw)) throw new ApiError();
|
||||
const routeIds = new Set(routes.map(route => route.id));
|
||||
const routeNumbers = new Set(routes.map(route => route.number));
|
||||
@@ -66,10 +71,12 @@ export function normalizeAlerts(raw, routes, cityId, now) {
|
||||
if (!numbers.length && clean(alert.route_number)) numbers.push(clean(alert.route_number));
|
||||
const targets = ids(alert.route_ids);
|
||||
if (!targets.length && Number(alert.route_id) > 0) targets.push(Number(alert.route_id));
|
||||
if (!numbers.length) numbers.push(...routes.filter(route => targets.includes(route.id)).map(route => route.number));
|
||||
const value = { id: String(alert.id), title: clean(alert.title) || 'Изменения движения', message: clean(alert.message),
|
||||
type: ['cancellation', 'delay', 'detour'].includes(alert.alert_type) ? alert.alert_type : 'info',
|
||||
routeNumbers: [...new Set(numbers)], routeIds: [...new Set(targets)],
|
||||
startAt: alert.start_time || null, endAt: alert.end_time || null };
|
||||
if (value.type === 'cancellation') value.cancellations = parseCancellationNotice(value.message, value.routeNumbers, stops);
|
||||
return [value.id, value];
|
||||
})).values()];
|
||||
}
|
||||
@@ -139,7 +146,7 @@ export function createBoardClient({
|
||||
const data = normalizeDepartures(eta, generatedAt, new Map(stops.map(stop => [Number(stop.id), clean(stop.name)])));
|
||||
const stopRoutes = Array.isArray(stop.routes) ? stop.routes.filter(route => route?.is_active !== false).map(routeInfo) : data.routes;
|
||||
let alerts = [], alertsAvailable = true;
|
||||
try { alerts = normalizeAlerts(await cached('alerts', 20000, () => upstream('api/v1/alerts/active')), stopRoutes, cityId, now()); }
|
||||
try { alerts = normalizeAlerts(await cached('alerts', 20000, () => upstream('api/v1/alerts/active')), stopRoutes, cityId, now(), stops); }
|
||||
catch { alertsAvailable = false; }
|
||||
return { cityId, cityName, stop: { id: stopId, name: clean(stop.name), direction: clean(stop.description) },
|
||||
...data, alerts, alertsAvailable, updatedAt: new Date(generatedAt).toISOString(), serviceDate: localDate(generatedAt), mode: 'live' };
|
||||
|
||||
@@ -33,7 +33,9 @@ function normalizeSchedules(raw, route, rawStops, stopId) {
|
||||
return [...new Set(value.map(Number))];
|
||||
};
|
||||
const terminal = end || [...rawStops].sort((a, b) => Number(b.sequence) - Number(a.sequence))[0];
|
||||
const origin = start || [...rawStops].sort((a, b) => Number(a.sequence) - Number(b.sequence))[0];
|
||||
return [{ arrivalSeconds: Number(time[1]) * 3600 + Number(time[2]) * 60 + Number(time[3] || 0) + offset * 60,
|
||||
originStopId: Number(origin?.stop_id), originTime: `${time[1].padStart(2, '0')}:${time[2]}`,
|
||||
destination: terminal?.name?.trim() || route.name.split(/\s+[-–—]\s+/).at(-1) || route.name,
|
||||
note: typeof trip.note === 'string' ? trip.note.trim() : '', daysOfWeek: days(trip.daysOfWeek), exceptDaysOfWeek: days(trip.exceptDaysOfWeek) }];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user