97 lines
No EOL
3.2 KiB
TypeScript
97 lines
No EOL
3.2 KiB
TypeScript
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<OrsResponse>(
|
|
`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))
|
|
}) |