Import initial : code en production sur Hetzner
This commit is contained in:
commit
cf6ee64e31
43 changed files with 14404 additions and 0 deletions
97
server/api/compare.post.ts
Normal file
97
server/api/compare.post.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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))
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue