import { z } from 'zod' import { EMISSION_FACTORS } from '#shared/emissions' import { thin } from '#shared/polyline' import { estimateTrainTrip } from '../utils/estimate' import type { Coord } from '#shared/types' const BodySchema = z.object({ from: z.object({ lat: z.number(), lon: z.number() }), to: z.object({ lat: z.number(), lon: z.number() }), }) interface OrsResponse { features: Array<{ properties: { summary: { distance: number, duration: number } } geometry: { coordinates: Coord[] } }> } /** Interroge OpenRouteService pour un profil donne. */ async function route(profile: string, body: unknown, key: string) { const data = await $fetch( `https://api.openrouteservice.org/v2/directions/${profile}/geojson`, { method: 'POST', headers: { Authorization: key, // ORS refuse de repondre si on n'annonce pas accepter ce format precis. Accept: 'application/geo+json', }, body, }, ) const feature = data.features[0] if (!feature) throw new Error('Itinéraire introuvable') return { km: feature.properties.summary.distance / 1000, min: feature.properties.summary.duration / 60, // ORS renvoie deja du GeoJSON [lon, lat] : rien a decoder, juste a alleger. geometry: thin(feature.geometry.coordinates, 800), } } const unavailable = (mode: string, reason: string) => ({ mode, unavailable: true, reason }) export default defineEventHandler(async (event) => { const parsed = BodySchema.safeParse(await readBody(event)) if (!parsed.success) { throw createError({ statusCode: 400, statusMessage: 'Départ ou arrivée invalide.' }) } const { from, to } = parsed.data const { orsApiKey } = useRuntimeConfig(event) const body = { coordinates: [[from.lon, from.lat], [to.lon, to.lat]] } // allSettled et non all : un fournisseur en panne ne doit pas vider la page. const [car, bike, pt] = await Promise.allSettled([ route('driving-car', body, orsApiKey), route('cycling-regular', body, orsApiKey), publicTransport(from, to, new Date()), ]) const options = [] options.push(car.status === 'fulfilled' ? { mode: 'electric_car', distanceKm: Math.round(car.value.km * 10) / 10, durationMin: Math.round(car.value.min), co2Grams: Math.round(car.value.km * EMISSION_FACTORS.electric_car), geometry: car.value.geometry, } : unavailable('electric_car', 'Aucune route carrossable trouvée.')) options.push(bike.status === 'fulfilled' ? { mode: 'bike', distanceKm: Math.round(bike.value.km * 10) / 10, durationMin: Math.round(bike.value.min), co2Grams: 0, geometry: bike.value.geometry, } : unavailable('bike', 'Aucun itinéraire cyclable trouvé.')) if (pt.status === 'fulfilled') { options.push(pt.value.train ?? estimateTrainTrip(from, to)) options.push(pt.value.bus ?? unavailable('bus', 'Pas de ligne régulière.')) } else { options.push(unavailable('train', 'Horaires indisponibles.')) options.push(unavailable('bus', 'Horaires indisponibles.')) } return options.sort((a, b) => ('co2Grams' in a ? a.co2Grams : Infinity) - ('co2Grams' in b ? b.co2Grams : Infinity)) })