diff --git a/.env b/.env new file mode 100644 index 0000000..7e44882 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_MOTIS_SERVER=https://motis.luemy.eu diff --git a/src/App.js b/src/App.js index b7a77f4..d6b92aa 100644 --- a/src/App.js +++ b/src/App.js @@ -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: , }, { - path: "/station/:theme/:stationSlug", + path: "/station/:theme/:stationId", element: } ]) @@ -54,14 +53,7 @@ function App() { }, }) - const localStoragePersister = createSyncStoragePersister({ - storage: window.localStorage, - }) - - persistQueryClient({ - queryClient, - persister: localStoragePersister, - }) + dayjs.locale('fr') return <> diff --git a/src/AutocompleteStation.jsx b/src/AutocompleteStation.jsx index 84b9ef3..1beeed4 100644 --- a/src/AutocompleteStation.jsx +++ b/src/AutocompleteStation.jsx @@ -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; diff --git a/src/Home.js b/src/Home.js index af19209..4da3f46 100644 --- a/src/Home.js +++ b/src/Home.js @@ -5,7 +5,7 @@ function Home() { const navigate = useNavigate() function onStationSelected(event, station) { - navigate(`/station/sncf/${station.slug}/`) + navigate(`/station/sncf/${station.id}/`) } return <> diff --git a/src/Station.js b/src/Station.js index 3810ad7..0098194 100644 --- a/src/Station.js +++ b/src/Station.js @@ -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}) { Modifier la date et l'heure de recherche : - - + } 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() {
- + - - + +
) diff --git a/src/TrainsTable.js b/src/TrainsTable.js index aabbe6d..1e103f1 100644 --- a/src/TrainsTable.js +++ b/src/TrainsTable.js @@ -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 <> - +
@@ -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) => - + ) 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 <> @@ -197,8 +156,8 @@ function TrainRow({train, tableType, date, time}) {
-
{trip.short_name}
-
{trip.headsign}
+
{train.routeShortName}
+
{train.headsign}
@@ -208,19 +167,16 @@ function TrainRow({train, tableType, date, time}) { {getDisplayTime(train, tableType)} - + {prettyDelay} - - {prettyScheduleRelationship} - - {headline.name} + {headline.name} {stopsNames} @@ -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; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 07250f0..37af84a 100644 --- a/src/index.js +++ b/src/index.js @@ -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';