Use MOTIS API now

This commit is contained in:
Emmy D'Anello 2024-11-11 21:24:01 +01:00
parent e3fd6a7f88
commit 1c99c5ca47
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
7 changed files with 116 additions and 193 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
REACT_APP_MOTIS_SERVER=https://motis.luemy.eu

View File

@ -6,10 +6,9 @@ import {frFR, LocalizationProvider} from "@mui/x-date-pickers"
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs" import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"
import 'dayjs/locale/fr' import 'dayjs/locale/fr'
import './App.css' import './App.css'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; import {QueryClient, QueryClientProvider} from "@tanstack/react-query"
import {createSyncStoragePersister} from "@tanstack/query-sync-storage-persister"; import Home from "./Home"
import {persistQueryClient} from "@tanstack/react-query-persist-client"; import dayjs from "dayjs"
import Home from "./Home";
function App() { function App() {
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -18,7 +17,7 @@ function App() {
element: <Home />, element: <Home />,
}, },
{ {
path: "/station/:theme/:stationSlug", path: "/station/:theme/:stationId",
element: <Station /> element: <Station />
} }
]) ])
@ -54,14 +53,7 @@ function App() {
}, },
}) })
const localStoragePersister = createSyncStoragePersister({ dayjs.locale('fr')
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister: localStoragePersister,
})
return <> return <>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>

View File

@ -1,9 +1,8 @@
import {Autocomplete, TextField} from "@mui/material"; import {Autocomplete, TextField} from "@mui/material";
import {useRef, useState} from "react"; import {useState} from "react";
function AutocompleteStation(params) { function AutocompleteStation(params) {
const [options, setOptions] = useState([]) const [options, setOptions] = useState([])
const previousController = useRef()
function onInputChange(event, value) { function onInputChange(event, value) {
if (!value) { if (!value) {
@ -11,17 +10,9 @@ function AutocompleteStation(params) {
return return
} }
if (previousController.current) fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/geocode?language=fr&text=${value}`)
previousController.current.abort()
const controller = new AbortController()
const signal = controller.signal
previousController.current = controller
fetch("/api/core/station/?search=" + value, {signal})
.then(response => response.json()) .then(response => response.json())
.then(data => data.results)
.then(setOptions) .then(setOptions)
.catch()
} }
return <> return <>
@ -29,7 +20,7 @@ function AutocompleteStation(params) {
id="stop" id="stop"
options={options} options={options}
onInputChange={onInputChange} onInputChange={onInputChange}
filterOptions={(x) => x} filterOptions={(x) => x.filter(stop => stop.type === "STOP").filter(stop => !stop.id.startsWith("node/"))}
getOptionKey={option => option.id} getOptionKey={option => option.id}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
groupBy={option => getOptionGroup(option)} groupBy={option => getOptionGroup(option)}
@ -40,7 +31,7 @@ function AutocompleteStation(params) {
} }
function getOptionGroup(option) { function getOptionGroup(option) {
return option.country return option.id.split('_')[0]
} }
export default AutocompleteStation; export default AutocompleteStation;

View File

@ -5,7 +5,7 @@ function Home() {
const navigate = useNavigate() const navigate = useNavigate()
function onStationSelected(event, station) { function onStationSelected(event, station) {
navigate(`/station/sncf/${station.slug}/`) navigate(`/station/sncf/${station.id}/`)
} }
return <> return <>

View File

@ -1,19 +1,19 @@
import {useNavigate, useParams, useSearchParams} from "react-router-dom" import {useNavigate, useParams, useSearchParams} from "react-router-dom"
import TrainsTable from "./TrainsTable" import TrainsTable from "./TrainsTable"
import TripsFilter from "./TripsFilter" import TripsFilter from "./TripsFilter"
import {useState} from "react"; import {useState} from "react"
import {Box, Button, FormLabel} from "@mui/material"; import {Box, Button, FormLabel} from "@mui/material"
import {DatePicker, TimePicker} from "@mui/x-date-pickers"; import {DateTimePicker} from "@mui/x-date-pickers"
import dayjs from "dayjs"; import dayjs from "dayjs"
import {useQuery, useQueryClient} from "@tanstack/react-query"; import {useQuery, useQueryClient} from "@tanstack/react-query"
import AutocompleteStation from "./AutocompleteStation"; import AutocompleteStation from "./AutocompleteStation"
function DateTimeSelector({station, date, time}) { function DateTimeSelector({datetime, setDatetime}) {
const navigate = useNavigate() const navigate = useNavigate()
function onStationSelected(event, station) { function onStationSelected(event, station) {
if (station !== null) if (station !== null)
navigate(`/station/sncf/${station.slug}/`) navigate(`/station/sncf/${station.id}/`)
} }
return <> return <>
@ -25,38 +25,31 @@ function DateTimeSelector({station, date, time}) {
<FormLabel> <FormLabel>
Modifier la date et l'heure de recherche : Modifier la date et l'heure de recherche :
</FormLabel> </FormLabel>
<DatePicker name="date" label="Date" format="YYYY-MM-DD" defaultValue={dayjs(`${date}`)} /> <DateTimePicker name="date" label="Date" onChange={setDatetime} value={datetime} />
<TimePicker name="time" label="Heure" format="HH:mm" defaultValue={dayjs(`${date} ${time}`)} />
<Button type="submit">Rechercher</Button> <Button type="submit">Rechercher</Button>
</Box> </Box>
</> </>
} }
function Station() { function Station() {
let {theme, stationSlug} = useParams() // eslint-disable-next-line no-unused-vars
let [searchParams, _setSearchParams] = useSearchParams() let {theme, stationId} = useParams()
const now = new Date() // eslint-disable-next-line no-unused-vars
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` let [searchParams, setSearchParams] = useSearchParams()
let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` const [datetime, setDatetime] = useState(dayjs())
let [date, setDate] = useState(searchParams.get('date') || dateNow)
let [time, setTime] = useState(searchParams.get('time') || timeNow)
useQueryClient() useQueryClient()
const stationQuery = useQuery({ const stationQuery = useQuery({
queryKey: ['station', stationSlug], queryKey: ['station', stationId],
queryFn: () => fetch(`/api/core/station/${stationSlug}/`) queryFn: () => fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/stoptimes?stopId=${stationId}&n=1`)
.then(response => response.json()), .then(response => response.json()),
enabled: !!stationSlug, enabled: !!stationId,
}) })
const station = stationQuery.data ?? {name: "Chargement…"} const station = stationQuery.data?.stopTimes[0].place ?? {name: "Chargement…"}
if (time === timeNow) { if (searchParams.get("time") === undefined) {
setInterval(() => { setInterval(() => {
const now = new Date() setDatetime(dayjs())
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
setDate(dateNow)
setTime(timeNow)
}, 5000) }, 5000)
} }
@ -67,10 +60,10 @@ function Station() {
</header> </header>
<main> <main>
<DateTimeSelector station={station} date={date} time={time} /> <DateTimeSelector datetime={datetime} setDatetime={setDatetime} />
<TripsFilter /> <TripsFilter />
<TrainsTable station={station} date={date} time={time} tableType="departures" /> <TrainsTable station={station} datetime={datetime} tableType="departures" />
<TrainsTable station={station} date={date} time={time} tableType="arrivals" /> <TrainsTable station={station} datetime={datetime} tableType="arrivals" />
</main> </main>
</div> </div>
) )

View File

@ -10,8 +10,9 @@ import {
Typography Typography
} from "@mui/material" } from "@mui/material"
import {CSSTransition, TransitionGroup} from 'react-transition-group' import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {useQueries, useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query"
import {useCallback, useEffect, useMemo, useRef, useState} from "react"; import {useCallback, useEffect, useMemo, useRef} from "react"
import dayjs from "dayjs"
const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({ const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({
'tbody &:nth-of-type(odd)': { 'tbody &:nth-of-type(odd)': {
@ -26,12 +27,12 @@ const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({
}, },
})); }));
function TrainsTable({station, date, time, tableType}) { function TrainsTable({station, datetime, tableType}) {
return <> return <>
<TableContainer> <TableContainer>
<Table> <Table>
<TrainsTableHeader tableType={tableType} /> <TrainsTableHeader tableType={tableType} />
<TrainsTableBody station={station} date={date} time={time} tableType={tableType} /> <TrainsTableBody station={station} datetime={datetime} tableType={tableType} />
</Table> </Table>
</TableContainer> </TableContainer>
</> </>
@ -49,25 +50,31 @@ function TrainsTableHeader({tableType}) {
</> </>
} }
function TrainsTableBody({station, date, time, tableType}) { function TrainsTableBody({station, datetime, tableType}) {
const filterTime = useCallback((train) => { const filterTime = useCallback((train) => {
if (tableType === "departures") if (tableType === "departures")
return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}` return dayjs(train.place.departure) >= datetime
else else
return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}` return dayjs(train.place.arrival) >= datetime
}, [date, time, tableType]) }, [datetime, tableType])
const updateTrains = useCallback(() => { const updateTrains = useCallback(() => {
return fetch(`/api/station/next_${tableType}/?station_slug=${station.slug}&date=${date}&time=${time}&offset=${0}&limit=${20}`) const query_params = new URLSearchParams({
stopId: station.stopId,
arriveBy: tableType === "arrivals",
time: datetime.format(),
n: 20,
}).toString()
return fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/stoptimes?${query_params}`)
.then(response => response.json()) .then(response => response.json())
.then(data => data.results) .then(data => data.stopTimes)
.then(data => [...data]) .then(data => [...data])
}, [station.id, date, time, tableType]) }, [station.stopId, datetime, tableType])
const trainsQuery = useQuery({ const trainsQuery = useQuery({
queryKey: ['trains', station.id, tableType], queryKey: ['trains', station.stopId, tableType],
queryFn: updateTrains, queryFn: updateTrains,
enabled: !!station.id, enabled: !!station.stopId,
}) })
const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data]) const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data])
@ -79,7 +86,7 @@ function TrainsTableBody({station, date, time, tableType}) {
const nullRef = useRef(null) const nullRef = useRef(null)
let table_rows = trains.map((train) => <CSSTransition key={train.id} timeout={500} classNames="shrink" nodeRef={nullRef}> let table_rows = trains.map((train) => <CSSTransition key={train.id} timeout={500} classNames="shrink" nodeRef={nullRef}>
<TrainRow train={train} tableType={tableType} date={date} time={time} /> <TrainRow train={train} tableType={tableType} />
</CSSTransition>) </CSSTransition>)
return <> return <>
@ -91,90 +98,42 @@ function TrainsTableBody({station, date, time, tableType}) {
</> </>
} }
function TrainRow({train, tableType, date, time}) { function TrainRow({train, tableType}) {
const tripQuery = useQuery({ const tripQuery = useQuery({
queryKey: ['trip', train.trip], queryKey: ['tripId', train.tripId],
queryFn: () => fetch(`/api/gtfs/trip/${train.trip}/`) queryFn: () => fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/trip?${new URLSearchParams({tripId: train.tripId})}`)
.then(response => response.json()), .then(response => response.json()),
enabled: !!train.trip, enabled: !!train.tripId,
}) })
const trip = tripQuery.data ?? {} const trip = tripQuery.data ?? {}
const leg = trip.legs ? trip.legs[0] : null
const routeQuery = useQuery({ const trainType = getTrainType(train)
queryKey: ['route', trip.route], const backgroundColor = getBackgroundColor(train)
queryFn: () => fetch(`/api/gtfs/route/${trip.route}/`) const textColor = getTextColor(train)
.then(response => response.json()),
enabled: !!trip.route,
})
const route = routeQuery.data ?? {}
const trainType = getTrainType(train, trip, route)
const backgroundColor = getBackgroundColor(train, trip, route)
const textColor = getTextColor(train, trip, route)
const trainTypeDisplay = getTrainTypeDisplay(trainType) const trainTypeDisplay = getTrainTypeDisplay(trainType)
const stopTimesQuery = useQuery({ const stops = useMemo(() => leg ? [leg.from, ...leg.intermediateStops, leg.to] : [], [leg])
queryKey: ['stop_times', trip.id], const stopIndex = useMemo(() => {
queryFn: () => fetch(`/api/gtfs/stop_time/?${new URLSearchParams({trip: trip.id, order: 'stop_sequence', limit: 1000})}`) if (stops.length === 0 || train.place.stopId === undefined)
.then(response => response.json()) return -1
.then(data => data.results), for (let i = 0; i < stops.length; i++) {
enabled: !!trip.id, const index = tableType === "departures" ? i : stops.length - 1 - i
}) const stop = stops[index]
const stopTimes = stopTimesQuery.data ?? [] let timeCond = tableType === "departures" ? stop.scheduledDeparture === train.place.scheduledDeparture
const stopIds = stopTimes.map(stop_time => stop_time.stop) : stop.scheduledArrival === train.place.scheduledArrival
if (stop.stopId === train.place.stopId && timeCond)
return index
}
}, [stops, train, tableType])
const nextStops = tableType === "departures" ? stops.slice(stopIndex + 1) : stops.slice(0, stopIndex)
const stopQueries = useQueries({ let headline = nextStops[tableType === "departures" ? nextStops.length - 1 : 0] ?? {name: "Chargement…"}
queries: stopIds.map(stopId => ({
queryKey: ['stop', stopId],
queryFn: () => fetch(`/api/gtfs/stop/${stopId}/`)
.then(response => response.json()),
enabled: !!stopId,
})),
})
const stops = stopTimes.map(((stopTime, i) => ({...stopTime, stop: stopQueries[i]?.data ?? {"name": "…"}}))) ?? []
let headline = stops[tableType === "departures" ? stops.length - 1 : 0]?.stop ?? {name: "Chargement…"} const canceled = false // TODO Implémenter l'annulation
const [delayed, prettyDelay] = getPrettyDelay(train, tableType)
const realtimeTripQuery = useQuery({ let stopsNames = nextStops.map(stopTime => stopTime?.name ?? "").join(" > ") ?? ""
queryKey: ['realtimeTrip', trip.id, date, time],
queryFn: () => fetch(`/api/gtfs-rt/trip_update/${trip.id}/`)
.then(response => response.json()),
enabled: !!trip.id,
})
const [realtimeTripData, setRealtimeTripData] = useState({})
useEffect(() => {
if (realtimeTripQuery.data)
setRealtimeTripData(realtimeTripQuery.data)
}, [realtimeTripQuery.data])
const tripScheduleRelationship = realtimeTripData.schedule_relationship ?? 0
const realtimeQuery = useQuery({
queryKey: ['realtime', train.id, date, time],
queryFn: () => fetch(`/api/gtfs-rt/stop_time_update/${train.id}/`)
.then(response => response.json()),
enabled: !!train.id,
})
const [realtimeData, setRealtimeData] = useState({})
useEffect(() => {
if (realtimeQuery.data)
setRealtimeData(realtimeQuery.data)
}, [realtimeQuery.data])
const stopScheduleRelationship = realtimeData.schedule_relationship ?? 0
const canceled = tripScheduleRelationship === 3 || stopScheduleRelationship === 1
const delay = tableType === "departures" ? realtimeData.departure_delay : realtimeData.arrival_delay
const prettyDelay = delay && !canceled ? getPrettyDelay(delay) : ""
const [prettyScheduleRelationship, scheduleRelationshipColor] = getPrettyScheduleRelationship(tripScheduleRelationship, stopScheduleRelationship)
let stopsFilter
if (canceled)
stopsFilter = (stop_time) => true
else if (tableType === "departures")
stopsFilter = (stop_time) => stop_time.stop_sequence > train.stop_sequence && stop_time.drop_off_type === 0
else
stopsFilter = (stop_time) => stop_time.stop_sequence < train.stop_sequence && stop_time.pickup_type === 0
let stopsNames = stops.filter(stopsFilter).map(stopTime => stopTime?.stop.name ?? "").join(" > ") ?? ""
return <> return <>
<StyledTableRow tabletype={tableType}> <StyledTableRow tabletype={tableType}>
@ -197,8 +156,8 @@ function TrainRow({train, tableType, date, time}) {
<TableCell> <TableCell>
<Box display="flex" alignItems="center" justifyContent="center" textAlign="center"> <Box display="flex" alignItems="center" justifyContent="center" textAlign="center">
<div> <div>
<div>{trip.short_name}</div> <div>{train.routeShortName}</div>
<div>{trip.headsign}</div> <div>{train.headsign}</div>
</div> </div>
</Box> </Box>
</TableCell> </TableCell>
@ -208,19 +167,16 @@ function TrainRow({train, tableType, date, time}) {
<Box fontWeight="bold" color="#FFED02" fontSize={24}> <Box fontWeight="bold" color="#FFED02" fontSize={24}>
{getDisplayTime(train, tableType)} {getDisplayTime(train, tableType)}
</Box> </Box>
<Box color={delay && delay !== "00:00:00" ? "#e86d2b" : "white"} <Box color={delayed ? "#e86d2b" : "white"}
fontWeight={delay && delay !== "00:00:00" ? "bold" : ""}> fontWeight={delayed ? "bold" : ""}>
{prettyDelay} {prettyDelay}
</Box> </Box>
<Box color={scheduleRelationshipColor} fontWeight="bold">
{prettyScheduleRelationship}
</Box>
</Box> </Box>
</Box> </Box>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Box style={{textDecoration: canceled ? 'line-through': ''}}> <Box style={{textDecoration: canceled ? 'line-through': ''}}>
<Typography fontSize={24} fontWeight="bold" data-stop-id={headline.id}>{headline.name}</Typography> <Typography fontSize={24} fontWeight="bold" data-stop-id={headline.stopId}>{headline.name}</Typography>
<span className="stops">{stopsNames}</span> <span className="stops">{stopsNames}</span>
</Box> </Box>
</TableCell> </TableCell>
@ -228,12 +184,14 @@ function TrainRow({train, tableType, date, time}) {
</> </>
} }
function getTrainType(train, trip, route) { function getTrainType(train) {
switch (route.gtfs_feed) { if (train.place.stopId === undefined)
return ""
switch (train.place.stopId.split('_')[0]) {
case "FR-SNCF-TGV": case "FR-SNCF-TGV":
case "FR-SNCF-IC": case "FR-SNCF-IC":
case "FR-SNCF-TER": case "FR-SNCF-TER":
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0] let trainType = train.place.stopId.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) { switch (trainType) {
case "Train TER": case "Train TER":
return "TER" return "TER"
@ -246,7 +204,7 @@ function getTrainType(train, trip, route) {
} }
case "FR-IDF-IDFM": case "FR-IDF-IDFM":
case "FR-GES-CTS": case "FR-GES-CTS":
return route.short_name return "A"
case "FR-EUROSTAR": case "FR-EUROSTAR":
return "Eurostar" return "Eurostar"
case "IT-FRA-TI": case "IT-FRA-TI":
@ -254,13 +212,13 @@ function getTrainType(train, trip, route) {
case "ES-RENFE": case "ES-RENFE":
return "RENFE" return "RENFE"
case "AT-OBB": case "AT-OBB":
if (trip.short_name?.startsWith("NJ")) if (train.routeShortName?.startsWith("NJ"))
return "NJ" return "NJ"
return "ÖBB" return "ÖBB"
case "CH-ALL": case "CH-ALL":
return route.desc return "A"
default: default:
return trip.short_name?.split(" ")[0] return train.routeShortName?.split(" ")[0]
} }
} }
@ -294,8 +252,8 @@ function getTrainTypeDisplay(trainType) {
} }
} }
function getBackgroundColor(train, trip, route) { function getBackgroundColor(train) {
let trainType = getTrainType(train, trip, route) let trainType = getTrainType(train)
switch (trainType) { switch (trainType) {
case "OUIGO": case "OUIGO":
return "#0096CA" return "#0096CA"
@ -304,17 +262,17 @@ function getBackgroundColor(train, trip, route) {
case "NJ": case "NJ":
return "#272759" return "#272759"
default: default:
if (route.color) if (train.routeColor)
return `#${route.color}` return `#${train.routeColor}`
return "#FFFFFF" return "#FFFFFF"
} }
} }
function getTextColor(train, trip, route) { function getTextColor(train) {
if (route.text_color) if (train.routeTextColor)
return `#${route.text_color}` return `#${train.routeTextColor}`
else { else {
let trainType = getTrainType(train, trip, route) let trainType = getTrainType(train)
switch (trainType) { switch (trainType) {
case "OUIGO": case "OUIGO":
return "#FFFFFF" return "#FFFFFF"
@ -332,33 +290,21 @@ function getTextColor(train, trip, route) {
} }
function getDisplayTime(train, tableType) { function getDisplayTime(train, tableType) {
let time = tableType === "departures" ? train.departure_time : train.arrival_time dayjs.locale('fr')
let day_split = time.split(' ') let time = tableType === "departures" ? train.place.scheduledDeparture : train.place.scheduledArrival
return day_split[day_split.length - 1].substring(0, 5) return dayjs(time).format('LT')
} }
function getPrettyDelay(delay) { function getPrettyDelay(train, tableType) {
let delay_split = delay.split(':') if (train === undefined || !train.realTime) {
let hours = parseInt(delay_split[0]) return [false, ""]
let minutes = parseInt(delay_split[1])
let full_minutes = hours * 60 + minutes
return full_minutes ? `+${full_minutes} min` : "À l'heure"
}
function getPrettyScheduleRelationship(tripScheduledRelationship, stopScheduledRelationship) {
switch (tripScheduledRelationship) {
case 1:
return ["Ajouté", "#3ebb18"]
case 3:
return ["Supprimé", "#ff8701"]
default:
switch (stopScheduledRelationship) {
case 1:
return ["Supprimé", "#ff8701"]
default:
return ["", ""]
}
} }
const [scheduled, projected] = tableType === "departures" ? [train.place.scheduledDeparture, train.place.departure]
: [train.place.scheduledArrival, train.place.arrival]
const delay_minutes = dayjs(projected).diff(dayjs(scheduled), "minute")
if (delay_minutes === 0)
return [false, "À l'heure"]
return [true, `+${delay_minutes} min`]
} }
export default TrainsTable; export default TrainsTable;

View File

@ -1,4 +1,4 @@
import React, {useMemo} from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';