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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,9 @@ import {
Typography
} from "@mui/material"
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {useQueries, useQuery} from "@tanstack/react-query";
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {useQuery} from "@tanstack/react-query"
import {useCallback, useEffect, useMemo, useRef} from "react"
import dayjs from "dayjs"
const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({
'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 <>
<TableContainer>
<Table>
<TrainsTableHeader tableType={tableType} />
<TrainsTableBody station={station} date={date} time={time} tableType={tableType} />
<TrainsTableBody station={station} datetime={datetime} tableType={tableType} />
</Table>
</TableContainer>
</>
@ -49,25 +50,31 @@ function TrainsTableHeader({tableType}) {
</>
}
function TrainsTableBody({station, date, time, tableType}) {
function TrainsTableBody({station, datetime, tableType}) {
const filterTime = useCallback((train) => {
if (tableType === "departures")
return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}`
return dayjs(train.place.departure) >= datetime
else
return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}`
}, [date, time, tableType])
return dayjs(train.place.arrival) >= datetime
}, [datetime, tableType])
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(data => data.results)
.then(data => data.stopTimes)
.then(data => [...data])
}, [station.id, date, time, tableType])
}, [station.stopId, datetime, tableType])
const trainsQuery = useQuery({
queryKey: ['trains', station.id, tableType],
queryKey: ['trains', station.stopId, tableType],
queryFn: updateTrains,
enabled: !!station.id,
enabled: !!station.stopId,
})
const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data])
@ -79,7 +86,7 @@ function TrainsTableBody({station, date, time, tableType}) {
const nullRef = useRef(null)
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>)
return <>
@ -91,90 +98,42 @@ function TrainsTableBody({station, date, time, tableType}) {
</>
}
function TrainRow({train, tableType, date, time}) {
function TrainRow({train, tableType}) {
const tripQuery = useQuery({
queryKey: ['trip', train.trip],
queryFn: () => fetch(`/api/gtfs/trip/${train.trip}/`)
queryKey: ['tripId', train.tripId],
queryFn: () => fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/trip?${new URLSearchParams({tripId: train.tripId})}`)
.then(response => response.json()),
enabled: !!train.trip,
enabled: !!train.tripId,
})
const trip = tripQuery.data ?? {}
const leg = trip.legs ? trip.legs[0] : null
const routeQuery = useQuery({
queryKey: ['route', trip.route],
queryFn: () => fetch(`/api/gtfs/route/${trip.route}/`)
.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 trainType = getTrainType(train)
const backgroundColor = getBackgroundColor(train)
const textColor = getTextColor(train)
const trainTypeDisplay = getTrainTypeDisplay(trainType)
const stopTimesQuery = useQuery({
queryKey: ['stop_times', trip.id],
queryFn: () => fetch(`/api/gtfs/stop_time/?${new URLSearchParams({trip: trip.id, order: 'stop_sequence', limit: 1000})}`)
.then(response => response.json())
.then(data => data.results),
enabled: !!trip.id,
})
const stopTimes = stopTimesQuery.data ?? []
const stopIds = stopTimes.map(stop_time => stop_time.stop)
const stops = useMemo(() => leg ? [leg.from, ...leg.intermediateStops, leg.to] : [], [leg])
const stopIndex = useMemo(() => {
if (stops.length === 0 || train.place.stopId === undefined)
return -1
for (let i = 0; i < stops.length; i++) {
const index = tableType === "departures" ? i : stops.length - 1 - i
const stop = stops[index]
let timeCond = tableType === "departures" ? stop.scheduledDeparture === train.place.scheduledDeparture
: 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({
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 = nextStops[tableType === "departures" ? nextStops.length - 1 : 0] ?? {name: "Chargement…"}
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({
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(" > ") ?? ""
let stopsNames = nextStops.map(stopTime => stopTime?.name ?? "").join(" > ") ?? ""
return <>
<StyledTableRow tabletype={tableType}>
@ -197,8 +156,8 @@ function TrainRow({train, tableType, date, time}) {
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center" textAlign="center">
<div>
<div>{trip.short_name}</div>
<div>{trip.headsign}</div>
<div>{train.routeShortName}</div>
<div>{train.headsign}</div>
</div>
</Box>
</TableCell>
@ -208,19 +167,16 @@ function TrainRow({train, tableType, date, time}) {
<Box fontWeight="bold" color="#FFED02" fontSize={24}>
{getDisplayTime(train, tableType)}
</Box>
<Box color={delay && delay !== "00:00:00" ? "#e86d2b" : "white"}
fontWeight={delay && delay !== "00:00:00" ? "bold" : ""}>
<Box color={delayed ? "#e86d2b" : "white"}
fontWeight={delayed ? "bold" : ""}>
{prettyDelay}
</Box>
<Box color={scheduleRelationshipColor} fontWeight="bold">
{prettyScheduleRelationship}
</Box>
</Box>
</Box>
</TableCell>
<TableCell>
<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>
</Box>
</TableCell>
@ -228,12 +184,14 @@ function TrainRow({train, tableType, date, time}) {
</>
}
function getTrainType(train, trip, route) {
switch (route.gtfs_feed) {
function getTrainType(train) {
if (train.place.stopId === undefined)
return ""
switch (train.place.stopId.split('_')[0]) {
case "FR-SNCF-TGV":
case "FR-SNCF-IC":
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) {
case "Train TER":
return "TER"
@ -246,7 +204,7 @@ function getTrainType(train, trip, route) {
}
case "FR-IDF-IDFM":
case "FR-GES-CTS":
return route.short_name
return "A"
case "FR-EUROSTAR":
return "Eurostar"
case "IT-FRA-TI":
@ -254,13 +212,13 @@ function getTrainType(train, trip, route) {
case "ES-RENFE":
return "RENFE"
case "AT-OBB":
if (trip.short_name?.startsWith("NJ"))
if (train.routeShortName?.startsWith("NJ"))
return "NJ"
return "ÖBB"
case "CH-ALL":
return route.desc
return "A"
default:
return trip.short_name?.split(" ")[0]
return train.routeShortName?.split(" ")[0]
}
}
@ -294,8 +252,8 @@ function getTrainTypeDisplay(trainType) {
}
}
function getBackgroundColor(train, trip, route) {
let trainType = getTrainType(train, trip, route)
function getBackgroundColor(train) {
let trainType = getTrainType(train)
switch (trainType) {
case "OUIGO":
return "#0096CA"
@ -304,17 +262,17 @@ function getBackgroundColor(train, trip, route) {
case "NJ":
return "#272759"
default:
if (route.color)
return `#${route.color}`
if (train.routeColor)
return `#${train.routeColor}`
return "#FFFFFF"
}
}
function getTextColor(train, trip, route) {
if (route.text_color)
return `#${route.text_color}`
function getTextColor(train) {
if (train.routeTextColor)
return `#${train.routeTextColor}`
else {
let trainType = getTrainType(train, trip, route)
let trainType = getTrainType(train)
switch (trainType) {
case "OUIGO":
return "#FFFFFF"
@ -332,33 +290,21 @@ function getTextColor(train, trip, route) {
}
function getDisplayTime(train, tableType) {
let time = tableType === "departures" ? train.departure_time : train.arrival_time
let day_split = time.split(' ')
return day_split[day_split.length - 1].substring(0, 5)
dayjs.locale('fr')
let time = tableType === "departures" ? train.place.scheduledDeparture : train.place.scheduledArrival
return dayjs(time).format('LT')
}
function getPrettyDelay(delay) {
let delay_split = delay.split(':')
let hours = parseInt(delay_split[0])
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 ["", ""]
}
function getPrettyDelay(train, tableType) {
if (train === undefined || !train.realTime) {
return [false, ""]
}
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;

View File

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