Import initial : code en production sur Hetzner
This commit is contained in:
commit
cf6ee64e31
43 changed files with 14404 additions and 0 deletions
94
shared/booking.ts
Normal file
94
shared/booking.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import type { Place } from './types'
|
||||
|
||||
export interface BookingLink {
|
||||
url: string
|
||||
label: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
/** Normalise un nom de ville pour l'inclure dans une URL slug. */
|
||||
function slugify(label: string): string {
|
||||
return label
|
||||
.normalize('NFD').replace(/\p{Diacritic}/gu, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
|
||||
/** Extrait le nom de ville avant la virgule ("Lille, France" -> "Lille"). */
|
||||
function cityName(label: string): string {
|
||||
return label.split(',')[0]?.trim() ?? label.trim()
|
||||
}
|
||||
|
||||
/** Format de date que Kombo et CheckMyBus acceptent tous les deux. */
|
||||
function dateISO(d: Date): string {
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** Kombo comprend les noms de ville en clair : pas de mapping de gares a maintenir. */
|
||||
export function buildKomboLink(from: Place, to: Place, date: Date): BookingLink {
|
||||
const f = encodeURIComponent(cityName(from.label))
|
||||
const t = encodeURIComponent(cityName(to.label))
|
||||
const d = dateISO(date)
|
||||
return {
|
||||
url: `https://www.kombo.co/fr/app/outward/1/1/18/${d}/${f}/${t}/0/0/results`,
|
||||
label: 'Reserver sur Kombo',
|
||||
provider: 'Kombo',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckMyBus accepte un fallback avec UUID zeros quand on ne connait pas
|
||||
* son identifiant interne : le nom + les coordonnees suffisent a lancer
|
||||
* la recherche. Le fragment porte tout, y compris la date.
|
||||
*/
|
||||
export function buildCheckMyBusLink(from: Place, to: Place, date: Date): BookingLink {
|
||||
const zeroId = '00000000-0000-0000-0000-000000000000'
|
||||
const encodeStop = (p: Place) =>
|
||||
encodeURIComponent(`${cityName(p.label)}, France`)
|
||||
+ `%24${p.lat}%2C${p.lon}`
|
||||
+ `%24${zeroId}%24${zeroId}%24true%24false`
|
||||
|
||||
const fromSlug = slugify(cityName(from.label))
|
||||
const toSlug = slugify(cityName(to.label))
|
||||
const d = dateISO(date)
|
||||
|
||||
const fragment = [
|
||||
`departureDate=${d}`,
|
||||
`origin=${encodeStop(from)}`,
|
||||
`destination=${encodeStop(to)}`,
|
||||
'sortValue=Relevance',
|
||||
'sortOrder=ascending',
|
||||
'adults=1',
|
||||
].join('&')
|
||||
|
||||
return {
|
||||
url: `https://www.checkmybus.fr/${fromSlug}/${toSlug}?mode=search#${fragment}`,
|
||||
label: 'Chercher sur CheckMyBus',
|
||||
provider: 'CheckMyBus',
|
||||
}
|
||||
}
|
||||
|
||||
/** Chargemap : on ouvre juste la page d'accueil du planificateur. */
|
||||
export function buildChargemapLink(): BookingLink {
|
||||
return {
|
||||
url: 'https://chargemap.com/map',
|
||||
label: 'Ouvrir Chargemap',
|
||||
provider: 'Chargemap',
|
||||
}
|
||||
}
|
||||
|
||||
/** Retourne le lien approprie pour le mode donne, ou null si aucun. */
|
||||
export function buildBookingLink(
|
||||
mode: 'train' | 'bus' | 'electric_car' | 'bike',
|
||||
from: Place,
|
||||
to: Place,
|
||||
date: Date,
|
||||
): BookingLink | null {
|
||||
switch (mode) {
|
||||
case 'train': return buildKomboLink(from, to, date)
|
||||
case 'bus': return buildCheckMyBusLink(from, to, date)
|
||||
case 'electric_car': return buildChargemapLink()
|
||||
case 'bike': return null
|
||||
}
|
||||
}
|
||||
38
shared/emissions.ts
Normal file
38
shared/emissions.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Grammes de CO₂ équivalent par passager et par kilomètre.
|
||||
* Source : ADEME, Base Carbone.
|
||||
*/
|
||||
export const EMISSION_FACTORS = {
|
||||
electric_car: 103,
|
||||
bike: 0,
|
||||
train: 2.4,
|
||||
bus: 113,
|
||||
} as const
|
||||
|
||||
export type TransportMode = keyof typeof EMISSION_FACTORS
|
||||
|
||||
/**
|
||||
* Facteurs par mode Transitous. Chaque tronçon est compté avec son propre
|
||||
* facteur : un TGV et un TER n'ont pas du tout la même empreinte.
|
||||
*/
|
||||
export const LEG_FACTORS: Record<string, number> = {
|
||||
HIGHSPEED_RAIL: 2.4,
|
||||
LONG_DISTANCE: 2.4,
|
||||
NIGHT_RAIL: 2.4,
|
||||
REGIONAL_RAIL: 24.8,
|
||||
REGIONAL_FAST_RAIL: 24.8,
|
||||
RAIL: 24.8,
|
||||
SUBWAY: 4.1,
|
||||
METRO: 4.1,
|
||||
TRAM: 3.6,
|
||||
BUS: 113,
|
||||
COACH: 35.4,
|
||||
WALK: 0,
|
||||
BIKE: 0,
|
||||
}
|
||||
|
||||
/** Mode inconnu : on prend le bus, le plus émetteur, pour ne pas sous-estimer. */
|
||||
export const DEFAULT_LEG_FACTOR = 113
|
||||
|
||||
/** Référence de comparaison : la voiture thermique. */
|
||||
export const PETROL_CAR = 218
|
||||
89
shared/polyline.ts
Normal file
89
shared/polyline.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { Coord } from './types'
|
||||
|
||||
/**
|
||||
* Decode le format « encoded polyline » de Google, utilise par MOTIS.
|
||||
* La precision n'est pas universelle : Google encode a 1e5, MOTIS a 1e7.
|
||||
* Se tromper d'un facteur 100 place le trace a des milliers de kilometres.
|
||||
*/
|
||||
export function decodePolyline(encoded: string, precision = 5): Coord[] {
|
||||
const factor = 10 ** precision
|
||||
const coords: Coord[] = []
|
||||
let index = 0
|
||||
let lat = 0
|
||||
let lon = 0
|
||||
|
||||
while (index < encoded.length) {
|
||||
let result = 0
|
||||
let shift = 0
|
||||
let byte: number
|
||||
|
||||
do {
|
||||
byte = encoded.charCodeAt(index++) - 63
|
||||
result |= (byte & 0x1F) << shift
|
||||
shift += 5
|
||||
} while (byte >= 0x20)
|
||||
lat += result & 1 ? ~(result >> 1) : result >> 1
|
||||
|
||||
result = 0
|
||||
shift = 0
|
||||
do {
|
||||
byte = encoded.charCodeAt(index++) - 63
|
||||
result |= (byte & 0x1F) << shift
|
||||
shift += 5
|
||||
} while (byte >= 0x20)
|
||||
lon += result & 1 ? ~(result >> 1) : result >> 1
|
||||
|
||||
// Ordre GeoJSON : longitude d'abord, comme l'attend MapLibre.
|
||||
coords.push([lon / factor, lat / factor])
|
||||
}
|
||||
|
||||
return coords
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduit le nombre de points d'un trace.
|
||||
* Un aller Lille-Paris peut compter plusieurs milliers de points ; au-dela
|
||||
* d'un millier, l'oeil ne voit plus la difference mais le reseau, si.
|
||||
*/
|
||||
export function thin(coords: Coord[], max = 800): Coord[] {
|
||||
if (coords.length <= max) return coords
|
||||
const step = Math.ceil(coords.length / max)
|
||||
const out = coords.filter((_, i) => i % step === 0)
|
||||
// Le dernier point est le terminus : il ne doit jamais sauter.
|
||||
if (out[out.length - 1] !== coords[coords.length - 1]) out.push(coords[coords.length - 1]!)
|
||||
return out
|
||||
}
|
||||
/**
|
||||
* Renvoie le point situe a un ratio donne de la distance cumulee le long
|
||||
* du trace. midpointAt(coords, 0.5) donne le vrai milieu geometrique, pas
|
||||
* le point d'index milieu qui atterrit dans le vide sur un trajet inegal.
|
||||
* Utilise Haversine simplifie en distance euclidienne : suffisant pour
|
||||
* placer une bulle a l'echelle d'un trajet inter-villes.
|
||||
*/
|
||||
export function midpointAt(coords: Coord[], ratio = 0.5): Coord {
|
||||
if (coords.length === 0) throw new Error('trace vide')
|
||||
if (coords.length === 1) return coords[0]!
|
||||
if (ratio <= 0) return coords[0]!
|
||||
if (ratio >= 1) return coords[coords.length - 1]!
|
||||
|
||||
const distances: number[] = [0]
|
||||
let total = 0
|
||||
for (let i = 1; i < coords.length; i++) {
|
||||
const [x1, y1] = coords[i - 1]!
|
||||
const [x2, y2] = coords[i]!
|
||||
total += Math.hypot(x2 - x1, y2 - y1)
|
||||
distances.push(total)
|
||||
}
|
||||
|
||||
const target = total * ratio
|
||||
for (let i = 1; i < distances.length; i++) {
|
||||
if (distances[i]! >= target) {
|
||||
const [x1, y1] = coords[i - 1]!
|
||||
const [x2, y2] = coords[i]!
|
||||
const segLength = distances[i]! - distances[i - 1]!
|
||||
const segRatio = segLength === 0 ? 0 : (target - distances[i - 1]!) / segLength
|
||||
return [x1 + (x2 - x1) * segRatio, y1 + (y2 - y1) * segRatio]
|
||||
}
|
||||
}
|
||||
return coords[coords.length - 1]!
|
||||
}
|
||||
36
shared/types.ts
Normal file
36
shared/types.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/** Un lieu résolu en coordonnées, tel que renvoyé par le géocodeur. */
|
||||
export interface Place {
|
||||
label: string
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
|
||||
/** Coordonnée au format GeoJSON : [longitude, latitude]. */
|
||||
export type Coord = [number, number]
|
||||
|
||||
/** Un tronçon d'un trajet en transport en commun, ou un rabattement à pied. */
|
||||
export interface Segment {
|
||||
/** Mode Transitous brut : SUBWAY, HIGHSPEED_RAIL, BUS, WALK... */
|
||||
mode: string
|
||||
/** Nom court affiché sur le véhicule : « M1 », « TGV 7204 », « 12 ». */
|
||||
line: string
|
||||
/** Libellé long, quand il apporte quelque chose : « METRO LIGNE 1 ». */
|
||||
lineLong?: string
|
||||
/** Exploitant commercial : ILEVIA, SNCF, FlixBus. */
|
||||
operator?: string
|
||||
operatorUrl?: string
|
||||
/** Couleur officielle de la ligne, telle que publiée dans le GTFS. */
|
||||
color?: string
|
||||
textColor?: string
|
||||
/** Direction affichée par le véhicule. */
|
||||
headsign?: string
|
||||
from: string
|
||||
to: string
|
||||
departure: string
|
||||
arrival: string
|
||||
durationMin: number
|
||||
/** true si l'horaire provient du temps réel et non du théorique. */
|
||||
realTime: boolean
|
||||
/** Tracé du tronçon, décodé et allégé. */
|
||||
geometry?: Coord[]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue