Files
ridebus-pwa/server/features.mjs
Zakhar Timoshenko 6fe039cefa feat: add StopsMap and WalkingDirections components for enhanced map functionality
- Implemented StopsMap component to display bus stops on a map using MapLibre GL.
- Integrated walking directions feature in WalkingDirections component to calculate routes from user location to selected bus stop.
- Added geo utility functions for coordinate validation and distance calculation.
- Created route grouping and stop filtering logic in routeGroups module.
- Developed custom hooks for managing stop routes with React Query.
- Added tests for routing and stop functionalities to ensure reliability.
2026-10-08 00:18:10 +03:00

39 lines
2.8 KiB
JavaScript

import { ApiError } from './errors.mjs';
import { coordinates } from './coordinates.mjs';
const routeNumber = value => typeof value === 'string' ? value.replace(/[\s№]/g, '').toLocaleUpperCase('ru') : '';
const positive = value => Number.isSafeInteger(Number(value)) && Number(value) > 0;
export function normalizeAlerts(raw, cityId) {
if (!Array.isArray(raw)) throw new ApiError(502);
const seen = new Set();
return raw.flatMap(alert => {
if (!alert || !positive(alert.id)) throw new ApiError(502);
if (alert.is_active === false || (alert.city_id != null && Number(alert.city_id) !== cityId) || seen.has(Number(alert.id))) return [];
if ((alert.route_numbers != null && (!Array.isArray(alert.route_numbers) || alert.route_numbers.some(value => !routeNumber(value))))
|| (alert.route_ids != null && (!Array.isArray(alert.route_ids) || alert.route_ids.some(value => !positive(value))))) throw new ApiError(502);
const numbers = Array.isArray(alert.route_numbers) ? alert.route_numbers.map(routeNumber).filter(Boolean) : [];
const ids = Array.isArray(alert.route_ids) ? alert.route_ids.filter(positive).map(Number) : [];
if (!numbers.length && routeNumber(alert.route_number)) numbers.push(routeNumber(alert.route_number));
if (!ids.length && positive(alert.route_id)) ids.push(Number(alert.route_id));
const startTime = Date.parse(alert.start_time);
const endTime = alert.end_time == null ? null : Date.parse(alert.end_time);
if (!Number.isFinite(startTime) || (endTime !== null && !Number.isFinite(endTime)) || typeof alert.title !== 'string' || typeof alert.message !== 'string') throw new ApiError(502);
seen.add(Number(alert.id));
return [{ id: Number(alert.id), cityId, title: alert.title.trim(), message: alert.message.trim(),
type: ['cancellation', 'delay', 'detour'].includes(alert.alert_type) ? alert.alert_type : 'info',
routeNumbers: [...new Set(numbers)].sort((a, b) => a.localeCompare(b, 'ru', { numeric: true })), routeIds: [...new Set(ids)], isCityWide: !numbers.length && !ids.length,
startTime: new Date(startTime).toISOString(), endTime: endTime === null ? null : new Date(endTime).toISOString() }];
});
}
export function normalizeNearby(raw) {
if (!Array.isArray(raw)) throw new ApiError(502);
return raw.map(stop => {
const distance = Number(stop?.distance_meters);
if (!stop || !positive(stop.id) || typeof stop.name !== 'string' || !stop.name.trim() || stop.distance_meters == null || !Number.isFinite(distance) || distance < 0) throw new ApiError(502);
return { id: Number(stop.id), name: stop.name.trim(), direction: typeof stop.description === 'string' ? stop.description.trim() : '',
routesCount: Number(stop.routes_count) || 0, distance: Math.round(distance), ...coordinates(stop.latitude, stop.longitude) };
}).sort((a, b) => a.distance - b.distance);
}