Compare commits

...

30 Commits

Author SHA1 Message Date
0a5bec6c4b Add map 2024-11-17 20:06:06 +01:00
6389406744 Hide not-working trip filter for now 2024-11-13 23:44:08 +01:00
41441a7803 Fix realtime mode 2024-11-13 23:43:20 +01:00
f0964d8fb7 Better datetime handle 2024-11-11 22:28:05 +01:00
e58ad34e43 Update logo for CTS 2024-11-11 22:05:00 +01:00
af61173e9d Fix Locale adapter with last version of MUI Date Picker 2024-11-11 21:44:09 +01:00
2e5b5970a9 React-query is still needed 2024-11-11 21:39:10 +01:00
ec9ac8d7ab Update dependencies 2024-11-11 21:32:25 +01:00
1c99c5ca47 Use MOTIS API now 2024-11-11 21:24:01 +01:00
e3fd6a7f88 Remove local proxy 2024-11-10 16:38:08 +01:00
036e1604bd Drop trainvel backend, this is now a frontend-only app 2024-11-10 16:34:18 +01:00
bc23d63c43 Fix transfers 2024-08-12 20:49:27 +02:00
bd8d39fc1e Visual prototype to filter routes 2024-08-12 20:49:17 +02:00
a4a8cd9e9f Drop transfers before populating new ones 2024-05-28 10:08:06 +02:00
0d622302ac Preserve last_modified and etag fields after reloading GTFS Feeds 2024-05-12 17:11:54 +02:00
c60a105b2d Never display first or last stop 2024-05-12 16:41:26 +02:00
b65dc10bc6 More calendar optimizations 2024-05-12 14:03:48 +02:00
2b6523c728 Update IDFM link 2024-05-12 13:57:02 +02:00
0ab4aa7976 Trainline stations are imported using a raw query 2024-05-12 13:49:23 +02:00
68b8606688 More optimizations 2024-05-12 13:44:25 +02:00
15239117f5 Fix SNCF TVS ID for stations 2024-05-12 12:23:19 +02:00
7d9b7d90cd Fix StopTime ID 2024-05-12 12:14:48 +02:00
b85a1b7734 More optimization 2024-05-12 11:52:44 +02:00
eade9e84de Install django-extensions + update requirements.txt 2024-05-12 10:03:12 +02:00
7ed0924108 Fix IDFM and CTS line display 2024-05-12 01:40:47 +02:00
86d274ac84 Optimize CSV processing 2024-05-11 23:18:57 +02:00
368f07da32 Replace Transilien by IDFM source, add CTS (Strasbourg) 2024-05-11 21:10:04 +02:00
070849c427 Display trains that are near a station 2024-05-11 20:52:22 +02:00
735191947d Add CFL GTFS (Luxembourg) 2024-05-11 20:39:30 +02:00
0486234b9f Import stations.csv file from Trainline 2024-05-10 00:43:24 +02:00
71 changed files with 7067 additions and 11339 deletions

1
.env Normal file
View File

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

65
.gitignore vendored
View File

@ -1,53 +1,26 @@
# Byte-compiled / optimized / DLL files
dist
build
__pycache__
*.py[cod]
*$py.class
*.swp
*.egg-info
_build
.tox
.coverage
coverage
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Translations
*.mo
*.pot
# Jupyter Notebook
.ipynb_checkpoints
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea
# VSCode project settings
.vscode
# Local data
secrets.py
settings_local.py
*.log
*.txt
media/
output/
/static/
/static_files/
# dependencies
/node_modules
/.pnp
.pnp.js
# Virtualenv
.env/
env/
.venv/
venv/
db.sqlite3
db.sqlite3-journal
# testing
/coverage
node_modules/
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "trainvel-front",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mapbox/polyline": "^1.2.1",
"@mui/icons-material": "^6.1.6",
"@mui/material": "^6.1.6",
"@mui/x-date-pickers": "^7.22.2",
"@tanstack/react-query": "^5.59.20",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@turf/rhumb-bearing": "^7.1.0",
"@turf/rhumb-distance": "^7.1.0",
"@types/leaflet": "^1.9.14",
"dayjs": "^1.11.13",
"leaflet": "^1.9.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-leaflet": "^4.2.1",
"react-router-dom": "^6.28.0",
"react-scripts": "^5.0.1",
"react-transition-group": "^4.4.5",
"sass": "^1.80.6",
"web-vitals": "^4.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1,7 +0,0 @@
Django>=5.0,<6.0
django-cors-headers
django-filter~=23.5
djangorestframework~=3.14.0
protobuf
requests~=2.31.0
tqdm

View File

@ -2,14 +2,14 @@ import {createBrowserRouter, RouterProvider} from "react-router-dom"
import Station from "./Station"
import {createTheme, CssBaseline, ThemeProvider, useMediaQuery} from "@mui/material"
import React, {useMemo} from "react"
import {frFR, LocalizationProvider} from "@mui/x-date-pickers"
import {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 TrainMap from "./Map"
import dayjs from "dayjs"
function App() {
const router = createBrowserRouter([
@ -18,8 +18,12 @@ function App() {
element: <Home />,
},
{
path: "/station/:theme/:stopId",
path: "/station/:theme/:stationId",
element: <Station />
},
{
path: "/map",
element: <TrainMap />
}
])
@ -54,19 +58,12 @@ function App() {
},
})
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister: localStoragePersister,
})
dayjs.locale('fr')
return <>
<ThemeProvider theme={theme}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs} localeText={frFR.components.MuiLocalizationProvider.defaultProps.localeText} adapterLocale="fr">
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>

View File

@ -0,0 +1,37 @@
import {Autocomplete, TextField} from "@mui/material";
import {useState} from "react";
function AutocompleteStation(params) {
const [options, setOptions] = useState([])
function onInputChange(event, value) {
if (!value) {
setOptions([])
return
}
fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/geocode?language=fr&text=${value}`)
.then(response => response.json())
.then(setOptions)
}
return <>
<Autocomplete
id="stop"
options={options}
onInputChange={onInputChange}
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)}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => <TextField {...params} label="Arrêt" />}
{...params} />
</>
}
function getOptionGroup(option) {
return option.id.split('_')[0]
}
export default AutocompleteStation;

View File

@ -1,11 +1,11 @@
import AutocompleteStop from "./AutocompleteStop"
import AutocompleteStation from "./AutocompleteStation"
import {useNavigate} from "react-router-dom"
function Home() {
const navigate = useNavigate()
function onStationSelected(event, stop) {
navigate(`/station/sncf/${stop.id}/`)
function onStationSelected(event, station) {
navigate(`/station/sncf/${station.id}/`)
}
return <>
@ -13,7 +13,7 @@ function Home() {
<h2>
Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées :
</h2>
<AutocompleteStop onChange={onStationSelected} />
<AutocompleteStation onChange={onStationSelected} />
</>
}

194
src/Map.js Normal file
View File

@ -0,0 +1,194 @@
import "leaflet/dist/leaflet.css"
import L from 'leaflet'
import {MapContainer, Marker, TileLayer, useMapEvents} from 'react-leaflet'
import {useEffect, useMemo, useState} from "react"
import dayjs from "dayjs"
import polyline from "@mapbox/polyline"
import getDistance from '@turf/rhumb-distance'
import getBearing from '@turf/rhumb-bearing'
export default function TrainMap () {
return <>
<MapContainer center={[46.47, 2.37]} zoom={6} style={{height: "100vh"}}>
<TileLayer
attribution='Données cartographiques : &copy; Les contributeurices <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<TileLayer
attribution="Rendu : OpenRailwayMap"
url="https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png"></TileLayer>
<MapContent />
</MapContainer>
</>
}
function MapContent () {
const [latitude, setLatitude] = useState(46.47)
const [longitude, setLongitude] = useState(2.37)
const [zoom, setZoom] = useState(6)
useEffect(() => {
fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/map/initial`)
.then(response => response.json())
.then(data => {
setLatitude(data['lat'])
setLongitude(data['lon'])
setZoom(data['zoom'])
})
}, [])
const map = useMapEvents({
moveend: () => {
updateTrips(map, setTrips)
},
zoomend: () => {
updateTrips(map, setTrips)
}
})
useEffect(() => {
map.flyTo([latitude, longitude], zoom)
}, [map, latitude, longitude, zoom])
const [trips, setTrips] = useState([])
useEffect(() => {
updateTrips(map, setTrips)
setInterval(() => updateTrips(map, setTrips), 30000)
}, [map])
return <>
{trips.map(trip => <TripMarker trip={trip} />)}
</>
}
function TripMarker ({trip}) {
const [position, setPosition] = useState([trip.from.lat, trip.from.lon])
const [heading, setHeading] = useState(0)
const style = getModeStyle(trip.mode)
const keyframes = useMemo(() => {
const keyframes = []
const departure = dayjs(trip.departure)
const arrival = dayjs(trip.arrival)
const coordinates = polyline.decode(trip.polyline)
const totalDuration = arrival.diff(departure, 'seconds')
let currDistance = 0
let totalDistance = 0
for (let i = 0; i < coordinates.length - 1; i++) {
let from = coordinates[i]
let to = coordinates[i + 1]
totalDistance += getDistance(from, to, { units: 'meters' })
}
for (let i = 0; i < coordinates.length - 1; i++) {
let from = coordinates[i]
let to = coordinates[i + 1]
const distance = getDistance(from, to, { units: 'meters' })
const heading = getBearing(from, to)
const r = currDistance / totalDistance
keyframes.push({ point: from, time: departure.add(r * totalDuration, 'seconds'), heading: heading })
currDistance += distance
}
keyframes.push({ point: coordinates[coordinates.length - 1], time: arrival, heading: 0 })
return keyframes
}, [trip])
useEffect(() => {
const interval = setInterval(() => {
const now = dayjs()
const index = keyframes.findIndex((kf) => kf.time >= now)
if (index === -1 || index === 0)
return
const startState = keyframes[index - 1]
const endState = keyframes[index]
const r = (now.diff(startState.time)) / (endState.time.diff(startState.time))
const lat = startState.point[0] * (1 - r) + endState.point[0] * r
const lon = startState.point[1] * (1 - r) + endState.point[1] * r
setPosition([lat, lon])
setHeading(startState.heading)
}, 100)
return () => clearInterval(interval)
}, [keyframes])
const icon = L.divIcon({
html: `<svg fill="${style[1]}" fill-opacity="0.8" xmlns="http://www.w3.org/2000/svg"
\t width="36px" height="36px" viewBox="0 0 512 512" xml:space="preserve">
<g transform="rotate(${-heading - 90}, 256, 256)">
\t<path d="M256 17.108c-75.73 0-137.122 61.392-137.122 137.122.055 23.25 6.022 46.107 11.58 56.262L256 494.892l119.982-274.244h-.063c11.27-20.324 17.188-43.18 17.202-66.418C393.122 78.5 331.73 17.108 256 17.108zm0 68.56a68.56 68.56 0 0 1 68.56 68.562A68.56 68.56 0 0 1 256 222.79a68.56 68.56 0 0 1-68.56-68.56A68.56 68.56 0 0 1 256 85.67z" />
</g>
</svg>`,
className: "",
iconSize: [36, 36],
iconAnchor: [36, 36],
})
return <Marker position={position} icon={icon} />
}
function getModeStyle (mode) {
switch (mode) {
case 'WALK':
case 'FLEXIBLE':
return ['walk', 'hsl(var(--foreground) / 1)', 'hsl(var(--background) / 1)']
case 'BIKE':
case 'BIKE_TO_PARK':
case 'BIKE_RENTAL':
case 'SCOOTER_RENTAL':
return ['bike', '#075985', 'white']
case 'CAR':
case 'CAR_TO_PARK':
case 'CAR_HAILING':
case 'CAR_SHARING':
case 'CAR_PICKUP':
case 'CAR_RENTAL':
return ['car', '#333', 'white']
case 'TRANSIT':
case 'BUS':
return ['bus', '#ff9800', 'white']
case 'COACH':
return ['bus', '#9ccc65', 'white']
case 'TRAM':
return ['tram', '#ff9800', 'white']
case 'METRO':
return ['sbahn', '#4caf50', 'white']
case 'SUBWAY':
return ['ubahn', '#3f51b5', 'white']
case 'FERRY':
return ['ship', '#00acc1', 'white']
case 'AIRPLANE':
return ['plane', '#90a4ae', 'white']
case 'HIGHSPEED_RAIL':
return ['train', '#9c27b0', 'white']
case 'LONG_DISTANCE':
return ['train', '#e91e63', 'white']
case 'NIGHT_RAIL':
return ['train', '#1a237e', 'white']
case 'REGIONAL_FAST_RAIL':
case 'REGIONAL_RAIL':
case 'RAIL':
return ['train', '#f44336', 'white']
}
return ['train', '#000000', 'white']
}
function updateTrips(map, setTrips) {
const bounds = map.getBounds()
const now = dayjs()
const now_plus_1_min = now.add(60000)
const query_params = new URLSearchParams({
min: `${bounds.getNorth()},${bounds.getWest()}`,
max: `${bounds.getSouth()},${bounds.getEast()}`,
zoom: map.getZoom(),
startTime: now.format(),
endTime: now_plus_1_min.format(),
}).toString()
fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/map/trips?${query_params}`)
.then(data => data.json())
.then(setTrips)
}

90
src/Station.js Normal file
View File

@ -0,0 +1,90 @@
import {useNavigate, useParams, useSearchParams} from "react-router-dom"
import TrainsTable from "./TrainsTable"
import {useEffect, useState} from "react"
import {Box, Checkbox, 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({datetime, setDatetime, realtime, setRealtime}) {
const navigate = useNavigate()
function onStationSelected(event, station) {
if (station !== null)
navigate(`/station/sncf/${station.id}/`)
}
return <>
<Box component="form" display="flex" alignItems="center" sx={{'& .MuiTextField-root': { m: 1, width: '25ch' },}}>
<FormLabel>
Changer la gare recherchée :
</FormLabel>
<AutocompleteStation onChange={onStationSelected} />
<FormLabel>
Modifier la date et l'heure de recherche :
</FormLabel>
<DateTimePicker name="date" label="Date" onChange={setDatetime} value={datetime} disabled={realtime} readOnly={realtime} />
<Checkbox onChange={event => setRealtime(event.target.checked)} checked={realtime} />
<FormLabel>
Temps réel
</FormLabel>
</Box>
</>
}
function Station() {
// eslint-disable-next-line no-unused-vars
let {theme, stationId} = useParams()
// eslint-disable-next-line no-unused-vars
let [searchParams, setSearchParams] = useSearchParams()
const [realtime, setRealtime] = useState(searchParams.get('realtime') === "1" || false)
const [datetime, setDatetime] = useState(dayjs(searchParams.get('time') || undefined))
if ((searchParams.get('realtime') === null || searchParams.get('realtime') === "0")
&& (searchParams.get('time') === null || realtime)) {
searchParams.set('realtime', "1")
searchParams.delete("time")
setRealtime(true)
window.history.replaceState({}, '', '?' + searchParams.toString())
}
else if (datetime.format() !== searchParams.get('time') && !realtime) {
searchParams.set('time', datetime.format())
searchParams.set('realtime', "0")
window.history.replaceState({}, '', '?' + searchParams.toString())
}
useQueryClient()
const stationQuery = useQuery({
queryKey: ['station', stationId],
queryFn: () => fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/stoptimes?stopId=${stationId}&n=1`)
.then(response => response.json()),
enabled: !!stationId,
})
const station = stationQuery.data?.stopTimes[0].place ?? {name: "Chargement…"}
useEffect(() => {
if (realtime) {
const interval = setInterval(() => {
setDatetime(dayjs())
}, 5000)
return () => clearInterval(interval)
}
}, [realtime])
return (
<div className="Station">
<header className="App-header">
<h1>Horaires en gare de {station.name}</h1>
</header>
<main>
<DateTimeSelector datetime={datetime} setDatetime={setDatetime} realtime={realtime} setRealtime={setRealtime} />
{/*<TripsFilter />*/}
<TrainsTable station={station} datetime={datetime} realtime={realtime} tableType="departures" />
<TrainsTable station={station} datetime={datetime} realtime={realtime} tableType="arrivals" />
</main>
</div>
)
}
export default Station;

318
src/TrainsTable.js Normal file
View File

@ -0,0 +1,318 @@
import {
Box,
styled,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from "@mui/material"
import {CSSTransition, TransitionGroup} from 'react-transition-group'
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)': {
backgroundColor: theme.palette.sncf[tabletype].light,
},
'th, &:nth-of-type(even)': {
backgroundColor: theme.palette.sncf[tabletype].dark,
},
// hide last border
'&:last-child td, &:last-child th': {
border: 0,
},
}));
function TrainsTable({station, datetime, realtime, tableType}) {
return <>
<TableContainer>
<Table>
<TrainsTableHeader tableType={tableType} />
<TrainsTableBody station={station} datetime={datetime} realtime={realtime} tableType={tableType} />
</Table>
</TableContainer>
</>
}
function TrainsTableHeader({tableType}) {
return <>
<TableHead>
<StyledTableRow tabletype={tableType}>
<TableCell colSpan="2" fontSize={16} fontWeight="bold">Train</TableCell>
<TableCell fontSize={16} fontWeight="bold">Heure</TableCell>
<TableCell fontSize={16} fontWeight="bold">Destination</TableCell>
</StyledTableRow>
</TableHead>
</>
}
function TrainsTableBody({station, datetime, realtime, tableType}) {
const filterTime = useCallback((train) => {
if (tableType === "departures")
return dayjs(train.place.departure) >= datetime
else
return dayjs(train.place.arrival) >= datetime
}, [datetime, tableType])
const updateTrains = useCallback(() => {
const params = {
stopId: station.stopId,
arriveBy: tableType === "arrivals",
direction: "LATER",
n: 20,
}
if (!realtime)
params['time'] = datetime.format()
const query_params = new URLSearchParams(params).toString()
return fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/stoptimes?${query_params}`)
.then(response => response.json())
.then(data => data.stopTimes)
.then(data => [...data])
}, [station.stopId, tableType, datetime, realtime])
const trainsQuery = useQuery({
queryKey: ['trains', station.stopId, tableType],
queryFn: updateTrains,
enabled: !!station.stopId,
})
const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data])
useEffect(() => {
if (realtime) {
let validTrains = trains?.filter(filterTime) ?? []
if ((trains?.length > 0 && validTrains.length < trains?.length))
trainsQuery.refetch().then()
}
}, [trains, filterTime, trainsQuery, realtime])
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} />
</CSSTransition>)
return <>
<TableBody>
<TransitionGroup component={null}>
{table_rows}
</TransitionGroup>
</TableBody>
</>
}
function TrainRow({train, tableType}) {
const tripQuery = useQuery({
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.tripId,
})
const trip = tripQuery.data ?? {}
const leg = trip.legs ? trip.legs[0] : null
const trainType = getTrainType(train)
const backgroundColor = getBackgroundColor(train)
const textColor = getTextColor(train)
const trainTypeDisplay = getTrainTypeDisplay(trainType)
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)
let headline = nextStops[tableType === "departures" ? nextStops.length - 1 : 0] ?? {name: "Chargement…"}
const canceled = false // TODO Implémenter l'annulation
const [delayed, prettyDelay] = getPrettyDelay(train, tableType)
let stopsNames = nextStops.map(stopTime => stopTime?.name ?? "").join(" > ") ?? ""
return <>
<StyledTableRow tabletype={tableType}>
<TableCell>
<div>
<Box display="flex"
justifyContent="center"
alignItems="center"
textAlign="center"
width="4em"
height="4em"
borderRadius="15%"
fontWeight="bold"
backgroundColor={backgroundColor}
color={textColor}>
{trainTypeDisplay}
</Box>
</div>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center" textAlign="center">
<div>
<div>{train.routeShortName}</div>
<div>{train.headsign}</div>
</div>
</Box>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center">
<Box>
<Box fontWeight="bold" color="#FFED02" fontSize={24}>
{getDisplayTime(train, tableType)}
</Box>
<Box color={delayed ? "#e86d2b" : "white"}
fontWeight={delayed ? "bold" : ""}>
{prettyDelay}
</Box>
</Box>
</Box>
</TableCell>
<TableCell>
<Box style={{textDecoration: canceled ? 'line-through': ''}}>
<Typography fontSize={24} fontWeight="bold" data-stop-id={headline.stopId}>{headline.name}</Typography>
<span className="stops">{stopsNames}</span>
</Box>
</TableCell>
</StyledTableRow>
</>
}
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.place.stopId.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) {
case "Train TER":
return "TER"
case "INTERCITES":
return "INTER-CITÉS"
case "INTERCITES de nuit":
return "INTER-CITÉS de nuit"
default:
return trainType
}
case "FR-IDFM":
const route_split = train.routeShortName.split(" ")
if (route_split[0] === "Bus")
return route_split[1]
return route_split[0]
case "FR-GES-CTS":
return train.routeShortName.split(" ")[1]
case "FR-EUROSTAR":
return "Eurostar"
case "IT-FRA-TI":
return "Trenitalia France"
case "ES-RENFE":
return "RENFE"
case "AT-OBB":
if (train.routeShortName?.startsWith("NJ"))
return "NJ"
return "ÖBB"
case "CH-ALL":
default:
return train.routeShortName?.split(" ")[0]
}
}
function getTrainTypeDisplay(trainType) {
switch (trainType) {
case "TGV INOUI":
return <img src="/tgv_inoui.svg" alt="TGV INOUI" width="80%" />
case "OUIGO":
return <img src="/ouigo.svg" alt="OUIGO" width="80%" />
case "ICE":
return <img src="/ice.svg" alt="ICE" width="80%" />
case "Lyria":
return <img src="/lyria.svg" alt="Lyria" width="80%" />
case "TER":
return <img src="/ter.svg" alt="TER" width="80%" />
case "Car TER":
return <div><img src="/bus.svg" alt="Car" width="40%" />
<br/>
<img src="/ter.svg" alt="TER" width="40%" /></div>
case "Eurostar":
return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE":
return <img src="/renfe.svg" alt="RENFE" width="80%" />
case "NJ":
return <img src="/nightjet.svg" alt="NightJet" width="80%" />
default:
return trainType
}
}
function getBackgroundColor(train) {
let trainType = getTrainType(train)
switch (trainType) {
case "OUIGO":
return "#0096CA"
case "Eurostar":
return "#00286A"
case "NJ":
return "#272759"
default:
if (train.routeColor)
return `#${train.routeColor}`
return "#FFFFFF"
}
}
function getTextColor(train) {
if (train.routeTextColor)
return `#${train.routeTextColor}`
else {
let trainType = getTrainType(train)
switch (trainType) {
case "OUIGO":
return "#FFFFFF"
case "TGV INOUI":
return "#9B2743"
case "ICE":
return "#B4B4B4"
case "INTER-CITÉS":
case "INTER-CITÉS de nuit":
return "#404042"
default:
return "#000000"
}
}
}
function getDisplayTime(train, tableType) {
dayjs.locale('fr')
let time = tableType === "departures" ? train.place.scheduledDeparture : train.place.scheduledArrival
return dayjs(time).format('LT')
}
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;

165
src/TripsFilter.js Normal file
View File

@ -0,0 +1,165 @@
import {useState} from "react"
import {
Box, Button,
Checkbox, Chip, FormControl,
FormControlLabel,
InputLabel, MenuItem, OutlinedInput, Select
} from "@mui/material"
import DirectionsBusTwoToneIcon from '@mui/icons-material/DirectionsBusTwoTone'
import SubwayTwoToneIcon from '@mui/icons-material/SubwayTwoTone'
import TrainTwoToneIcon from '@mui/icons-material/TrainTwoTone'
import TramTwoToneIcon from '@mui/icons-material/TramTwoTone'
function TripsFilter() {
const [transportModeFilter, setTransportModeFilter] = useState(
{longDistanceTrain: true, regionalTrain: true, metro: true, tram: true, bus: true})
const transportModeNames = {
train: "Trains",
longDistanceTrain: "Trains longue distance",
regionalTrain: "Trains régionaux",
metro: "Métro",
tram: "Tram",
bus: "Bus",
}
const trainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.longDistanceTrain && transportModeFilter.regionalTrain}
indeterminate={transportModeFilter.longDistanceTrain !== transportModeFilter.regionalTrain}
onChange={(event) =>
setTransportModeFilter(
{...transportModeFilter, longDistanceTrain: event.target.checked, regionalTrain: event.target.checked})}
onClick={(event) => event.stopPropagation()}
/>
</>
const longDistanceTrainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.longDistanceTrain}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, longDistanceTrain: event.target.checked})} />
</>
const regionalTrainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.regionalTrain}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, regionalTrain: event.target.checked})} />
</>
const metroCheckbox = <>
<SubwayTwoToneIcon />
<Checkbox
checked={transportModeFilter.metro}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, metro: event.target.checked})} />
</>
const tramCheckbox = <>
<TramTwoToneIcon />
<Checkbox
checked={transportModeFilter.tram}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, tram: event.target.checked})} />
</>
const busCheckbox = <>
<DirectionsBusTwoToneIcon />
<Checkbox
checked={transportModeFilter.bus}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, bus: event.target.checked})} />
</>
// TODO Fetch routes that are accessible from one stop
// For now, we have the tram and bus routes accessible in Strasbourg main station
const routesList = [
{name: "Tous"},
{name: "A", bgColor: "#E10D19", color: "#FFFFFF"},
{name: "C", bgColor: "#F29400", color: "#FFFFFF"},
{name: "D", bgColor: "#009933", color: "#FFFFFF"},
{name: "G", bgColor: "#F6C900", color: "#000000"},
{name: "H", bgColor: "#A62341", color: "#FFFFFF"},
{name: "2", bgColor: "#FF0000", color: "#FFFFFF"},
{name: "10", bgColor: "#FFAA00", color: "#000000"},
]
const routesDict = {}
for (const route of routesList) {
routesDict[route.name] = route
}
const [selectedRoutes, setSelectedRoutes] = useState(["Tous"])
return <>
<h2>Filtres</h2>
<Box display="flex" alignItems="center" sx={{mb: 3}}>
<FormControl>
<InputLabel>Mode de transport</InputLabel>
<Select
multiple
value={selectedRoutes}
input={<OutlinedInput id="select-multiple-chip" label="Lignes" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{Object.keys(transportModeFilter).filter(key => transportModeFilter[key]).map((filterType) => (
<Chip key={filterType} label={transportModeNames[filterType]} sx={{fontWeight: "bold"}} />
))}
</Box>
)}
>
<MenuItem key="train" value="train">
<FormControlLabel label={transportModeNames["train"]} control={trainCheckbox} />
</MenuItem>
<MenuItem key="longDistanceTrain" value="longDistanceTrain">
<FormControlLabel label={transportModeNames["longDistanceTrain"]} sx={{pl: 4}} control={longDistanceTrainCheckbox} />
</MenuItem>
<MenuItem key="regionalTrain" value="regionalTrain">
<FormControlLabel label={transportModeNames["regionalTrain"]} sx={{pl: 4}} control={regionalTrainCheckbox} />
</MenuItem>
<MenuItem key="metro" value="metro">
<FormControlLabel label={transportModeNames["metro"]} control={metroCheckbox} />
</MenuItem>
<MenuItem key="tram" value="tram">
<FormControlLabel label={transportModeNames["tram"]} control={tramCheckbox} />
</MenuItem>
<MenuItem key="bus" value="bus">
<FormControlLabel label={transportModeNames["bus"]} control={busCheckbox} />
</MenuItem>
</Select>
</FormControl>
<FormControl>
<InputLabel>Ligne</InputLabel>
<Select
multiple
value={selectedRoutes}
onChange={(event) => setSelectedRoutes(event.target.value)}
input={<OutlinedInput id="select-multiple-chip" label="Lignes" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map(routeName => routesDict[routeName]).map((route) => (
<Chip key={route.name} label={route.name} sx={{backgroundColor: route.bgColor, color: route.color, fontWeight: "bold"}} />
))}
</Box>
)}
>
{routesList.map((route) =>
<MenuItem key={route.name} value={route.name}>
<Checkbox checked={selectedRoutes.includes(route.name)} />
<Chip label={route.name} sx={{backgroundColor: route.bgColor, color: route.color, fontWeight: "bold"}} />
</MenuItem>
)}
</Select>
</FormControl>
<Button>
Filtrer
</Button>
</Box>
</>
}
export default TripsFilter

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';

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,50 +0,0 @@
{
"name": "trainvel-front",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.6",
"@mui/x-date-pickers": "^6.19.2",
"@tanstack/query-sync-storage-persister": "^5.18.0",
"@tanstack/react-query": "^5.18.0",
"@tanstack/react-query-persist-client": "^5.18.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"dayjs": "^1.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"react-scripts": "5.0.1",
"react-transition-group": "^4.4.5",
"sass": "^1.70.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8000"
}

View File

@ -1,63 +0,0 @@
import {Autocomplete, TextField} from "@mui/material";
import {useRef, useState} from "react";
function AutocompleteStop(params) {
const [options, setOptions] = useState([])
const previousController = useRef()
function onInputChange(event, value) {
if (!value) {
setOptions([])
return
}
if (previousController.current)
previousController.current.abort()
const controller = new AbortController()
const signal = controller.signal
previousController.current = controller
fetch("/api/gtfs/stop/?location_type=1&search=" + value, {signal})
.then(response => response.json())
.then(data => data.results)
.then(setOptions)
.catch()
}
return <>
<Autocomplete
id="stop"
options={options}
onInputChange={onInputChange}
filterOptions={(x) => x}
getOptionKey={option => option.id}
getOptionLabel={option => option.name}
groupBy={option => getOptionGroup(option)}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => <TextField {...params} label="Arrêt" />}
{...params} />
</>
}
function getOptionGroup(option) {
switch (option.gtfs_feed) {
case "FR-SNCF-TGV":
case "FR-SNCF-IC":
case "FR-SNCF-TER":
return "TGV/TER/Intercités"
case "FR-IDF-TN":
return "Transilien"
case "FR-EUROSTAR":
return "Eurostar"
case "IT-FRA-TI":
return "Trenitalia France"
case "ES-RENFE":
return "RENFE"
case "AT-OBB":
return "ÖBB"
default:
return option.gtfs_feed
}
}
export default AutocompleteStop;

View File

@ -1,77 +0,0 @@
import {useNavigate, useParams, useSearchParams} from "react-router-dom"
import TrainsTable from "./TrainsTable"
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 AutocompleteStop from "./AutocompleteStop";
function DateTimeSelector({stop, date, time}) {
const navigate = useNavigate()
function onStationSelected(event, stop) {
if (stop !== null)
navigate(`/station/sncf/${stop.id}/`)
}
return <>
<Box component="form" display="flex" alignItems="center" sx={{'& .MuiTextField-root': { m: 1, width: '25ch' },}}>
<FormLabel>
Changer la gare recherchée :
</FormLabel>
<AutocompleteStop onChange={onStationSelected} />
<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}`)} />
<Button type="submit">Rechercher</Button>
</Box>
</>
}
function Station() {
let {stopId, theme} = 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)
useQueryClient()
const stopQuery = useQuery({
queryKey: ['stop', stopId],
queryFn: () => fetch(`/api/gtfs/stop/${stopId}/`)
.then(response => response.json()),
enabled: !!stopId,
})
const stop = stopQuery.data ?? {name: "Chargement…"}
if (time === timeNow) {
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)
}, 5000)
}
return (
<div className="Station">
<header className="App-header">
<h1>Horaires en gare de {stop.name}</h1>
</header>
<main>
<DateTimeSelector stop={stop} date={date} time={time} />
<TrainsTable stop={stop} date={date} time={time} tableType="departures" />
<TrainsTable stop={stop} date={date} time={time} tableType="arrivals" />
</main>
</div>
)
}
export default Station;

View File

@ -1,361 +0,0 @@
import {
Box,
styled,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
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";
const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({
'tbody &:nth-of-type(odd)': {
backgroundColor: theme.palette.sncf[tabletype].light,
},
'th, &:nth-of-type(even)': {
backgroundColor: theme.palette.sncf[tabletype].dark,
},
// hide last border
'&:last-child td, &:last-child th': {
border: 0,
},
}));
function TrainsTable({stop, date, time, tableType}) {
return <>
<TableContainer>
<Table>
<TrainsTableHeader tableType={tableType} />
<TrainsTableBody stop={stop} date={date} time={time} tableType={tableType} />
</Table>
</TableContainer>
</>
}
function TrainsTableHeader({tableType}) {
return <>
<TableHead>
<StyledTableRow tabletype={tableType}>
<TableCell colSpan="2" fontSize={16} fontWeight="bold">Train</TableCell>
<TableCell fontSize={16} fontWeight="bold">Heure</TableCell>
<TableCell fontSize={16} fontWeight="bold">Destination</TableCell>
</StyledTableRow>
</TableHead>
</>
}
function TrainsTableBody({stop, date, time, tableType}) {
const filterTime = useCallback((train) => {
if (tableType === "departures")
return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}`
else
return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}`
}, [date, time, tableType])
const updateTrains = useCallback(() => {
return fetch(`/api/station/next_${tableType}/?stop_id=${stop.id}&date=${date}&time=${time}&offset=${0}&limit=${20}`)
.then(response => response.json())
.then(data => data.results)
.then(data => [...data])
}, [stop.id, date, time, tableType])
const trainsQuery = useQuery({
queryKey: ['trains', stop.id, tableType],
queryFn: updateTrains,
enabled: !!stop.id,
})
const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data])
useEffect(() => {
let validTrains = trains?.filter(filterTime) ?? []
if (trains?.length > 0 && validTrains.length < trains?.length)
trainsQuery.refetch().then()
}, [trains, filterTime, trainsQuery])
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} />
</CSSTransition>)
return <>
<TableBody>
<TransitionGroup component={null}>
{table_rows}
</TransitionGroup>
</TableBody>
</>
}
function TrainRow({train, tableType, date, time}) {
const tripQuery = useQuery({
queryKey: ['trip', train.trip],
queryFn: () => fetch(`/api/gtfs/trip/${train.trip}/`)
.then(response => response.json()),
enabled: !!train.trip,
})
const trip = tripQuery.data ?? {}
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 trainTypeDisplay = getTrainTypeDisplay(trainType)
const stopTimesQuery = useQuery({
queryKey: ['stop_times', trip.id],
queryFn: () => fetch(`/api/gtfs/stop_time/?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 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 = stops[tableType === "departures" ? stops.length - 1 : 0]?.stop ?? {name: "Chargement…"}
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(" > ") ?? ""
return <>
<StyledTableRow tabletype={tableType}>
<TableCell>
<div>
<Box display="flex"
justifyContent="center"
alignItems="center"
textAlign="center"
width="4em"
height="4em"
borderRadius="15%"
fontWeight="bold"
backgroundColor={backgroundColor}
color={textColor}>
{trainTypeDisplay}
</Box>
</div>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center" textAlign="center">
<div>
<div>{trip.short_name}</div>
<div>{trip.headsign}</div>
</div>
</Box>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center">
<Box>
<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" : ""}>
{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>
<span className="stops">{stopsNames}</span>
</Box>
</TableCell>
</StyledTableRow>
</>
}
function getTrainType(train, trip, route) {
switch (route.gtfs_feed) {
case "FR-SNCF-TGV":
case "FR-SNCF-IC":
case "FR-SNCF-TER":
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) {
case "Train TER":
return "TER"
case "INTERCITES":
return "INTER-CITÉS"
case "INTERCITES de nuit":
return "INTER-CITÉS de nuit"
default:
return trainType
}
case "FR-IDF-TN":
return route.short_name
case "FR-EUROSTAR":
return "Eurostar"
case "IT-FRA-TI":
return "Trenitalia France"
case "ES-RENFE":
return "RENFE"
case "AT-OBB":
if (trip.short_name?.startsWith("NJ"))
return "NJ"
return "ÖBB"
default:
return trip.short_name?.split(" ")[0]
}
}
function getTrainTypeDisplay(trainType) {
switch (trainType) {
case "TGV INOUI":
return <img src="/tgv_inoui.svg" alt="TGV INOUI" width="80%" />
case "OUIGO":
return <img src="/ouigo.svg" alt="OUIGO" width="80%" />
case "ICE":
return <img src="/ice.svg" alt="ICE" width="80%" />
case "Lyria":
return <img src="/lyria.svg" alt="Lyria" width="80%" />
case "TER":
return <img src="/ter.svg" alt="TER" width="80%" />
case "Car TER":
return <div><img src="/bus.svg" alt="Car" width="40%" />
<br/>
<img src="/ter.svg" alt="TER" width="40%" /></div>
case "Eurostar":
return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE":
return <img src="/renfe.svg" alt="RENFE" width="80%" />
case "NJ":
return <img src="/nightjet.svg" alt="NightJet" width="80%" />
default:
return trainType
}
}
function getBackgroundColor(train, trip, route) {
let trainType = getTrainType(train, trip, route)
switch (trainType) {
case "OUIGO":
return "#0096CA"
case "Eurostar":
return "#00286A"
case "NJ":
return "#272759"
default:
if (route.color)
return `#${route.color}`
return "#FFFFFF"
}
}
function getTextColor(train, trip, route) {
if (route.text_color)
return `#${route.text_color}`
else {
let trainType = getTrainType(train, trip, route)
switch (trainType) {
case "OUIGO":
return "#FFFFFF"
case "TGV INOUI":
return "#9B2743"
case "ICE":
return "#B4B4B4"
case "INTER-CITÉS":
case "INTER-CITÉS de nuit":
return "#404042"
default:
return "#000000"
}
}
}
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)
}
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 ["", ""]
}
}
}
export default TrainsTable;

View File

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "trainvel.api"

View File

@ -1,76 +0,0 @@
from rest_framework import serializers
from trainvel.gtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo, TripUpdate, StopTimeUpdate
class AgencySerializer(serializers.ModelSerializer):
class Meta:
model = Agency
fields = '__all__'
class StopSerializer(serializers.ModelSerializer):
class Meta:
model = Stop
fields = '__all__'
class RouteSerializer(serializers.ModelSerializer):
class Meta:
model = Route
fields = '__all__'
class TripSerializer(serializers.ModelSerializer):
class Meta:
model = Trip
fields = '__all__'
class StopTimeSerializer(serializers.ModelSerializer):
arrival_date = serializers.DateField(required=False)
departure_date = serializers.DateField(required=False)
arrival_time_24h = serializers.DurationField(required=False)
departure_time_24h = serializers.DurationField(required=False)
departure_time_real = serializers.CharField(required=False)
class Meta:
model = StopTime
fields = '__all__'
class CalendarSerializer(serializers.ModelSerializer):
class Meta:
model = Calendar
fields = '__all__'
class CalendarDateSerializer(serializers.ModelSerializer):
class Meta:
model = CalendarDate
fields = '__all__'
class TransferSerializer(serializers.ModelSerializer):
class Meta:
model = Transfer
fields = '__all__'
class FeedInfoSerializer(serializers.ModelSerializer):
class Meta:
model = FeedInfo
fields = '__all__'
class TripUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = TripUpdate
fields = '__all__'
class StopTimeUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = StopTimeUpdate
fields = '__all__'

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,300 +0,0 @@
from datetime import datetime, timedelta, date
from django.db.models import F, Q, Value, When, Case, Exists, OuterRef
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.http import last_modified
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter, SearchFilter
from trainvel.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \
FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer
from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, StopTimeUpdate, \
Transfer, Trip, TripUpdate
CACHE_CONTROL = cache_control(max_age=7200)
LAST_MODIFIED = last_modified(lambda *args, **kwargs: GTFSFeed.objects.order_by('-last_modified').first().last_modified)
LOOKUP_VALUE_REGEX = r"[\w.: |-]+"
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class AgencyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Agency.objects.all()
serializer_class = AgencySerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class StopViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Stop.objects.all()
serializer_class = StopSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = '__all__'
search_fields = ['name',]
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class RouteViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Route.objects.all()
serializer_class = RouteSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class TripViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Trip.objects.all()
serializer_class = TripSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class StopTimeViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StopTime.objects.order_by('id').all()
serializer_class = StopTimeSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = '__all__'
ordering_fields = ['arrival_time', 'departure_time', 'stop_sequence', ]
ordering = ['stop_sequence', ]
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class CalendarViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Calendar.objects.all()
serializer_class = CalendarSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class CalendarDateViewSet(viewsets.ReadOnlyModelViewSet):
queryset = CalendarDate.objects.all()
serializer_class = CalendarDateSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class TransferViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Transfer.objects.all()
serializer_class = TransferSerializer
filter_backends = [DjangoFilterBackend]
lookup_value_regex = LOOKUP_VALUE_REGEX
@method_decorator(name='list', decorator=[CACHE_CONTROL, LAST_MODIFIED])
@method_decorator(name='retrieve', decorator=[CACHE_CONTROL, LAST_MODIFIED])
class FeedInfoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = FeedInfo.objects.all()
serializer_class = FeedInfoSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
class TripUpdateViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TripUpdate.objects.all()
serializer_class = TripUpdateSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
class StopTimeUpdateViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StopTimeUpdate.objects.all()
serializer_class = StopTimeUpdateSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = '__all__'
lookup_value_regex = LOOKUP_VALUE_REGEX
class NextDeparturesViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StopTime.objects.none()
serializer_class = StopTimeSerializer
filter_backends = [DjangoFilterBackend]
def get_queryset(self):
now = datetime.now()
stop_id = self.request.query_params.get('stop_id', None)
stop_name = self.request.query_params.get('stop_name', None)
query_date = date.fromisoformat(self.request.query_params.get('date', now.date().isoformat()))
query_time = self.request.query_params.get('time', now.time().isoformat(timespec='seconds'))
query_time = timedelta(seconds=int(query_time[:2]) * 3600
+ int(query_time[3:5]) * 60
+ (int(query_time[6:]) if len(query_time) > 6 else 0))
yesterday = query_date - timedelta(days=1)
time_yesterday = query_time + timedelta(days=1)
tomorrow = query_date + timedelta(days=1)
stop_filter = Q(stop__location_type=0)
if stop_id:
stop = Stop.objects.get(id=stop_id)
stops = Stop.objects.filter(Q(id=stop_id)
| Q(parent_station=stop_id))
if stop.location_type == 0 and stop.parent_station_id is not None:
stops |= Stop.objects.filter(parent_station=stop.parent_station_id)
stop_filter = Q(stop__in=stops.values_list('id', flat=True))
elif stop_name:
stops = Stop.objects.filter(name__iexact=stop_name).values_list('id', flat=True)
stop_filter = Q(stop__in=stops)
def calendar_filter(d: date):
return Q(trip__service_id__in=CalendarDate.objects.filter(date=d, exception_type=1)
.values_list('service_id')) \
| Q(trip__service_id__in=Calendar.objects.filter(
start_date__lte=d,
end_date__gte=d,
**{f"{d:%A}".lower(): True})
.filter(~Q(id__in=CalendarDate.objects.filter(date=d, exception_type=2)
.values_list('service_id', flat=True)))
.values_list('id'))
def stop_time_update_qs(d: date):
return StopTimeUpdate.objects.filter(trip_update__start_date=d) \
.exclude(departure_time=datetime.fromtimestamp(0)).filter(stop_time_id=OuterRef('pk'))
def departure_time_real(d: date):
return Case(
When(
condition=Exists(stop_time_update_qs(d)),
then=F('departure_time') + stop_time_update_qs(d).values('departure_delay'),
),
default=F('departure_time'),
)
def canceled_filter(d: date):
return Exists(stop_time_update_qs(d).filter(Q(schedule_relationship=1) | Q(schedule_relationship=3)))
qs_today = StopTime.objects.filter(stop_filter) \
.annotate(departure_time_real=departure_time_real(query_date)) \
.filter(departure_time_real__gte=query_time) \
.filter(Q(pickup_type=0) | canceled_filter(query_date)) \
.filter(calendar_filter(query_date)) \
.annotate(departure_date=Value(query_date)) \
.annotate(departure_time_24h=F('departure_time'))
qs_yesterday = StopTime.objects.filter(stop_filter) \
.annotate(departure_time_real=departure_time_real(query_date)) \
.filter(departure_time_real__gte=time_yesterday) \
.filter(Q(pickup_type=0) | canceled_filter(yesterday)) \
.filter(calendar_filter(yesterday)) \
.annotate(departure_date=Value(yesterday)) \
.annotate(departure_time_24h=F('departure_time') - timedelta(days=1))
qs_tomorrow = StopTime.objects.filter(stop_filter) \
.annotate(departure_time_real=departure_time_real(query_date)) \
.filter(departure_time_real__gte=timedelta(0)) \
.filter(Q(pickup_type=0) | canceled_filter(tomorrow)) \
.filter(calendar_filter(tomorrow)) \
.annotate(departure_date=Value(tomorrow)) \
.annotate(departure_time_24h=F('departure_time') + timedelta(days=1))
return qs_today.union(qs_yesterday).union(qs_tomorrow).order_by("departure_time_24h").all()
class NextArrivalsViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StopTime.objects.none()
serializer_class = StopTimeSerializer
filter_backends = [DjangoFilterBackend]
def get_queryset(self):
now = datetime.now()
stop_id = self.request.query_params.get('stop_id', None)
stop_name = self.request.query_params.get('stop_name', None)
query_date = date.fromisoformat(self.request.query_params.get('date', now.date().isoformat()))
query_time = self.request.query_params.get('time', now.time().isoformat(timespec='seconds'))
query_time = timedelta(seconds=int(query_time[:2]) * 3600
+ int(query_time[3:5]) * 60
+ (int(query_time[6:]) if len(query_time) > 6 else 0))
query_time -= timedelta(minutes=5) # Keep the last trains of the 5 previous minutes
yesterday = query_date - timedelta(days=1)
time_yesterday = query_time + timedelta(days=1)
tomorrow = query_date + timedelta(days=1)
stop_filter = Q(stop__location_type=0)
if stop_id:
stop = Stop.objects.get(id=stop_id)
stops = Stop.objects.filter(Q(id=stop_id)
| Q(parent_station=stop_id))
if stop.location_type == 0 and stop.parent_station_id is not None:
stops |= Stop.objects.filter(parent_station=stop.parent_station_id)
stop_filter = Q(stop__in=stops.values_list('id', flat=True))
elif stop_name:
stops = Stop.objects.filter(name__iexact=stop_name).values_list('id', flat=True)
stop_filter = Q(stop__in=stops)
def calendar_filter(d: date):
return Q(trip__service_id__in=CalendarDate.objects.filter(date=d, exception_type=1)
.values_list('service_id')) \
| Q(trip__service_id__in=Calendar.objects.filter(
start_date__lte=d,
end_date__gte=d,
**{f"{d:%A}".lower(): True})
.filter(~Q(id__in=CalendarDate.objects.filter(date=d, exception_type=2)
.values_list('service_id', flat=True)))
.values_list('id'))
def stop_time_update_qs(d: date):
return StopTimeUpdate.objects.filter(trip_update__start_date=d) \
.exclude(arrival_time=datetime.fromtimestamp(0)).filter(stop_time_id=OuterRef('pk'))
def arrival_time_real(d: date):
return Case(
When(
condition=Exists(stop_time_update_qs(d)),
then=F('arrival_time') + stop_time_update_qs(d).values('arrival_delay'),
),
default=F('arrival_time'),
)
def canceled_filter(d: date):
return Exists(stop_time_update_qs(d).filter(Q(schedule_relationship=1) | Q(schedule_relationship=3)))
qs_today = StopTime.objects.filter(stop_filter) \
.annotate(arrival_time_real=arrival_time_real(query_date)) \
.filter(arrival_time_real__gte=query_time) \
.filter(Q(drop_off_type=0) | canceled_filter(query_date)) \
.filter(calendar_filter(query_date)) \
.annotate(arrival_date=Value(query_date)) \
.annotate(arrival_time_24h=F('arrival_time'))
qs_yesterday = StopTime.objects.filter(stop_filter) \
.annotate(arrival_time_real=arrival_time_real(yesterday)) \
.filter(arrival_time_real__gte=time_yesterday) \
.filter(Q(drop_off_type=0) | canceled_filter(yesterday)) \
.filter(calendar_filter(yesterday)) \
.annotate(arrival_date=Value(yesterday)) \
.annotate(arrival_time_24h=F('arrival_time') - timedelta(days=1))
qs_tomorrow = StopTime.objects.filter(stop_filter) \
.annotate(arrival_time_real=arrival_time_real(tomorrow)) \
.filter(arrival_time_real__gte=timedelta(0)) \
.filter(Q(drop_off_type=0) | canceled_filter(tomorrow)) \
.filter(calendar_filter(tomorrow)) \
.annotate(arrival_date=Value(tomorrow)) \
.annotate(arrival_time_24h=F('arrival_time') + timedelta(days=1))
return qs_today.union(qs_yesterday).union(qs_tomorrow).order_by("arrival_time_24h").all()

View File

@ -1,16 +0,0 @@
"""
ASGI config for trainvel project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_asgi_application()

View File

@ -1,165 +0,0 @@
from django.contrib import admin
from django.forms import BaseInlineFormSet
from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, \
Route, Stop, StopTime, StopTimeUpdate, Transfer, Trip, TripUpdate
class LimitModelFormset(BaseInlineFormSet):
""" Base Inline formset to limit inline Model query results. """
def get_queryset(self):
return super(LimitModelFormset, self).get_queryset()[:50]
class CalendarDateInline(admin.TabularInline):
model = CalendarDate
extra = 0
formset = LimitModelFormset
class TripInline(admin.TabularInline):
model = Trip
extra = 0
formset = LimitModelFormset
autocomplete_fields = ('route', 'service',)
show_change_link = True
ordering = ('service',)
readonly_fields = ('gtfs_feed',)
class StopTimeInline(admin.TabularInline):
model = StopTime
extra = 0
formset = LimitModelFormset
autocomplete_fields = ('stop',)
readonly_fields = ('id',)
show_change_link = True
ordering = ('stop_sequence',)
class TripUpdateInline(admin.StackedInline):
model = TripUpdate
extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip',)
class StopTimeUpdateInline(admin.StackedInline):
model = StopTimeUpdate
extra = 0
formset = LimitModelFormset
autocomplete_fields = ('trip_update', 'stop_time',)
@admin.register(GTFSFeed)
class GTFSFeedAdmin(admin.ModelAdmin):
list_display = ('name', 'code', 'country', 'last_modified',)
list_filter = ('country', 'last_modified',)
search_fields = ('name', 'code',)
readonly_fields = ('code',)
@admin.register(Agency)
class AgencyAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'url', 'timezone', 'gtfs_feed',)
list_filter = ('gtfs_feed', 'timezone',)
search_fields = ('name',)
autocomplete_fields = ('gtfs_feed',)
@admin.register(Stop)
class StopAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'lat', 'lon', 'location_type',)
list_filter = ('location_type', 'gtfs_feed',)
search_fields = ('name', 'id',)
ordering = ('name',)
autocomplete_fields = ('parent_station', 'gtfs_feed',)
@admin.register(Route)
class RouteAdmin(admin.ModelAdmin):
list_display = ('__str__', 'id', 'type', 'gtfs_feed',)
list_filter = ('gtfs_feed', 'type', 'agency',)
search_fields = ('long_name', 'short_name', 'id',)
ordering = ('long_name',)
autocomplete_fields = ('agency', 'gtfs_feed',)
inlines = (TripInline,)
@admin.register(Trip)
class TripAdmin(admin.ModelAdmin):
list_display = ('id', 'origin_destination', 'route', 'service', 'headsign', 'direction_id',)
list_filter = ('direction_id', 'route__gtfs_feed',)
search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',)
ordering = ('route', 'service',)
autocomplete_fields = ('route', 'service', 'gtfs_feed',)
inlines = (StopTimeInline, TripUpdateInline,)
@admin.register(StopTime)
class StopTimeAdmin(admin.ModelAdmin):
list_display = ('trip', 'stop', 'arrival_time', 'departure_time',
'stop_sequence', 'pickup_type', 'drop_off_type',)
list_filter = ('pickup_type', 'drop_off_type', 'trip__route__gtfs_feed',)
search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip', 'stop_sequence',)
autocomplete_fields = ('trip', 'stop',)
readonly_fields = ('id',)
inlines = (StopTimeUpdateInline,)
@admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin):
list_display = ('id', 'gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'sunday', 'start_date', 'end_date',)
list_filter = ('gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
'start_date', 'end_date',)
search_fields = ('id', 'start_date', 'end_date',)
autocomplete_fields = ('gtfs_feed',)
ordering = ('gtfs_feed', 'id',)
inlines = (CalendarDateInline, TripInline,)
@admin.register(CalendarDate)
class CalendarDateAdmin(admin.ModelAdmin):
list_display = ('id', 'service_id', 'date', 'exception_type',)
list_filter = ('exception_type', 'date', 'service__gtfs_feed',)
search_fields = ('id', 'date',)
ordering = ('date', 'service_id',)
@admin.register(Transfer)
class TransferAdmin(admin.ModelAdmin):
list_display = ('from_stop', 'to_stop', 'transfer_type', 'min_transfer_time',)
list_filter = ('transfer_type',)
search_fields = ('from_stop__name', 'to_stop__name',)
autocomplete_fields = ('from_stop', 'to_stop',)
@admin.register(FeedInfo)
class FeedInfoAdmin(admin.ModelAdmin):
list_display = ('publisher_name', 'publisher_url', 'lang', 'start_date',
'end_date', 'version',)
search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date',
'end_date', 'version',)
autocomplete_fields = ('gtfs_feed',)
ordering = ('publisher_name',)
@admin.register(StopTimeUpdate)
class StopTimeUpdateAdmin(admin.ModelAdmin):
list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time',
'departure_delay', 'departure_time', 'schedule_relationship',)
list_filter = ('schedule_relationship', 'trip_update__trip__gtfs_feed',)
search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip_update', 'stop_time',)
autocomplete_fields = ('trip_update', 'stop_time',)
@admin.register(TripUpdate)
class TripUpdateAdmin(admin.ModelAdmin):
list_display = ('trip_id', 'start_date', 'start_time',)
list_filter = ('start_date', 'schedule_relationship',)
search_fields = ('trip__id', 'start_date', 'start_time',)
ordering = ('trip_id', 'start_date', 'start_time',)
autocomplete_fields = ('trip',)

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class TrainvelGTFSConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "trainvel.gtfs"
verbose_name = _("Trainvel - GTFS")

View File

@ -1,92 +0,0 @@
[
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-TGV",
"fields": {
"name": "SNCF - TGV",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-IC",
"fields": {
"name": "SNCF - Intercités",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-SNCF-TER",
"fields": {
"name": "SNCF - TER",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip",
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-IDF-TN",
"fields": {
"name": "SNCF - Transilien",
"country": "FR",
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "FR-EUROSTAR",
"fields": {
"name": "Eurostar",
"country": "FR",
"feed_url": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "IT-FRA-TI",
"fields": {
"name": "Trenitalia France",
"country": "FR",
"feed_url": "https://thello.axelor.com/public/gtfs/gtfs.zip",
"rt_feed_url": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin"
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "ES-RENFE",
"fields": {
"name": "Renfe",
"country": "ES",
"feed_url": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "AT-ÖBB",
"fields": {
"name": "ÖBB",
"country": "AT",
"feed_url": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
"rt_feed_url": ""
}
},
{
"model": "gtfs.gtfsfeed",
"pk": "CH-ALL",
"fields": {
"name": "Transports suisses",
"country": "CH",
"feed_url": "https://opentransportdata.swiss/fr/dataset/timetable-2024-gtfs2020/permalink",
"rt_feed_url": "https://api.opentransportdata.swiss/gtfsrt2020"
}
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,436 +0,0 @@
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf.internal import python_message as _python_message
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class FeedMessage(_message.Message):
__slots__ = ("header", "entity")
Extensions: _python_message._ExtensionDict
HEADER_FIELD_NUMBER: _ClassVar[int]
ENTITY_FIELD_NUMBER: _ClassVar[int]
header: FeedHeader
entity: _containers.RepeatedCompositeFieldContainer[FeedEntity]
def __init__(self, header: _Optional[_Union[FeedHeader, _Mapping]] = ..., entity: _Optional[_Iterable[_Union[FeedEntity, _Mapping]]] = ...) -> None: ...
class FeedHeader(_message.Message):
__slots__ = ("gtfs_realtime_version", "incrementality", "timestamp")
Extensions: _python_message._ExtensionDict
class Incrementality(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
FULL_DATASET: _ClassVar[FeedHeader.Incrementality]
DIFFERENTIAL: _ClassVar[FeedHeader.Incrementality]
FULL_DATASET: FeedHeader.Incrementality
DIFFERENTIAL: FeedHeader.Incrementality
GTFS_REALTIME_VERSION_FIELD_NUMBER: _ClassVar[int]
INCREMENTALITY_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
gtfs_realtime_version: str
incrementality: FeedHeader.Incrementality
timestamp: int
def __init__(self, gtfs_realtime_version: _Optional[str] = ..., incrementality: _Optional[_Union[FeedHeader.Incrementality, str]] = ..., timestamp: _Optional[int] = ...) -> None: ...
class FeedEntity(_message.Message):
__slots__ = ("id", "is_deleted", "trip_update", "vehicle", "alert", "shape")
Extensions: _python_message._ExtensionDict
ID_FIELD_NUMBER: _ClassVar[int]
IS_DELETED_FIELD_NUMBER: _ClassVar[int]
TRIP_UPDATE_FIELD_NUMBER: _ClassVar[int]
VEHICLE_FIELD_NUMBER: _ClassVar[int]
ALERT_FIELD_NUMBER: _ClassVar[int]
SHAPE_FIELD_NUMBER: _ClassVar[int]
id: str
is_deleted: bool
trip_update: TripUpdate
vehicle: VehiclePosition
alert: Alert
shape: Shape
def __init__(self, id: _Optional[str] = ..., is_deleted: bool = ..., trip_update: _Optional[_Union[TripUpdate, _Mapping]] = ..., vehicle: _Optional[_Union[VehiclePosition, _Mapping]] = ..., alert: _Optional[_Union[Alert, _Mapping]] = ..., shape: _Optional[_Union[Shape, _Mapping]] = ...) -> None: ...
class TripUpdate(_message.Message):
__slots__ = ("trip", "vehicle", "stop_time_update", "timestamp", "delay", "trip_properties")
Extensions: _python_message._ExtensionDict
class StopTimeEvent(_message.Message):
__slots__ = ("delay", "time", "uncertainty")
Extensions: _python_message._ExtensionDict
DELAY_FIELD_NUMBER: _ClassVar[int]
TIME_FIELD_NUMBER: _ClassVar[int]
UNCERTAINTY_FIELD_NUMBER: _ClassVar[int]
delay: int
time: int
uncertainty: int
def __init__(self, delay: _Optional[int] = ..., time: _Optional[int] = ..., uncertainty: _Optional[int] = ...) -> None: ...
class StopTimeUpdate(_message.Message):
__slots__ = ("stop_sequence", "stop_id", "arrival", "departure", "departure_occupancy_status", "schedule_relationship", "stop_time_properties")
Extensions: _python_message._ExtensionDict
class ScheduleRelationship(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
SCHEDULED: _ClassVar[TripUpdate.StopTimeUpdate.ScheduleRelationship]
SKIPPED: _ClassVar[TripUpdate.StopTimeUpdate.ScheduleRelationship]
NO_DATA: _ClassVar[TripUpdate.StopTimeUpdate.ScheduleRelationship]
UNSCHEDULED: _ClassVar[TripUpdate.StopTimeUpdate.ScheduleRelationship]
SCHEDULED: TripUpdate.StopTimeUpdate.ScheduleRelationship
SKIPPED: TripUpdate.StopTimeUpdate.ScheduleRelationship
NO_DATA: TripUpdate.StopTimeUpdate.ScheduleRelationship
UNSCHEDULED: TripUpdate.StopTimeUpdate.ScheduleRelationship
class StopTimeProperties(_message.Message):
__slots__ = ("assigned_stop_id",)
Extensions: _python_message._ExtensionDict
ASSIGNED_STOP_ID_FIELD_NUMBER: _ClassVar[int]
assigned_stop_id: str
def __init__(self, assigned_stop_id: _Optional[str] = ...) -> None: ...
STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
STOP_ID_FIELD_NUMBER: _ClassVar[int]
ARRIVAL_FIELD_NUMBER: _ClassVar[int]
DEPARTURE_FIELD_NUMBER: _ClassVar[int]
DEPARTURE_OCCUPANCY_STATUS_FIELD_NUMBER: _ClassVar[int]
SCHEDULE_RELATIONSHIP_FIELD_NUMBER: _ClassVar[int]
STOP_TIME_PROPERTIES_FIELD_NUMBER: _ClassVar[int]
stop_sequence: int
stop_id: str
arrival: TripUpdate.StopTimeEvent
departure: TripUpdate.StopTimeEvent
departure_occupancy_status: VehiclePosition.OccupancyStatus
schedule_relationship: TripUpdate.StopTimeUpdate.ScheduleRelationship
stop_time_properties: TripUpdate.StopTimeUpdate.StopTimeProperties
def __init__(self, stop_sequence: _Optional[int] = ..., stop_id: _Optional[str] = ..., arrival: _Optional[_Union[TripUpdate.StopTimeEvent, _Mapping]] = ..., departure: _Optional[_Union[TripUpdate.StopTimeEvent, _Mapping]] = ..., departure_occupancy_status: _Optional[_Union[VehiclePosition.OccupancyStatus, str]] = ..., schedule_relationship: _Optional[_Union[TripUpdate.StopTimeUpdate.ScheduleRelationship, str]] = ..., stop_time_properties: _Optional[_Union[TripUpdate.StopTimeUpdate.StopTimeProperties, _Mapping]] = ...) -> None: ...
class TripProperties(_message.Message):
__slots__ = ("trip_id", "start_date", "start_time", "shape_id")
Extensions: _python_message._ExtensionDict
TRIP_ID_FIELD_NUMBER: _ClassVar[int]
START_DATE_FIELD_NUMBER: _ClassVar[int]
START_TIME_FIELD_NUMBER: _ClassVar[int]
SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
trip_id: str
start_date: str
start_time: str
shape_id: str
def __init__(self, trip_id: _Optional[str] = ..., start_date: _Optional[str] = ..., start_time: _Optional[str] = ..., shape_id: _Optional[str] = ...) -> None: ...
TRIP_FIELD_NUMBER: _ClassVar[int]
VEHICLE_FIELD_NUMBER: _ClassVar[int]
STOP_TIME_UPDATE_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
DELAY_FIELD_NUMBER: _ClassVar[int]
TRIP_PROPERTIES_FIELD_NUMBER: _ClassVar[int]
trip: TripDescriptor
vehicle: VehicleDescriptor
stop_time_update: _containers.RepeatedCompositeFieldContainer[TripUpdate.StopTimeUpdate]
timestamp: int
delay: int
trip_properties: TripUpdate.TripProperties
def __init__(self, trip: _Optional[_Union[TripDescriptor, _Mapping]] = ..., vehicle: _Optional[_Union[VehicleDescriptor, _Mapping]] = ..., stop_time_update: _Optional[_Iterable[_Union[TripUpdate.StopTimeUpdate, _Mapping]]] = ..., timestamp: _Optional[int] = ..., delay: _Optional[int] = ..., trip_properties: _Optional[_Union[TripUpdate.TripProperties, _Mapping]] = ...) -> None: ...
class VehiclePosition(_message.Message):
__slots__ = ("trip", "vehicle", "position", "current_stop_sequence", "stop_id", "current_status", "timestamp", "congestion_level", "occupancy_status", "occupancy_percentage", "multi_carriage_details")
Extensions: _python_message._ExtensionDict
class VehicleStopStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
INCOMING_AT: _ClassVar[VehiclePosition.VehicleStopStatus]
STOPPED_AT: _ClassVar[VehiclePosition.VehicleStopStatus]
IN_TRANSIT_TO: _ClassVar[VehiclePosition.VehicleStopStatus]
INCOMING_AT: VehiclePosition.VehicleStopStatus
STOPPED_AT: VehiclePosition.VehicleStopStatus
IN_TRANSIT_TO: VehiclePosition.VehicleStopStatus
class CongestionLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
UNKNOWN_CONGESTION_LEVEL: _ClassVar[VehiclePosition.CongestionLevel]
RUNNING_SMOOTHLY: _ClassVar[VehiclePosition.CongestionLevel]
STOP_AND_GO: _ClassVar[VehiclePosition.CongestionLevel]
CONGESTION: _ClassVar[VehiclePosition.CongestionLevel]
SEVERE_CONGESTION: _ClassVar[VehiclePosition.CongestionLevel]
UNKNOWN_CONGESTION_LEVEL: VehiclePosition.CongestionLevel
RUNNING_SMOOTHLY: VehiclePosition.CongestionLevel
STOP_AND_GO: VehiclePosition.CongestionLevel
CONGESTION: VehiclePosition.CongestionLevel
SEVERE_CONGESTION: VehiclePosition.CongestionLevel
class OccupancyStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
EMPTY: _ClassVar[VehiclePosition.OccupancyStatus]
MANY_SEATS_AVAILABLE: _ClassVar[VehiclePosition.OccupancyStatus]
FEW_SEATS_AVAILABLE: _ClassVar[VehiclePosition.OccupancyStatus]
STANDING_ROOM_ONLY: _ClassVar[VehiclePosition.OccupancyStatus]
CRUSHED_STANDING_ROOM_ONLY: _ClassVar[VehiclePosition.OccupancyStatus]
FULL: _ClassVar[VehiclePosition.OccupancyStatus]
NOT_ACCEPTING_PASSENGERS: _ClassVar[VehiclePosition.OccupancyStatus]
NO_DATA_AVAILABLE: _ClassVar[VehiclePosition.OccupancyStatus]
NOT_BOARDABLE: _ClassVar[VehiclePosition.OccupancyStatus]
EMPTY: VehiclePosition.OccupancyStatus
MANY_SEATS_AVAILABLE: VehiclePosition.OccupancyStatus
FEW_SEATS_AVAILABLE: VehiclePosition.OccupancyStatus
STANDING_ROOM_ONLY: VehiclePosition.OccupancyStatus
CRUSHED_STANDING_ROOM_ONLY: VehiclePosition.OccupancyStatus
FULL: VehiclePosition.OccupancyStatus
NOT_ACCEPTING_PASSENGERS: VehiclePosition.OccupancyStatus
NO_DATA_AVAILABLE: VehiclePosition.OccupancyStatus
NOT_BOARDABLE: VehiclePosition.OccupancyStatus
class CarriageDetails(_message.Message):
__slots__ = ("id", "label", "occupancy_status", "occupancy_percentage", "carriage_sequence")
Extensions: _python_message._ExtensionDict
ID_FIELD_NUMBER: _ClassVar[int]
LABEL_FIELD_NUMBER: _ClassVar[int]
OCCUPANCY_STATUS_FIELD_NUMBER: _ClassVar[int]
OCCUPANCY_PERCENTAGE_FIELD_NUMBER: _ClassVar[int]
CARRIAGE_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
id: str
label: str
occupancy_status: VehiclePosition.OccupancyStatus
occupancy_percentage: int
carriage_sequence: int
def __init__(self, id: _Optional[str] = ..., label: _Optional[str] = ..., occupancy_status: _Optional[_Union[VehiclePosition.OccupancyStatus, str]] = ..., occupancy_percentage: _Optional[int] = ..., carriage_sequence: _Optional[int] = ...) -> None: ...
TRIP_FIELD_NUMBER: _ClassVar[int]
VEHICLE_FIELD_NUMBER: _ClassVar[int]
POSITION_FIELD_NUMBER: _ClassVar[int]
CURRENT_STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
STOP_ID_FIELD_NUMBER: _ClassVar[int]
CURRENT_STATUS_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
CONGESTION_LEVEL_FIELD_NUMBER: _ClassVar[int]
OCCUPANCY_STATUS_FIELD_NUMBER: _ClassVar[int]
OCCUPANCY_PERCENTAGE_FIELD_NUMBER: _ClassVar[int]
MULTI_CARRIAGE_DETAILS_FIELD_NUMBER: _ClassVar[int]
trip: TripDescriptor
vehicle: VehicleDescriptor
position: Position
current_stop_sequence: int
stop_id: str
current_status: VehiclePosition.VehicleStopStatus
timestamp: int
congestion_level: VehiclePosition.CongestionLevel
occupancy_status: VehiclePosition.OccupancyStatus
occupancy_percentage: int
multi_carriage_details: _containers.RepeatedCompositeFieldContainer[VehiclePosition.CarriageDetails]
def __init__(self, trip: _Optional[_Union[TripDescriptor, _Mapping]] = ..., vehicle: _Optional[_Union[VehicleDescriptor, _Mapping]] = ..., position: _Optional[_Union[Position, _Mapping]] = ..., current_stop_sequence: _Optional[int] = ..., stop_id: _Optional[str] = ..., current_status: _Optional[_Union[VehiclePosition.VehicleStopStatus, str]] = ..., timestamp: _Optional[int] = ..., congestion_level: _Optional[_Union[VehiclePosition.CongestionLevel, str]] = ..., occupancy_status: _Optional[_Union[VehiclePosition.OccupancyStatus, str]] = ..., occupancy_percentage: _Optional[int] = ..., multi_carriage_details: _Optional[_Iterable[_Union[VehiclePosition.CarriageDetails, _Mapping]]] = ...) -> None: ...
class Alert(_message.Message):
__slots__ = ("active_period", "informed_entity", "cause", "effect", "url", "header_text", "description_text", "tts_header_text", "tts_description_text", "severity_level", "image", "image_alternative_text", "cause_detail", "effect_detail")
Extensions: _python_message._ExtensionDict
class Cause(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
UNKNOWN_CAUSE: _ClassVar[Alert.Cause]
OTHER_CAUSE: _ClassVar[Alert.Cause]
TECHNICAL_PROBLEM: _ClassVar[Alert.Cause]
STRIKE: _ClassVar[Alert.Cause]
DEMONSTRATION: _ClassVar[Alert.Cause]
ACCIDENT: _ClassVar[Alert.Cause]
HOLIDAY: _ClassVar[Alert.Cause]
WEATHER: _ClassVar[Alert.Cause]
MAINTENANCE: _ClassVar[Alert.Cause]
CONSTRUCTION: _ClassVar[Alert.Cause]
POLICE_ACTIVITY: _ClassVar[Alert.Cause]
MEDICAL_EMERGENCY: _ClassVar[Alert.Cause]
UNKNOWN_CAUSE: Alert.Cause
OTHER_CAUSE: Alert.Cause
TECHNICAL_PROBLEM: Alert.Cause
STRIKE: Alert.Cause
DEMONSTRATION: Alert.Cause
ACCIDENT: Alert.Cause
HOLIDAY: Alert.Cause
WEATHER: Alert.Cause
MAINTENANCE: Alert.Cause
CONSTRUCTION: Alert.Cause
POLICE_ACTIVITY: Alert.Cause
MEDICAL_EMERGENCY: Alert.Cause
class Effect(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
NO_SERVICE: _ClassVar[Alert.Effect]
REDUCED_SERVICE: _ClassVar[Alert.Effect]
SIGNIFICANT_DELAYS: _ClassVar[Alert.Effect]
DETOUR: _ClassVar[Alert.Effect]
ADDITIONAL_SERVICE: _ClassVar[Alert.Effect]
MODIFIED_SERVICE: _ClassVar[Alert.Effect]
OTHER_EFFECT: _ClassVar[Alert.Effect]
UNKNOWN_EFFECT: _ClassVar[Alert.Effect]
STOP_MOVED: _ClassVar[Alert.Effect]
NO_EFFECT: _ClassVar[Alert.Effect]
ACCESSIBILITY_ISSUE: _ClassVar[Alert.Effect]
NO_SERVICE: Alert.Effect
REDUCED_SERVICE: Alert.Effect
SIGNIFICANT_DELAYS: Alert.Effect
DETOUR: Alert.Effect
ADDITIONAL_SERVICE: Alert.Effect
MODIFIED_SERVICE: Alert.Effect
OTHER_EFFECT: Alert.Effect
UNKNOWN_EFFECT: Alert.Effect
STOP_MOVED: Alert.Effect
NO_EFFECT: Alert.Effect
ACCESSIBILITY_ISSUE: Alert.Effect
class SeverityLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
UNKNOWN_SEVERITY: _ClassVar[Alert.SeverityLevel]
INFO: _ClassVar[Alert.SeverityLevel]
WARNING: _ClassVar[Alert.SeverityLevel]
SEVERE: _ClassVar[Alert.SeverityLevel]
UNKNOWN_SEVERITY: Alert.SeverityLevel
INFO: Alert.SeverityLevel
WARNING: Alert.SeverityLevel
SEVERE: Alert.SeverityLevel
ACTIVE_PERIOD_FIELD_NUMBER: _ClassVar[int]
INFORMED_ENTITY_FIELD_NUMBER: _ClassVar[int]
CAUSE_FIELD_NUMBER: _ClassVar[int]
EFFECT_FIELD_NUMBER: _ClassVar[int]
URL_FIELD_NUMBER: _ClassVar[int]
HEADER_TEXT_FIELD_NUMBER: _ClassVar[int]
DESCRIPTION_TEXT_FIELD_NUMBER: _ClassVar[int]
TTS_HEADER_TEXT_FIELD_NUMBER: _ClassVar[int]
TTS_DESCRIPTION_TEXT_FIELD_NUMBER: _ClassVar[int]
SEVERITY_LEVEL_FIELD_NUMBER: _ClassVar[int]
IMAGE_FIELD_NUMBER: _ClassVar[int]
IMAGE_ALTERNATIVE_TEXT_FIELD_NUMBER: _ClassVar[int]
CAUSE_DETAIL_FIELD_NUMBER: _ClassVar[int]
EFFECT_DETAIL_FIELD_NUMBER: _ClassVar[int]
active_period: _containers.RepeatedCompositeFieldContainer[TimeRange]
informed_entity: _containers.RepeatedCompositeFieldContainer[EntitySelector]
cause: Alert.Cause
effect: Alert.Effect
url: TranslatedString
header_text: TranslatedString
description_text: TranslatedString
tts_header_text: TranslatedString
tts_description_text: TranslatedString
severity_level: Alert.SeverityLevel
image: TranslatedImage
image_alternative_text: TranslatedString
cause_detail: TranslatedString
effect_detail: TranslatedString
def __init__(self, active_period: _Optional[_Iterable[_Union[TimeRange, _Mapping]]] = ..., informed_entity: _Optional[_Iterable[_Union[EntitySelector, _Mapping]]] = ..., cause: _Optional[_Union[Alert.Cause, str]] = ..., effect: _Optional[_Union[Alert.Effect, str]] = ..., url: _Optional[_Union[TranslatedString, _Mapping]] = ..., header_text: _Optional[_Union[TranslatedString, _Mapping]] = ..., description_text: _Optional[_Union[TranslatedString, _Mapping]] = ..., tts_header_text: _Optional[_Union[TranslatedString, _Mapping]] = ..., tts_description_text: _Optional[_Union[TranslatedString, _Mapping]] = ..., severity_level: _Optional[_Union[Alert.SeverityLevel, str]] = ..., image: _Optional[_Union[TranslatedImage, _Mapping]] = ..., image_alternative_text: _Optional[_Union[TranslatedString, _Mapping]] = ..., cause_detail: _Optional[_Union[TranslatedString, _Mapping]] = ..., effect_detail: _Optional[_Union[TranslatedString, _Mapping]] = ...) -> None: ...
class TimeRange(_message.Message):
__slots__ = ("start", "end")
Extensions: _python_message._ExtensionDict
START_FIELD_NUMBER: _ClassVar[int]
END_FIELD_NUMBER: _ClassVar[int]
start: int
end: int
def __init__(self, start: _Optional[int] = ..., end: _Optional[int] = ...) -> None: ...
class Position(_message.Message):
__slots__ = ("latitude", "longitude", "bearing", "odometer", "speed")
Extensions: _python_message._ExtensionDict
LATITUDE_FIELD_NUMBER: _ClassVar[int]
LONGITUDE_FIELD_NUMBER: _ClassVar[int]
BEARING_FIELD_NUMBER: _ClassVar[int]
ODOMETER_FIELD_NUMBER: _ClassVar[int]
SPEED_FIELD_NUMBER: _ClassVar[int]
latitude: float
longitude: float
bearing: float
odometer: float
speed: float
def __init__(self, latitude: _Optional[float] = ..., longitude: _Optional[float] = ..., bearing: _Optional[float] = ..., odometer: _Optional[float] = ..., speed: _Optional[float] = ...) -> None: ...
class TripDescriptor(_message.Message):
__slots__ = ("trip_id", "route_id", "direction_id", "start_time", "start_date", "schedule_relationship")
Extensions: _python_message._ExtensionDict
class ScheduleRelationship(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
SCHEDULED: _ClassVar[TripDescriptor.ScheduleRelationship]
ADDED: _ClassVar[TripDescriptor.ScheduleRelationship]
UNSCHEDULED: _ClassVar[TripDescriptor.ScheduleRelationship]
CANCELED: _ClassVar[TripDescriptor.ScheduleRelationship]
REPLACEMENT: _ClassVar[TripDescriptor.ScheduleRelationship]
DUPLICATED: _ClassVar[TripDescriptor.ScheduleRelationship]
DELETED: _ClassVar[TripDescriptor.ScheduleRelationship]
SCHEDULED: TripDescriptor.ScheduleRelationship
ADDED: TripDescriptor.ScheduleRelationship
UNSCHEDULED: TripDescriptor.ScheduleRelationship
CANCELED: TripDescriptor.ScheduleRelationship
REPLACEMENT: TripDescriptor.ScheduleRelationship
DUPLICATED: TripDescriptor.ScheduleRelationship
DELETED: TripDescriptor.ScheduleRelationship
TRIP_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
DIRECTION_ID_FIELD_NUMBER: _ClassVar[int]
START_TIME_FIELD_NUMBER: _ClassVar[int]
START_DATE_FIELD_NUMBER: _ClassVar[int]
SCHEDULE_RELATIONSHIP_FIELD_NUMBER: _ClassVar[int]
trip_id: str
route_id: str
direction_id: int
start_time: str
start_date: str
schedule_relationship: TripDescriptor.ScheduleRelationship
def __init__(self, trip_id: _Optional[str] = ..., route_id: _Optional[str] = ..., direction_id: _Optional[int] = ..., start_time: _Optional[str] = ..., start_date: _Optional[str] = ..., schedule_relationship: _Optional[_Union[TripDescriptor.ScheduleRelationship, str]] = ...) -> None: ...
class VehicleDescriptor(_message.Message):
__slots__ = ("id", "label", "license_plate", "wheelchair_accessible")
Extensions: _python_message._ExtensionDict
class WheelchairAccessible(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
NO_VALUE: _ClassVar[VehicleDescriptor.WheelchairAccessible]
UNKNOWN: _ClassVar[VehicleDescriptor.WheelchairAccessible]
WHEELCHAIR_ACCESSIBLE: _ClassVar[VehicleDescriptor.WheelchairAccessible]
WHEELCHAIR_INACCESSIBLE: _ClassVar[VehicleDescriptor.WheelchairAccessible]
NO_VALUE: VehicleDescriptor.WheelchairAccessible
UNKNOWN: VehicleDescriptor.WheelchairAccessible
WHEELCHAIR_ACCESSIBLE: VehicleDescriptor.WheelchairAccessible
WHEELCHAIR_INACCESSIBLE: VehicleDescriptor.WheelchairAccessible
ID_FIELD_NUMBER: _ClassVar[int]
LABEL_FIELD_NUMBER: _ClassVar[int]
LICENSE_PLATE_FIELD_NUMBER: _ClassVar[int]
WHEELCHAIR_ACCESSIBLE_FIELD_NUMBER: _ClassVar[int]
id: str
label: str
license_plate: str
wheelchair_accessible: VehicleDescriptor.WheelchairAccessible
def __init__(self, id: _Optional[str] = ..., label: _Optional[str] = ..., license_plate: _Optional[str] = ..., wheelchair_accessible: _Optional[_Union[VehicleDescriptor.WheelchairAccessible, str]] = ...) -> None: ...
class EntitySelector(_message.Message):
__slots__ = ("agency_id", "route_id", "route_type", "trip", "stop_id", "direction_id")
Extensions: _python_message._ExtensionDict
AGENCY_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_TYPE_FIELD_NUMBER: _ClassVar[int]
TRIP_FIELD_NUMBER: _ClassVar[int]
STOP_ID_FIELD_NUMBER: _ClassVar[int]
DIRECTION_ID_FIELD_NUMBER: _ClassVar[int]
agency_id: str
route_id: str
route_type: int
trip: TripDescriptor
stop_id: str
direction_id: int
def __init__(self, agency_id: _Optional[str] = ..., route_id: _Optional[str] = ..., route_type: _Optional[int] = ..., trip: _Optional[_Union[TripDescriptor, _Mapping]] = ..., stop_id: _Optional[str] = ..., direction_id: _Optional[int] = ...) -> None: ...
class TranslatedString(_message.Message):
__slots__ = ("translation",)
Extensions: _python_message._ExtensionDict
class Translation(_message.Message):
__slots__ = ("text", "language")
Extensions: _python_message._ExtensionDict
TEXT_FIELD_NUMBER: _ClassVar[int]
LANGUAGE_FIELD_NUMBER: _ClassVar[int]
text: str
language: str
def __init__(self, text: _Optional[str] = ..., language: _Optional[str] = ...) -> None: ...
TRANSLATION_FIELD_NUMBER: _ClassVar[int]
translation: _containers.RepeatedCompositeFieldContainer[TranslatedString.Translation]
def __init__(self, translation: _Optional[_Iterable[_Union[TranslatedString.Translation, _Mapping]]] = ...) -> None: ...
class TranslatedImage(_message.Message):
__slots__ = ("localized_image",)
Extensions: _python_message._ExtensionDict
class LocalizedImage(_message.Message):
__slots__ = ("url", "media_type", "language")
Extensions: _python_message._ExtensionDict
URL_FIELD_NUMBER: _ClassVar[int]
MEDIA_TYPE_FIELD_NUMBER: _ClassVar[int]
LANGUAGE_FIELD_NUMBER: _ClassVar[int]
url: str
media_type: str
language: str
def __init__(self, url: _Optional[str] = ..., media_type: _Optional[str] = ..., language: _Optional[str] = ...) -> None: ...
LOCALIZED_IMAGE_FIELD_NUMBER: _ClassVar[int]
localized_image: _containers.RepeatedCompositeFieldContainer[TranslatedImage.LocalizedImage]
def __init__(self, localized_image: _Optional[_Iterable[_Union[TranslatedImage.LocalizedImage, _Mapping]]] = ...) -> None: ...
class Shape(_message.Message):
__slots__ = ("shape_id", "encoded_polyline")
Extensions: _python_message._ExtensionDict
SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
ENCODED_POLYLINE_FIELD_NUMBER: _ClassVar[int]
shape_id: str
encoded_polyline: str
def __init__(self, shape_id: _Optional[str] = ..., encoded_polyline: _Optional[str] = ...) -> None: ...

View File

@ -1,825 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-09 22:04+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: trainvel/gtfs/apps.py:8
msgid "Trainvel - GTFS"
msgstr "Trainvel - GTFS"
#: trainvel/gtfs/models.py:11
msgid "Albania"
msgstr "Albanie"
#: trainvel/gtfs/models.py:12
msgid "Andorra"
msgstr "Andorre"
#: trainvel/gtfs/models.py:13
msgid "Armenia"
msgstr "Arménie"
#: trainvel/gtfs/models.py:14
msgid "Austria"
msgstr "Autriche"
#: trainvel/gtfs/models.py:15
msgid "Azerbaijan"
msgstr "Azerbaijan"
#: trainvel/gtfs/models.py:16
msgid "Belgium"
msgstr "Belgique"
#: trainvel/gtfs/models.py:17
msgid "Bosnia and Herzegovina"
msgstr " Bosnie-Herzégovine"
#: trainvel/gtfs/models.py:18
msgid "Bulgaria"
msgstr "Bulgarie"
#: trainvel/gtfs/models.py:19
msgid "Croatia"
msgstr "Croatie"
#: trainvel/gtfs/models.py:20
msgid "Cyprus"
msgstr "Chypre"
#: trainvel/gtfs/models.py:21
msgid "Czech Republic"
msgstr "République Tchèque"
#: trainvel/gtfs/models.py:22
msgid "Denmark"
msgstr "Danemark"
#: trainvel/gtfs/models.py:23
msgid "Estonia"
msgstr "Estonie"
#: trainvel/gtfs/models.py:24
msgid "Finland"
msgstr "Finlande"
#: trainvel/gtfs/models.py:25
msgid "France"
msgstr "France"
#: trainvel/gtfs/models.py:26
msgid "Georgia"
msgstr "Géorgie"
#: trainvel/gtfs/models.py:27
msgid "Germany"
msgstr "Allemagne"
#: trainvel/gtfs/models.py:28
msgid "Greece"
msgstr "Grèce"
#: trainvel/gtfs/models.py:29
msgid "Hungary"
msgstr "Hongrie"
#: trainvel/gtfs/models.py:30
msgid "Iceland"
msgstr "Islande"
#: trainvel/gtfs/models.py:31
msgid "Ireland"
msgstr "Irlande"
#: trainvel/gtfs/models.py:32
msgid "Italy"
msgstr "Italie"
#: trainvel/gtfs/models.py:33
msgid "Latvia"
msgstr "Lettonie"
#: trainvel/gtfs/models.py:34
msgid "Liechtenstein"
msgstr "Liechtenstein"
#: trainvel/gtfs/models.py:35
msgid "Lithuania"
msgstr "Lituanie"
#: trainvel/gtfs/models.py:36
msgid "Luxembourg"
msgstr "Luxembourg"
#: trainvel/gtfs/models.py:37
msgid "Malta"
msgstr "Malte"
#: trainvel/gtfs/models.py:38
msgid "Moldova"
msgstr "Moldavie"
#: trainvel/gtfs/models.py:39
msgid "Monaco"
msgstr "Monaco"
#: trainvel/gtfs/models.py:40
msgid "Montenegro"
msgstr "Monténégro"
#: trainvel/gtfs/models.py:41
msgid "Netherlands"
msgstr "Pays-Bas"
#: trainvel/gtfs/models.py:42
msgid "North Macedonia"
msgstr "Macédoine du Nord"
#: trainvel/gtfs/models.py:43
msgid "Norway"
msgstr "Norvège"
#: trainvel/gtfs/models.py:44
msgid "Poland"
msgstr "Pologne"
#: trainvel/gtfs/models.py:45
msgid "Portugal"
msgstr "Portugal"
#: trainvel/gtfs/models.py:46
msgid "Romania"
msgstr "Roumanie"
#: trainvel/gtfs/models.py:47
msgid "San Marino"
msgstr "Saint-Marin"
#: trainvel/gtfs/models.py:48
msgid "Serbia"
msgstr "Serbie"
#: trainvel/gtfs/models.py:49
msgid "Slovakia"
msgstr "Slovaquie"
#: trainvel/gtfs/models.py:50
msgid "Slovenia"
msgstr "Slovénie"
#: trainvel/gtfs/models.py:51
msgid "Spain"
msgstr "Espagne"
#: trainvel/gtfs/models.py:52
msgid "Sweden"
msgstr "Suède"
#: trainvel/gtfs/models.py:53
msgid "Switzerland"
msgstr "Suisse"
#: trainvel/gtfs/models.py:54
msgid "Turkey"
msgstr "Turquie"
#: trainvel/gtfs/models.py:55
msgid "United Kingdom"
msgstr "Royaume-Uni"
#: trainvel/gtfs/models.py:56
msgid "Ukraine"
msgstr "Ukraine"
#: trainvel/gtfs/models.py:60
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: trainvel/gtfs/models.py:61
msgid "Station"
msgstr "Gare"
#: trainvel/gtfs/models.py:62
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: trainvel/gtfs/models.py:63
msgid "Generic node"
msgstr "Nœud générique"
#: trainvel/gtfs/models.py:64
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: trainvel/gtfs/models.py:68
msgid "No information"
msgstr "Pas d'information"
#: trainvel/gtfs/models.py:69
msgid "Possible"
msgstr "Possible"
#: trainvel/gtfs/models.py:70 trainvel/gtfs/models.py:100
msgid "Not possible"
msgstr "Impossible"
#: trainvel/gtfs/models.py:74
msgid "Regular"
msgstr "Régulier"
#: trainvel/gtfs/models.py:75
msgid "None"
msgstr "Aucun"
#: trainvel/gtfs/models.py:76
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: trainvel/gtfs/models.py:77
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: trainvel/gtfs/models.py:81
msgid "Tram"
msgstr "Tram"
#: trainvel/gtfs/models.py:82
msgid "Metro"
msgstr "Métro"
#: trainvel/gtfs/models.py:83
msgid "Rail"
msgstr "Rail"
#: trainvel/gtfs/models.py:84
msgid "Bus"
msgstr "Bus"
#: trainvel/gtfs/models.py:85
msgid "Ferry"
msgstr "Ferry"
#: trainvel/gtfs/models.py:86
msgid "Cable car"
msgstr "Câble"
#: trainvel/gtfs/models.py:87
msgid "Gondola"
msgstr "Gondole"
#: trainvel/gtfs/models.py:88
msgid "Funicular"
msgstr "Funiculaire"
#: trainvel/gtfs/models.py:92
msgid "Outbound"
msgstr "Vers l'extérieur"
#: trainvel/gtfs/models.py:93
msgid "Inbound"
msgstr "Vers l'intérieur"
#: trainvel/gtfs/models.py:97
msgid "Recommended"
msgstr "Recommandé"
#: trainvel/gtfs/models.py:98
msgid "Timed"
msgstr "Correspondance programmée"
#: trainvel/gtfs/models.py:99
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: trainvel/gtfs/models.py:104 trainvel/gtfs/models.py:110
msgid "Added"
msgstr "Ajouté"
#: trainvel/gtfs/models.py:105
msgid "Removed"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:109 trainvel/gtfs/models.py:119
msgid "Scheduled"
msgstr "Planifié"
#: trainvel/gtfs/models.py:111 trainvel/gtfs/models.py:122
msgid "Unscheduled"
msgstr "Non planifié"
#: trainvel/gtfs/models.py:112
msgid "Canceled"
msgstr "Annulé"
#: trainvel/gtfs/models.py:113
msgid "Replacement"
msgstr "Remplacé"
#: trainvel/gtfs/models.py:114
msgid "Duplicated"
msgstr "Dupliqué"
#: trainvel/gtfs/models.py:115
msgid "Deleted"
msgstr "Supprimé"
#: trainvel/gtfs/models.py:120
msgid "Skipped"
msgstr "Sauté"
#: trainvel/gtfs/models.py:121
msgid "No data"
msgstr "Pas de données"
#: trainvel/gtfs/models.py:129
msgid "code"
msgstr "code"
#: trainvel/gtfs/models.py:130
msgid "Unique code of the feed."
msgstr "Code unique du flux."
#: trainvel/gtfs/models.py:135
msgid "name"
msgstr "nom"
#: trainvel/gtfs/models.py:137
msgid "Full name that describes the feed."
msgstr "Nom complet qui décrit le flux."
#: trainvel/gtfs/models.py:142
msgid "country"
msgstr "pays"
#: trainvel/gtfs/models.py:147
msgid "feed URL"
msgstr "URL du flux"
#: trainvel/gtfs/models.py:148
msgid ""
"URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs."
"org/schedule/ for more information."
msgstr ""
"URL où télécharger le flux GTFS. Doit pointer vers une archive ZIP. Voir "
"https://gtfs.org/fr/schedule/ pour plus d'informations."
#: trainvel/gtfs/models.py:153
msgid "realtime feed URL"
msgstr "URL du flux temps réel"
#: trainvel/gtfs/models.py:156
msgid ""
"URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://"
"gtfs.org/realtime/ for more information."
msgstr ""
"URL où télécharger le flux GTFS-Temps réel, au format GTFS-RT. Voir https://"
"gtfs.org/fr/realtime/ pour plus d'informations."
#: trainvel/gtfs/models.py:161
msgid "last modified date"
msgstr "Date de dernière modification"
#: trainvel/gtfs/models.py:168
msgid "ETag"
msgstr "ETag"
#: trainvel/gtfs/models.py:171
msgid ""
"If applicable, corresponds to the tag of the last downloaded file. If it is "
"not modified, the file is the same."
msgstr ""
"Si applicable, correspond au tag du dernier fichier téléchargé. S'il n'est "
"pas modifié, le fichier est considéré comme identique."
#: trainvel/gtfs/models.py:179 trainvel/gtfs/models.py:226
#: trainvel/gtfs/models.py:326 trainvel/gtfs/models.py:405
#: trainvel/gtfs/models.py:486 trainvel/gtfs/models.py:696
#: trainvel/gtfs/models.py:811
msgid "GTFS feed"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:180
msgid "GTFS feeds"
msgstr "flux GTFS"
#: trainvel/gtfs/models.py:189
msgid "Agency ID"
msgstr "ID de l'agence"
#: trainvel/gtfs/models.py:194
msgid "Agency name"
msgstr "Nom de l'agence"
#: trainvel/gtfs/models.py:198
msgid "Agency URL"
msgstr "URL de l'agence"
#: trainvel/gtfs/models.py:203
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: trainvel/gtfs/models.py:208
msgid "Agency language"
msgstr "Langue de l'agence"
#: trainvel/gtfs/models.py:214
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: trainvel/gtfs/models.py:219
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: trainvel/gtfs/models.py:233 trainvel/gtfs/models.py:356
msgid "Agency"
msgstr "Agence"
#: trainvel/gtfs/models.py:234
msgid "Agencies"
msgstr "Agences"
#: trainvel/gtfs/models.py:243 trainvel/gtfs/models.py:593
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: trainvel/gtfs/models.py:248
msgid "Stop code"
msgstr "Code de l'arrêt"
#: trainvel/gtfs/models.py:254
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: trainvel/gtfs/models.py:259
msgid "Stop description"
msgstr "Description de l'arrêt"
#: trainvel/gtfs/models.py:264
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: trainvel/gtfs/models.py:268
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: trainvel/gtfs/models.py:273
msgid "Zone ID"
msgstr "ID de la zone"
#: trainvel/gtfs/models.py:278
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: trainvel/gtfs/models.py:283
msgid "Location type"
msgstr "Type de localisation"
#: trainvel/gtfs/models.py:292
msgid "Parent station"
msgstr "Gare parente"
#: trainvel/gtfs/models.py:300
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: trainvel/gtfs/models.py:306
msgid "Level ID"
msgstr "ID du niveau"
#: trainvel/gtfs/models.py:311
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: trainvel/gtfs/models.py:319
msgid "Platform code"
msgstr "Code du quai"
#: trainvel/gtfs/models.py:338
msgid "Stop"
msgstr "Arrêt"
#: trainvel/gtfs/models.py:339
msgid "Stops"
msgstr "Arrêts"
#: trainvel/gtfs/models.py:350 trainvel/gtfs/models.py:572
#: trainvel/gtfs/models.py:713 trainvel/gtfs/models.py:746
msgid "ID"
msgstr "Identifiant"
#: trainvel/gtfs/models.py:365
msgid "Route short name"
msgstr "Nom court de la ligne"
#: trainvel/gtfs/models.py:370
msgid "Route long name"
msgstr "Nom long de la ligne"
#: trainvel/gtfs/models.py:376
msgid "Route description"
msgstr "Description de la ligne"
#: trainvel/gtfs/models.py:381
msgid "Route type"
msgstr "Type de ligne"
#: trainvel/gtfs/models.py:386
msgid "Route URL"
msgstr "URL de la ligne"
#: trainvel/gtfs/models.py:392
msgid "Route color"
msgstr "Couleur de la ligne"
#: trainvel/gtfs/models.py:398
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: trainvel/gtfs/models.py:412 trainvel/gtfs/models.py:428
msgid "Route"
msgstr "Ligne"
#: trainvel/gtfs/models.py:413
msgid "Routes"
msgstr "Lignes"
#: trainvel/gtfs/models.py:422
msgid "Trip ID"
msgstr "ID du trajet"
#: trainvel/gtfs/models.py:435 trainvel/gtfs/models.py:719
msgid "Service"
msgstr "Service"
#: trainvel/gtfs/models.py:441
msgid "Trip headsign"
msgstr "Destination du trajet"
#: trainvel/gtfs/models.py:447
msgid "Trip short name"
msgstr "Nom court du trajet"
#: trainvel/gtfs/models.py:452
msgid "Direction"
msgstr "Direction"
#: trainvel/gtfs/models.py:459
msgid "Block ID"
msgstr "ID du bloc"
#: trainvel/gtfs/models.py:465
msgid "Shape ID"
msgstr "ID de la forme"
#: trainvel/gtfs/models.py:470
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: trainvel/gtfs/models.py:477
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: trainvel/gtfs/models.py:500 trainvel/gtfs/models.py:509
#: trainvel/gtfs/models.py:552 trainvel/gtfs/models.py:554
msgid "Unknown"
msgstr "Inconnu"
#: trainvel/gtfs/models.py:557
msgid "Origin → Destination"
msgstr "Origine → Destination"
#: trainvel/gtfs/models.py:563 trainvel/gtfs/models.py:578
#: trainvel/gtfs/models.py:825
msgid "Trip"
msgstr "Trajet"
#: trainvel/gtfs/models.py:564
msgid "Trips"
msgstr "Trajets"
#: trainvel/gtfs/models.py:583 trainvel/gtfs/models.py:876
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: trainvel/gtfs/models.py:587 trainvel/gtfs/models.py:884
msgid "Departure time"
msgstr "Heure de départ"
#: trainvel/gtfs/models.py:598
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: trainvel/gtfs/models.py:603
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: trainvel/gtfs/models.py:608
msgid "Pickup type"
msgstr "Type de prise en charge"
#: trainvel/gtfs/models.py:615
msgid "Drop off type"
msgstr "Type de dépose"
#: trainvel/gtfs/models.py:622
msgid "Timepoint"
msgstr "Ponctualité"
#: trainvel/gtfs/models.py:645 trainvel/gtfs/models.py:866
msgid "Stop time"
msgstr "Heure d'arrêt"
#: trainvel/gtfs/models.py:646
msgid "Stop times"
msgstr "Heures d'arrêt"
#: trainvel/gtfs/models.py:654
msgid "Service ID"
msgstr "ID du service"
#: trainvel/gtfs/models.py:658
msgid "Monday"
msgstr "Lundi"
#: trainvel/gtfs/models.py:662
msgid "Tuesday"
msgstr "Mardi"
#: trainvel/gtfs/models.py:666
msgid "Wednesday"
msgstr "Mercredi"
#: trainvel/gtfs/models.py:670
msgid "Thursday"
msgstr "Jeudi"
#: trainvel/gtfs/models.py:674
msgid "Friday"
msgstr "Vendredi"
#: trainvel/gtfs/models.py:678
msgid "Saturday"
msgstr "Samedi"
#: trainvel/gtfs/models.py:682
msgid "Sunday"
msgstr "Dimanche"
#: trainvel/gtfs/models.py:686 trainvel/gtfs/models.py:831
msgid "Start date"
msgstr "Date de début"
#: trainvel/gtfs/models.py:690
msgid "End date"
msgstr "Date de fin"
#: trainvel/gtfs/models.py:703
msgid "Calendar"
msgstr "Calendrier"
#: trainvel/gtfs/models.py:704
msgid "Calendars"
msgstr "Calendriers"
#: trainvel/gtfs/models.py:724
msgid "Date"
msgstr "Date"
#: trainvel/gtfs/models.py:728
msgid "Exception type"
msgstr "Type d'exception"
#: trainvel/gtfs/models.py:736
msgid "Calendar date"
msgstr "Date du calendrier"
#: trainvel/gtfs/models.py:737
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: trainvel/gtfs/models.py:752
msgid "From stop"
msgstr "Depuis l'arrêt"
#: trainvel/gtfs/models.py:759
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: trainvel/gtfs/models.py:764
msgid "Transfer type"
msgstr "Type de correspondance"
#: trainvel/gtfs/models.py:770
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: trainvel/gtfs/models.py:775
msgid "Transfer"
msgstr "Correspondance"
#: trainvel/gtfs/models.py:776
msgid "Transfers"
msgstr "Correspondances"
#: trainvel/gtfs/models.py:783
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: trainvel/gtfs/models.py:787
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: trainvel/gtfs/models.py:792
msgid "Feed language"
msgstr "Langue du flux"
#: trainvel/gtfs/models.py:796
msgid "Feed start date"
msgstr "Date de début du flux"
#: trainvel/gtfs/models.py:800
msgid "Feed end date"
msgstr "Date de fin du flux"
#: trainvel/gtfs/models.py:805
msgid "Feed version"
msgstr "Version du flux"
#: trainvel/gtfs/models.py:815
msgid "Feed info"
msgstr "Information du flux"
#: trainvel/gtfs/models.py:816
msgid "Feed infos"
msgstr "Informations du flux"
#: trainvel/gtfs/models.py:835
msgid "Start time"
msgstr "Heure de début"
#: trainvel/gtfs/models.py:839 trainvel/gtfs/models.py:888
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: trainvel/gtfs/models.py:848 trainvel/gtfs/models.py:859
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: trainvel/gtfs/models.py:849
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: trainvel/gtfs/models.py:872
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: trainvel/gtfs/models.py:880
msgid "Departure delay"
msgstr "Retard au départ"
#: trainvel/gtfs/models.py:897
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: trainvel/gtfs/models.py:898
msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt"
#~ msgid "TGV"
#~ msgstr "TGV"
#~ msgid "TER"
#~ msgstr "TER"
#~ msgid "Intercités"
#~ msgstr "Intercités"
#~ msgid "Transilien"
#~ msgstr "Transilien"
#~ msgid "Eurostar"
#~ msgstr "Eurostar"
#~ msgid "Trenitalia"
#~ msgstr "Trenitalia"
#~ msgid "Renfe"
#~ msgstr "Renfe"
#~ msgid "ÖBB"
#~ msgstr "ÖBB"
#~ msgid "Last update"
#~ msgstr "Dernière mise à jour"
#~ msgid "Transport type"
#~ msgstr "Type de transport"

View File

@ -1,392 +0,0 @@
import csv
from datetime import datetime, timedelta
from io import BytesIO
from zipfile import ZipFile
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from tqdm import tqdm
from trainvel.gtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, Transfer, Trip, \
PickupType
class Command(BaseCommand):
help = "Update the Trainvel GTFS database."
def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
parser.add_argument('--bulk_size', type=int, default=1000, help="Number of objects to create in bulk.")
parser.add_argument('--dry-run', action='store_true',
help="Do not update the database, only print what would be done.")
parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.")
def handle(self, debug: bool = False, bulk_size: int = 100, dry_run: bool = False, force: bool = False,
verbosity: int = 1, *args, **options):
if dry_run:
self.stdout.write(self.style.WARNING("Dry run mode activated."))
self.stdout.write("Updating database...")
for gtfs_feed in GTFSFeed.objects.all():
gtfs_code = gtfs_feed.code
if not force:
# Check if the source file was updated
resp = requests.head(gtfs_feed.feed_url, allow_redirects=True)
if 'ETag' in resp.headers and gtfs_feed.etag:
if resp.headers['ETag'] == gtfs_feed.etag:
if verbosity >= 1:
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
continue
if 'Last-Modified' in resp.headers and gtfs_feed.last_modified:
last_modified = resp.headers['Last-Modified']
last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
if last_modified <= gtfs_feed.last_modified:
if verbosity >= 1:
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
continue
self.stdout.write(f"Downloading GTFS feed for {gtfs_feed}...")
resp = requests.get(gtfs_feed.feed_url, allow_redirects=True, stream=True)
with ZipFile(BytesIO(resp.content)) as zipfile:
def read_file(filename):
lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines()
return [line.strip() for line in lines]
agencies = []
for agency_dict in csv.DictReader(read_file("agency.txt")):
agency_dict: dict
# if gtfs_code == "FR-EUROSTAR" \
# and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER':
# continue
agency = Agency(
id=f"{gtfs_code}-{agency_dict['agency_id']}",
name=agency_dict['agency_name'],
url=agency_dict['agency_url'],
timezone=agency_dict['agency_timezone'],
lang=agency_dict.get('agency_lang', "fr"),
phone=agency_dict.get('agency_phone', ""),
email=agency_dict.get('agency_email', ""),
gtfs_feed=gtfs_feed,
)
agencies.append(agency)
if agencies and not dry_run:
Agency.objects.bulk_create(agencies,
update_conflicts=True,
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email',
'gtfs_feed'],
unique_fields=['id'])
agencies.clear()
stops = []
for stop_dict in csv.DictReader(tqdm(read_file("stops.txt"), desc="Stops")):
stop_dict: dict
stop_id = stop_dict['stop_id']
stop_id = f"{gtfs_code}-{stop_id}"
parent_station_id = stop_dict.get('parent_station', None)
parent_station_id = f"{gtfs_code}-{parent_station_id}" if parent_station_id else None
stop = Stop(
id=stop_id,
name=stop_dict['stop_name'],
desc=stop_dict.get('stop_desc', ""),
lat=stop_dict['stop_lat'],
lon=stop_dict['stop_lon'],
zone_id=stop_dict.get('zone_id', ""),
url=stop_dict.get('stop_url', ""),
location_type=stop_dict.get('location_type', 0) or 0,
parent_station_id=parent_station_id,
timezone=stop_dict.get('stop_timezone', ""),
wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0),
level_id=stop_dict.get('level_id', ""),
platform_code=stop_dict.get('platform_code', ""),
gtfs_feed=gtfs_feed,
)
stops.append(stop)
if stops and not dry_run:
Stop.objects.bulk_create(stops,
batch_size=bulk_size,
update_conflicts=True,
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
'location_type', 'parent_station_id', 'timezone',
'wheelchair_boarding', 'level_id', 'platform_code',
'gtfs_feed'],
unique_fields=['id'])
stops.clear()
routes = []
for route_dict in csv.DictReader(tqdm(read_file("routes.txt"), desc="Routes")):
route_dict: dict
route_id = route_dict['route_id']
route_id = f"{gtfs_code}-{route_id}"
route = Route(
id=route_id,
agency_id=f"{gtfs_code}-{route_dict['agency_id']}",
short_name=route_dict['route_short_name'],
long_name=route_dict['route_long_name'],
desc=route_dict.get('route_desc', ""),
type=route_dict['route_type'],
url=route_dict.get('route_url', ""),
color=route_dict.get('route_color', ""),
text_color=route_dict.get('route_text_color', ""),
gtfs_feed=gtfs_feed,
)
routes.append(route)
if len(routes) >= bulk_size and not dry_run:
Route.objects.bulk_create(routes,
update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color',
'gtfs_feed'],
unique_fields=['id'])
routes.clear()
if routes and not dry_run:
Route.objects.bulk_create(routes,
update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color',
'gtfs_feed'],
unique_fields=['id'])
routes.clear()
# Calendar.objects.filter(gtfs_feed=gtfs_feed).delete()
calendars = {}
if "calendar.txt" in zipfile.namelist():
for calendar_dict in csv.DictReader(tqdm(read_file("calendar.txt"), desc="Calendars")):
calendar_dict: dict
calendar = Calendar(
id=f"{gtfs_code}-{calendar_dict['service_id']}",
monday=calendar_dict['monday'],
tuesday=calendar_dict['tuesday'],
wednesday=calendar_dict['wednesday'],
thursday=calendar_dict['thursday'],
friday=calendar_dict['friday'],
saturday=calendar_dict['saturday'],
sunday=calendar_dict['sunday'],
start_date=calendar_dict['start_date'],
end_date=calendar_dict['end_date'],
gtfs_feed=gtfs_feed,
)
calendars[calendar.id] = calendar
if len(calendars) >= bulk_size and not dry_run:
Calendar.objects.bulk_create(calendars.values(),
update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'gtfs_feed'],
unique_fields=['id'])
calendars.clear()
if calendars and not dry_run:
Calendar.objects.bulk_create(calendars.values(), update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'gtfs_feed'],
unique_fields=['id'])
calendars.clear()
calendar_dates = []
for calendar_date_dict in csv.DictReader(tqdm(read_file("calendar_dates.txt"), desc="Calendar dates")):
calendar_date_dict: dict
calendar_date = CalendarDate(
id=f"{gtfs_code}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}",
service_id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
date=calendar_date_dict['date'],
exception_type=calendar_date_dict['exception_type'],
)
calendar_dates.append(calendar_date)
if calendar_date.service_id not in calendars:
calendar = Calendar(
id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
monday=False,
tuesday=False,
wednesday=False,
thursday=False,
friday=False,
saturday=False,
sunday=False,
start_date=calendar_date_dict['date'],
end_date=calendar_date_dict['date'],
gtfs_feed=gtfs_feed,
)
calendars[calendar.id] = calendar
else:
calendar = calendars[f"{gtfs_code}-{calendar_date_dict['service_id']}"]
if calendar.start_date > calendar_date.date:
calendar.start_date = calendar_date.date
if calendar.end_date < calendar_date.date:
calendar.end_date = calendar_date.date
if calendar_dates and not dry_run:
Calendar.objects.bulk_create(calendars.values(),
batch_size=bulk_size,
update_conflicts=True,
update_fields=['start_date', 'end_date', 'gtfs_feed'],
unique_fields=['id'])
CalendarDate.objects.bulk_create(calendar_dates,
batch_size=bulk_size,
update_conflicts=True,
update_fields=['service_id', 'date', 'exception_type'],
unique_fields=['id'])
calendars.clear()
calendar_dates.clear()
trips = []
for trip_dict in csv.DictReader(tqdm(read_file("trips.txt"), desc="Trips")):
trip_dict: dict
trip_id = trip_dict['trip_id']
route_id = trip_dict['route_id']
trip_id = f"{gtfs_code}-{trip_id}"
route_id = f"{gtfs_code}-{route_id}"
trip = Trip(
id=trip_id,
route_id=route_id,
service_id=f"{gtfs_code}-{trip_dict['service_id']}",
headsign=trip_dict.get('trip_headsign', ""),
short_name=trip_dict.get('trip_short_name', ""),
direction_id=trip_dict.get('direction_id', None) or None,
block_id=trip_dict.get('block_id', ""),
shape_id=trip_dict.get('shape_id', ""),
wheelchair_accessible=trip_dict.get('wheelchair_accessible', None),
bikes_allowed=trip_dict.get('bikes_allowed', None),
gtfs_feed=gtfs_feed,
)
trips.append(trip)
if len(trips) >= bulk_size and not dry_run:
Trip.objects.bulk_create(trips,
update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id'])
trips.clear()
if trips and not dry_run:
Trip.objects.bulk_create(trips,
update_conflicts=True,
update_fields=['route_id', 'service_id', 'headsign', 'short_name',
'direction_id', 'block_id', 'shape_id',
'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
unique_fields=['id'])
trips.clear()
stop_times = []
for stop_time_dict in csv.DictReader(tqdm(read_file("stop_times.txt"), desc="Stop times")):
stop_time_dict: dict
stop_id = stop_time_dict['stop_id']
stop_id = f"{gtfs_code}-{stop_id}"
trip_id = stop_time_dict['trip_id']
trip_id = f"{gtfs_code}-{trip_id}"
arr_time = stop_time_dict['arrival_time']
arr_h, arr_m, arr_s = map(int, arr_time.split(':'))
arr_time = arr_h * 3600 + arr_m * 60 + arr_s
dep_time = stop_time_dict['departure_time']
dep_h, dep_m, dep_s = map(int, dep_time.split(':'))
dep_time = dep_h * 3600 + dep_m * 60 + dep_s
pickup_type = stop_time_dict.get('pickup_type', 0)
drop_off_type = stop_time_dict.get('drop_off_type', 0)
if stop_time_dict['stop_sequence'] == "1":
# First stop
drop_off_type = PickupType.NONE
elif arr_time == dep_time:
# Last stop
pickup_type = PickupType.NONE
st = StopTime(
id=f"{gtfs_code}-{stop_time_dict['trip_id']}-{stop_time_dict['stop_id']}"
f"-{stop_time_dict['departure_time']}",
trip_id=trip_id,
arrival_time=timedelta(seconds=arr_time),
departure_time=timedelta(seconds=dep_time),
stop_id=stop_id,
stop_sequence=stop_time_dict['stop_sequence'],
stop_headsign=stop_time_dict.get('stop_headsign', ""),
pickup_type=pickup_type,
drop_off_type=drop_off_type,
timepoint=stop_time_dict.get('timepoint', None),
)
stop_times.append(st)
if len(stop_times) >= bulk_size and not dry_run:
StopTime.objects.bulk_create(stop_times,
update_conflicts=True,
update_fields=['stop_id', 'arrival_time', 'departure_time',
'stop_headsign', 'pickup_type',
'drop_off_type', 'timepoint'],
unique_fields=['id'])
stop_times.clear()
if stop_times and not dry_run:
StopTime.objects.bulk_create(stop_times,
update_conflicts=True,
update_fields=['stop_id', 'arrival_time', 'departure_time',
'stop_headsign', 'pickup_type',
'drop_off_type', 'timepoint'],
unique_fields=['id'])
stop_times.clear()
if "transfers.txt" in zipfile.namelist():
transfers = []
for transfer_dict in csv.DictReader(tqdm(read_file("transfers.txt"), desc="Transfers")):
transfer_dict: dict
from_stop_id = transfer_dict['from_stop_id']
to_stop_id = transfer_dict['to_stop_id']
from_stop_id = f"{gtfs_code}-{from_stop_id}"
to_stop_id = f"{gtfs_code}-{to_stop_id}"
transfer = Transfer(
id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}",
from_stop_id=from_stop_id,
to_stop_id=to_stop_id,
transfer_type=transfer_dict['transfer_type'],
min_transfer_time=transfer_dict['min_transfer_time'],
)
transfers.append(transfer)
if len(transfers) >= bulk_size and not dry_run:
Transfer.objects.bulk_create(transfers,
update_conflicts=True,
update_fields=['transfer_type', 'min_transfer_time'],
unique_fields=['id'])
transfers.clear()
if transfers and not dry_run:
Transfer.objects.bulk_create(transfers,
update_conflicts=True,
update_fields=['transfer_type', 'min_transfer_time'],
unique_fields=['id'])
transfers.clear()
if "feed_info.txt" in zipfile.namelist() and not dry_run:
for feed_info_dict in csv.DictReader(tqdm(read_file("feed_info.txt"), desc="Feed info")):
feed_info_dict: dict
FeedInfo.objects.update_or_create(
publisher_name=feed_info_dict['feed_publisher_name'],
gtfs_feed=gtfs_feed,
defaults=dict(
publisher_url=feed_info_dict['feed_publisher_url'],
lang=feed_info_dict['feed_lang'],
start_date=feed_info_dict.get('feed_start_date', datetime.now().date()),
end_date=feed_info_dict.get('feed_end_date', datetime.now().date()),
version=feed_info_dict.get('feed_version', 1),
)
)
if 'ETag' in resp.headers:
gtfs_feed.etag = resp.headers['ETag']
gtfs_feed.save()
if 'Last-Modified' in resp.headers:
last_modified = resp.headers['Last-Modified']
gtfs_feed.last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
gtfs_feed.save()

View File

@ -1,203 +0,0 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from trainvel import settings
from trainvel.gtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate
from trainvel.gtfs.models import Calendar, CalendarDate, ExceptionType, GTFSFeed, PickupType, \
Route, RouteType, StopScheduleRelationship, StopTime, StopTimeUpdate, \
Trip, TripUpdate, TripScheduleRelationship
class Command(BaseCommand):
help = "Update the Trainvel GTFS Realtime database."
def add_arguments(self, parser):
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
def handle(self, debug: bool = False, verbosity: int = 1, *args, **options):
for gtfs_feed in GTFSFeed.objects.all():
if not gtfs_feed.rt_feed_url:
if verbosity >= 2:
self.stdout.write(self.style.WARNING(f"No GTFS-RT feed found for {gtfs_feed}."))
continue
self.stdout.write(f"Updating GTFS-RT feed for {gtfs_feed}")
gtfs_code = gtfs_feed.code
headers = {}
if gtfs_code == "CH-ALL":
headers["Authorization"] = settings.OPENTRANSPORTDATA_SWISS_TOKEN
resp = requests.get(gtfs_feed.rt_feed_url, allow_redirects=True)
feed_message = FeedMessage()
feed_message.ParseFromString(resp.content)
stop_times_updates = []
if debug:
with open(f'feed_message-{gtfs_code}.txt', 'w') as f:
f.write(str(feed_message))
for entity in feed_message.entity:
if entity.HasField("trip_update"):
trip_update = entity.trip_update
trip_id = trip_update.trip.trip_id
trip_id = f"{gtfs_code}-{trip_id}"
start_date = date(year=int(trip_update.trip.start_date[:4]),
month=int(trip_update.trip.start_date[4:6]),
day=int(trip_update.trip.start_date[6:]))
start_dt = datetime.combine(start_date, time(0), tzinfo=ZoneInfo("Europe/Paris"))
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
# C'est un trajet nouveau. On crée le trajet associé.
self.create_trip(trip_update, trip_id, start_dt, gtfs_feed)
if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
continue
# Création du TripUpdate
tu, _created = TripUpdate.objects.update_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
defaults=dict(
schedule_relationship=trip_update.trip.schedule_relationship,
)
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop_id = f"{gtfs_code}-{stop_id}"
if StopTime.objects.filter(trip_id=trip_id, stop=stop_id).exists():
st = StopTime.objects.filter(trip_id=trip_id, stop=stop_id)
if st.count() > 1:
st = st.get(stop_sequence=stop_sequence)
else:
st = st.first()
else:
# Stop is added
st = StopTime.objects.create(
id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id,
stop_id=stop_id,
defaults={
"stop_sequence": stop_sequence,
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"departure_time": datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt,
"pickup_type": (PickupType.REGULAR if stop_time_update.departure.time
else PickupType.NONE),
"drop_off_type": (PickupType.REGULAR if stop_time_update.arrival.time
else PickupType.NONE),
}
)
if stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE:
st.pickup_type = PickupType.NONE
st.drop_off_type = PickupType.NONE
st.save()
if st.stop_sequence != stop_sequence:
st.stop_sequence = stop_sequence
st.save()
st_update = StopTimeUpdate(
trip_update=tu,
stop_time=st,
arrival_delay=timedelta(seconds=stop_time_update.arrival.delay),
arrival_time=datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")),
departure_delay=timedelta(seconds=stop_time_update.departure.delay),
departure_time=datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")),
schedule_relationship=stop_time_update.schedule_relationship
or StopScheduleRelationship.SCHEDULED,
)
stop_times_updates.append(st_update)
else:
self.stdout.write(str(entity))
StopTimeUpdate.objects.bulk_create(stop_times_updates,
update_conflicts=True,
update_fields=['arrival_delay', 'arrival_time',
'departure_delay', 'departure_time'],
unique_fields=['trip_update', 'stop_time'])
def create_trip(self, trip_update: GTFSTripUpdate, trip_id: str, start_dt: datetime, gtfs_feed: GTFSFeed) -> None:
headsign = trip_id[5:-1]
gtfs_code = gtfs_feed.code
route, _created = Route.objects.get_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
gtfs_feed=gtfs_feed,
type=RouteType.RAIL,
short_name="ADDED",
long_name="ADDED ROUTE",
)
Calendar.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}",
defaults={
"gtfs_feed": gtfs_feed,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_dt.date(),
"end_date": start_dt.date(),
}
)
CalendarDate.objects.update_or_create(
id=f"{gtfs_code}-ADDED-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": ExceptionType.ADDED,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route.id,
"service_id": f"{gtfs_code}-ADDED-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
"gtfs_feed": gtfs_feed,
}
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop_id = f"{gtfs_code}-{stop_id}"
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
dep_time = datetime.fromtimestamp(stop_time_update.departure.time,
tz=ZoneInfo("Europe/Paris")) - start_dt
pickup_type = PickupType.REGULAR if stop_time_update.departure.time and stop_sequence > 0 \
else PickupType.NONE
drop_off_type = PickupType.REGULAR if stop_time_update.arrival.time \
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_time_update.stop_id}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"stop_sequence": stop_sequence,
"arrival_time": arr_time,
"departure_time": dep_time,
"pickup_type": pickup_type,
"drop_off_type": drop_off_type,
}
)

View File

@ -1,911 +0,0 @@
# Generated by Django 5.0.1 on 2024-05-09 17:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="StopTime",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("arrival_time", models.DurationField(verbose_name="Arrival time")),
("departure_time", models.DurationField(verbose_name="Departure time")),
("stop_sequence", models.IntegerField(verbose_name="Stop sequence")),
(
"stop_headsign",
models.CharField(
blank=True, max_length=255, verbose_name="Stop headsign"
),
),
(
"pickup_type",
models.IntegerField(
choices=[
(0, "Regular"),
(1, "None"),
(2, "Must phone agency"),
(3, "Must coordinate with driver"),
],
default=0,
null=True,
verbose_name="Pickup type",
),
),
(
"drop_off_type",
models.IntegerField(
choices=[
(0, "Regular"),
(1, "None"),
(2, "Must phone agency"),
(3, "Must coordinate with driver"),
],
default=0,
null=True,
verbose_name="Drop off type",
),
),
(
"timepoint",
models.BooleanField(
default=True, null=True, verbose_name="Timepoint"
),
),
],
options={
"verbose_name": "Stop time",
"verbose_name_plural": "Stop times",
},
),
migrations.CreateModel(
name="Trip",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="Trip ID",
),
),
(
"headsign",
models.CharField(
blank=True, max_length=255, verbose_name="Trip headsign"
),
),
(
"short_name",
models.CharField(
blank=True, max_length=255, verbose_name="Trip short name"
),
),
(
"direction_id",
models.IntegerField(
choices=[(0, "Outbound"), (1, "Inbound")],
null=True,
verbose_name="Direction",
),
),
(
"block_id",
models.CharField(
blank=True, max_length=255, verbose_name="Block ID"
),
),
(
"shape_id",
models.CharField(
blank=True, max_length=255, verbose_name="Shape ID"
),
),
(
"wheelchair_accessible",
models.IntegerField(
choices=[
(0, "No information"),
(1, "Possible"),
(2, "Not possible"),
],
default=0,
null=True,
verbose_name="Wheelchair accessible",
),
),
(
"bikes_allowed",
models.IntegerField(
choices=[
(0, "No information"),
(1, "Possible"),
(2, "Not possible"),
],
default=0,
null=True,
verbose_name="Bikes allowed",
),
),
],
options={
"verbose_name": "Trip",
"verbose_name_plural": "Trips",
},
),
migrations.CreateModel(
name="GTFSFeed",
fields=[
(
"code",
models.CharField(
help_text="Unique code of the feed.",
max_length=64,
primary_key=True,
serialize=False,
verbose_name="code",
),
),
(
"name",
models.CharField(
help_text="Full name that describes the feed.",
max_length=255,
unique=True,
verbose_name="name",
),
),
(
"country",
models.CharField(
choices=[
("AL", "Albania"),
("AD", "Andorra"),
("AM", "Armenia"),
("AT", "Austria"),
("AZ", "Azerbaijan"),
("BE", "Belgium"),
("BA", "Bosnia and Herzegovina"),
("BG", "Bulgaria"),
("HR", "Croatia"),
("CY", "Cyprus"),
("CZ", "Czech Republic"),
("DK", "Denmark"),
("EE", "Estonia"),
("FI", "Finland"),
("FR", "France"),
("GE", "Georgia"),
("DE", "Germany"),
("GR", "Greece"),
("HU", "Hungary"),
("IS", "Iceland"),
("IE", "Ireland"),
("IT", "Italy"),
("LV", "Latvia"),
("LI", "Liechtenstein"),
("LT", "Lithuania"),
("LU", "Luxembourg"),
("MT", "Malta"),
("MD", "Moldova"),
("MC", "Monaco"),
("ME", "Montenegro"),
("NL", "Netherlands"),
("MK", "North Macedonia"),
("NO", "Norway"),
("PL", "Poland"),
("PT", "Portugal"),
("RO", "Romania"),
("SM", "San Marino"),
("RS", "Serbia"),
("SK", "Slovakia"),
("SI", "Slovenia"),
("ES", "Spain"),
("SE", "Sweden"),
("CH", "Switzerland"),
("TR", "Turkey"),
("GB", "United Kingdom"),
("UA", "Ukraine"),
],
max_length=2,
verbose_name="country",
),
),
(
"feed_url",
models.URLField(
help_text="URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs.org/schedule/ for more information.",
verbose_name="feed URL",
),
),
(
"rt_feed_url",
models.URLField(
blank=True,
default="",
help_text="URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://gtfs.org/realtime/ for more information.",
verbose_name="realtime feed URL",
),
),
(
"last_modified",
models.DateTimeField(
default=None, null=True, verbose_name="last modified date"
),
),
(
"etag",
models.CharField(
blank=True,
default="",
help_text="If applicable, corresponds to the tag of the last downloaded file. If it is not modified, the file is the same.",
max_length=255,
verbose_name="ETag",
),
),
],
options={
"verbose_name": "GTFS feed",
"verbose_name_plural": "GTFS feeds",
"ordering": ("country", "name"),
"indexes": [
models.Index(fields=["name"], name="gtfs_gtfsfe_name_aabd02_idx")
],
},
),
migrations.CreateModel(
name="Calendar",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="Service ID",
),
),
("monday", models.BooleanField(verbose_name="Monday")),
("tuesday", models.BooleanField(verbose_name="Tuesday")),
("wednesday", models.BooleanField(verbose_name="Wednesday")),
("thursday", models.BooleanField(verbose_name="Thursday")),
("friday", models.BooleanField(verbose_name="Friday")),
("saturday", models.BooleanField(verbose_name="Saturday")),
("sunday", models.BooleanField(verbose_name="Sunday")),
("start_date", models.DateField(verbose_name="Start date")),
("end_date", models.DateField(verbose_name="End date")),
(
"gtfs_feed",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
],
options={
"verbose_name": "Calendar",
"verbose_name_plural": "Calendars",
"ordering": ("id",),
},
),
migrations.CreateModel(
name="Agency",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="Agency ID",
),
),
("name", models.CharField(max_length=255, verbose_name="Agency name")),
("url", models.URLField(verbose_name="Agency URL")),
(
"timezone",
models.CharField(max_length=255, verbose_name="Agency timezone"),
),
(
"lang",
models.CharField(
blank=True, max_length=255, verbose_name="Agency language"
),
),
(
"phone",
models.CharField(
blank=True, max_length=255, verbose_name="Agency phone"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="Agency email"
),
),
(
"gtfs_feed",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
],
options={
"verbose_name": "Agency",
"verbose_name_plural": "Agencies",
"ordering": ("name",),
},
),
migrations.CreateModel(
name="Route",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"short_name",
models.CharField(max_length=255, verbose_name="Route short name"),
),
(
"long_name",
models.CharField(
blank=True, max_length=255, verbose_name="Route long name"
),
),
(
"desc",
models.CharField(
blank=True, max_length=255, verbose_name="Route description"
),
),
(
"type",
models.IntegerField(
choices=[
(0, "Tram"),
(1, "Metro"),
(2, "Rail"),
(3, "Bus"),
(4, "Ferry"),
(5, "Cable car"),
(6, "Gondola"),
(7, "Funicular"),
],
verbose_name="Route type",
),
),
("url", models.URLField(blank=True, verbose_name="Route URL")),
(
"color",
models.CharField(
blank=True, max_length=255, verbose_name="Route color"
),
),
(
"text_color",
models.CharField(
blank=True, max_length=255, verbose_name="Route text color"
),
),
(
"agency",
models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="routes",
to="gtfs.agency",
verbose_name="Agency",
),
),
(
"gtfs_feed",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
],
options={
"verbose_name": "Route",
"verbose_name_plural": "Routes",
"ordering": ("id",),
},
),
migrations.CreateModel(
name="Stop",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="Stop ID",
),
),
(
"code",
models.CharField(
blank=True, max_length=255, verbose_name="Stop code"
),
),
("name", models.CharField(max_length=255, verbose_name="Stop name")),
(
"desc",
models.CharField(
blank=True, max_length=255, verbose_name="Stop description"
),
),
("lon", models.FloatField(verbose_name="Stop longitude")),
("lat", models.FloatField(verbose_name="Stop latitude")),
(
"zone_id",
models.CharField(
blank=True, max_length=255, verbose_name="Zone ID"
),
),
("url", models.URLField(blank=True, verbose_name="Stop URL")),
(
"location_type",
models.IntegerField(
blank=True,
choices=[
(0, "Stop/platform"),
(1, "Station"),
(2, "Entrance/exit"),
(3, "Generic node"),
(4, "Boarding area"),
],
default=0,
verbose_name="Location type",
),
),
(
"timezone",
models.CharField(
blank=True, max_length=255, verbose_name="Stop timezone"
),
),
(
"level_id",
models.CharField(
blank=True, max_length=255, verbose_name="Level ID"
),
),
(
"wheelchair_boarding",
models.IntegerField(
blank=True,
choices=[
(0, "No information"),
(1, "Possible"),
(2, "Not possible"),
],
default=0,
verbose_name="Wheelchair boarding",
),
),
(
"platform_code",
models.CharField(
blank=True, max_length=255, verbose_name="Platform code"
),
),
(
"gtfs_feed",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
(
"parent_station",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="children",
to="gtfs.stop",
verbose_name="Parent station",
),
),
],
options={
"verbose_name": "Stop",
"verbose_name_plural": "Stops",
"ordering": ("id",),
},
),
migrations.CreateModel(
name="StopTimeUpdate",
fields=[
(
"stop_time",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="update",
serialize=False,
to="gtfs.stoptime",
verbose_name="Stop time",
),
),
("arrival_delay", models.DurationField(verbose_name="Arrival delay")),
("arrival_time", models.DateTimeField(verbose_name="Arrival time")),
(
"departure_delay",
models.DurationField(verbose_name="Departure delay"),
),
("departure_time", models.DateTimeField(verbose_name="Departure time")),
(
"schedule_relationship",
models.IntegerField(
choices=[
(0, "Scheduled"),
(1, "Skipped"),
(2, "No data"),
(3, "Unscheduled"),
],
default=0,
verbose_name="Schedule relationship",
),
),
],
options={
"verbose_name": "Stop time update",
"verbose_name_plural": "Stop time updates",
"ordering": ("trip_update", "stop_time"),
},
),
migrations.AddField(
model_name="stoptime",
name="stop",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_times",
to="gtfs.stop",
verbose_name="Stop ID",
),
),
migrations.CreateModel(
name="Transfer",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"transfer_type",
models.IntegerField(
choices=[
(0, "Recommended"),
(1, "Timed"),
(2, "Minimum time"),
(3, "Not possible"),
],
default=0,
verbose_name="Transfer type",
),
),
(
"min_transfer_time",
models.IntegerField(
blank=True, verbose_name="Minimum transfer time"
),
),
(
"from_stop",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers_from",
to="gtfs.stop",
verbose_name="From stop",
),
),
(
"to_stop",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers_to",
to="gtfs.stop",
verbose_name="To stop",
),
),
],
options={
"verbose_name": "Transfer",
"verbose_name_plural": "Transfers",
"ordering": ("id",),
},
),
migrations.CreateModel(
name="TripUpdate",
fields=[
(
"trip",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="update",
serialize=False,
to="gtfs.trip",
verbose_name="Trip",
),
),
("start_date", models.DateField(verbose_name="Start date")),
("start_time", models.TimeField(verbose_name="Start time")),
(
"schedule_relationship",
models.IntegerField(
choices=[
(0, "Scheduled"),
(1, "Added"),
(2, "Unscheduled"),
(3, "Canceled"),
(5, "Replacement"),
(6, "Duplicated"),
(7, "Deleted"),
],
default=0,
verbose_name="Schedule relationship",
),
),
],
options={
"verbose_name": "Trip update",
"verbose_name_plural": "Trip updates",
"ordering": ("start_date", "trip"),
},
),
migrations.AddField(
model_name="trip",
name="gtfs_feed",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
migrations.AddField(
model_name="trip",
name="route",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="trips",
to="gtfs.route",
verbose_name="Route",
),
),
migrations.AddField(
model_name="trip",
name="service",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="trips",
to="gtfs.calendar",
verbose_name="Service",
),
),
migrations.AddField(
model_name="stoptime",
name="trip",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_times",
to="gtfs.trip",
verbose_name="Trip",
),
),
migrations.CreateModel(
name="CalendarDate",
fields=[
(
"id",
models.CharField(
max_length=255,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateField(verbose_name="Date")),
(
"exception_type",
models.IntegerField(
choices=[(1, "Added"), (2, "Removed")],
verbose_name="Exception type",
),
),
(
"service",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="dates",
to="gtfs.calendar",
verbose_name="Service",
),
),
],
options={
"verbose_name": "Calendar date",
"verbose_name_plural": "Calendar dates",
"ordering": ("id",),
"indexes": [
models.Index(
fields=["service"], name="gtfs_calend_service_211472_idx"
),
models.Index(fields=["date"], name="gtfs_calend_date_e90040_idx"),
],
},
),
migrations.CreateModel(
name="FeedInfo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"publisher_name",
models.CharField(
max_length=255, verbose_name="Feed publisher name"
),
),
("publisher_url", models.URLField(verbose_name="Feed publisher URL")),
(
"lang",
models.CharField(max_length=255, verbose_name="Feed language"),
),
("start_date", models.DateField(verbose_name="Feed start date")),
("end_date", models.DateField(verbose_name="Feed end date")),
(
"version",
models.CharField(max_length=255, verbose_name="Feed version"),
),
(
"gtfs_feed",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gtfs.gtfsfeed",
verbose_name="GTFS feed",
),
),
],
options={
"verbose_name": "Feed info",
"verbose_name_plural": "Feed infos",
"ordering": ("publisher_name",),
"indexes": [
models.Index(
fields=["gtfs_feed"], name="gtfs_feedin_gtfs_fe_73554b_idx"
)
],
},
),
migrations.AddIndex(
model_name="calendar",
index=models.Index(
fields=["gtfs_feed"], name="gtfs_calend_gtfs_fe_ff03d1_idx"
),
),
migrations.AddIndex(
model_name="agency",
index=models.Index(fields=["name"], name="gtfs_agency_name_a6dd2b_idx"),
),
migrations.AddIndex(
model_name="agency",
index=models.Index(
fields=["gtfs_feed"], name="gtfs_agency_gtfs_fe_86414c_idx"
),
),
migrations.AddIndex(
model_name="route",
index=models.Index(
fields=["gtfs_feed"], name="gtfs_route_gtfs_fe_c6ac59_idx"
),
),
migrations.AddIndex(
model_name="stop",
index=models.Index(fields=["name"], name="gtfs_stop_name_1c87d7_idx"),
),
migrations.AddIndex(
model_name="stop",
index=models.Index(fields=["code"], name="gtfs_stop_code_5f4ebc_idx"),
),
migrations.AddIndex(
model_name="stop",
index=models.Index(
fields=["gtfs_feed"], name="gtfs_stop_gtfs_fe_0e17d6_idx"
),
),
migrations.AddIndex(
model_name="tripupdate",
index=models.Index(fields=["trip"], name="gtfs_tripup_trip_id_b3ee0e_idx"),
),
migrations.AlterUniqueTogether(
name="tripupdate",
unique_together={("trip", "start_date", "start_time")},
),
migrations.AddField(
model_name="stoptimeupdate",
name="trip_update",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_time_updates",
to="gtfs.tripupdate",
verbose_name="Trip update",
),
),
migrations.AddIndex(
model_name="trip",
index=models.Index(fields=["route"], name="gtfs_trip_route_i_6d85d9_idx"),
),
migrations.AddIndex(
model_name="trip",
index=models.Index(
fields=["gtfs_feed"], name="gtfs_trip_gtfs_fe_e63eac_idx"
),
),
migrations.AddIndex(
model_name="stoptime",
index=models.Index(fields=["stop"], name="gtfs_stopti_stop_id_64a4e3_idx"),
),
migrations.AddIndex(
model_name="stoptime",
index=models.Index(fields=["trip"], name="gtfs_stopti_trip_id_bec7fe_idx"),
),
migrations.AddIndex(
model_name="stoptimeupdate",
index=models.Index(
fields=["trip_update"], name="gtfs_stopti_trip_up_ffe901_idx"
),
),
migrations.AddIndex(
model_name="stoptimeupdate",
index=models.Index(
fields=["stop_time"], name="gtfs_stopti_stop_ti_4f2c63_idx"
),
),
migrations.AlterUniqueTogether(
name="stoptimeupdate",
unique_together={("trip_update", "stop_time")},
),
]

View File

@ -1,901 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class Country(models.TextChoices):
"""
Country list by ISO 3166-1 alpha-2 code.
Only countries that are member of the Council of Europe
are listed for now.
"""
ALBANIA = "AL", _("Albania")
ANDORRA = "AD", _("Andorra")
ARMENIA = "AM", _("Armenia")
AUSTRIA = "AT", _("Austria")
AZERBAIJAN = "AZ", _("Azerbaijan")
BELGIUM = "BE", _("Belgium")
BOSNIA_AND_HERZEGOVINA = "BA", _("Bosnia and Herzegovina")
BULGARIA = "BG", _("Bulgaria")
CROATIA = "HR", _("Croatia")
CYPRUS = "CY", _("Cyprus")
CZECH_REPUBLIC = "CZ", _("Czech Republic")
DENMARK = "DK", _("Denmark")
ESTONIA = "EE", _("Estonia")
FINLAND = "FI", _("Finland")
FRANCE = "FR", _("France")
GEORGIA = "GE", _("Georgia")
GERMANY = "DE", _("Germany")
GREECE = "GR", _("Greece")
HUNGARY = "HU", _("Hungary")
ICELAND = "IS", _("Iceland")
IRELAND = "IE", _("Ireland")
ITALY = "IT", _("Italy")
LATVIA = "LV", _("Latvia")
LIECHTENSTEIN = "LI", _("Liechtenstein")
LITHUANIA = "LT", _("Lithuania")
LUXEMBOURG = "LU", _("Luxembourg")
MALTA = "MT", _("Malta")
MOLDOVA = "MD", _("Moldova")
MONACO = "MC", _("Monaco")
MONTENEGRO = "ME", _("Montenegro")
NETHERLANDS = "NL", _("Netherlands")
NORTH_MACEDONIA = "MK", _("North Macedonia")
NORWAY = "NO", _("Norway")
POLAND = "PL", _("Poland")
PORTUGAL = "PT", _("Portugal")
ROMANIA = "RO", _("Romania")
SAN_MARINO = "SM", _("San Marino")
SERBIA = "RS", _("Serbia")
SLOVAKIA = "SK", _("Slovakia")
SLOVENIA = "SI", _("Slovenia")
SPAIN = "ES", _("Spain")
SWEDEN = "SE", _("Sweden")
SWITZERLAND = "CH", _("Switzerland")
TURKEY = "TR", _("Turkey")
UNITED_KINGDOM = "GB", _("United Kingdom")
UKRAINE = "UA", _("Ukraine")
class LocationType(models.IntegerChoices):
STOP_PLATFORM = 0, _("Stop/platform")
STATION = 1, _("Station")
ENTRANCE_EXIT = 2, _("Entrance/exit")
GENERIC_NODE = 3, _("Generic node")
BOARDING_AREA = 4, _("Boarding area")
class AccessInformation(models.IntegerChoices):
NO_INFORMATION = 0, _("No information")
POSSIBLE = 1, _("Possible")
NOT_POSSIBLE = 2, _("Not possible")
class PickupType(models.IntegerChoices):
REGULAR = 0, _("Regular")
NONE = 1, _("None")
MUST_PHONE_AGENCY = 2, _("Must phone agency")
MUST_COORDINATE_WITH_DRIVER = 3, _("Must coordinate with driver")
class RouteType(models.IntegerChoices):
TRAM = 0, _("Tram")
METRO = 1, _("Metro")
RAIL = 2, _("Rail")
BUS = 3, _("Bus")
FERRY = 4, _("Ferry")
CABLE_CAR = 5, _("Cable car")
GONDOLA = 6, _("Gondola")
FUNICULAR = 7, _("Funicular")
class Direction(models.IntegerChoices):
OUTBOUND = 0, _("Outbound")
INBOUND = 1, _("Inbound")
class TransferType(models.IntegerChoices):
RECOMMENDED = 0, _("Recommended")
TIMED = 1, _("Timed")
MINIMUM_TIME = 2, _("Minimum time")
NOT_POSSIBLE = 3, _("Not possible")
class ExceptionType(models.IntegerChoices):
ADDED = 1, _("Added")
REMOVED = 2, _("Removed")
class TripScheduleRelationship(models.IntegerChoices):
SCHEDULED = 0, _("Scheduled")
ADDED = 1, _("Added")
UNSCHEDULED = 2, _("Unscheduled")
CANCELED = 3, _("Canceled")
REPLACEMENT = 5, _("Replacement")
DUPLICATED = 6, _("Duplicated")
DELETED = 7, _("Deleted")
class StopScheduleRelationship(models.IntegerChoices):
SCHEDULED = 0, _("Scheduled")
SKIPPED = 1, _("Skipped")
NO_DATA = 2, _("No data")
UNSCHEDULED = 3, _("Unscheduled")
class GTFSFeed(models.Model):
code = models.CharField(
primary_key=True,
max_length=64,
verbose_name=_("code"),
help_text=_("Unique code of the feed.")
)
name = models.CharField(
max_length=255,
verbose_name=_("name"),
unique=True,
help_text=_("Full name that describes the feed."),
)
country = models.CharField(
max_length=2,
verbose_name=_("country"),
choices=Country,
)
feed_url = models.URLField(
verbose_name=_("feed URL"),
help_text=_("URL to download the GTFS feed. Must point to a ZIP archive. "
"See https://gtfs.org/schedule/ for more information."),
)
rt_feed_url = models.URLField(
verbose_name=_("realtime feed URL"),
blank=True,
default="",
help_text=_("URL to download the GTFS-Realtime feed, in the GTFS-RT format. "
"See https://gtfs.org/realtime/ for more information."),
)
last_modified = models.DateTimeField(
verbose_name=_("last modified date"),
null=True,
default=None,
)
etag = models.CharField(
max_length=255,
verbose_name=_("ETag"),
blank=True,
default="",
help_text=_("If applicable, corresponds to the tag of the last downloaded file. "
"If it is not modified, the file is the same."),
)
def __str__(self):
return f"{self.name} ({self.code})"
class Meta:
verbose_name = _("GTFS feed")
verbose_name_plural = _("GTFS feeds")
ordering = ('country', 'name',)
indexes = (models.Index(fields=['name']),)
class Agency(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Agency ID"),
)
name = models.CharField(
max_length=255,
verbose_name=_("Agency name"),
)
url = models.URLField(
verbose_name=_("Agency URL"),
)
timezone = models.CharField(
max_length=255,
verbose_name=_("Agency timezone"),
)
lang = models.CharField(
max_length=255,
verbose_name=_("Agency language"),
blank=True,
)
phone = models.CharField(
max_length=255,
verbose_name=_("Agency phone"),
blank=True,
)
email = models.EmailField(
verbose_name=_("Agency email"),
blank=True,
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Agency")
verbose_name_plural = _("Agencies")
ordering = ("name",)
indexes = (models.Index(fields=['name']), models.Index(fields=['gtfs_feed']),)
class Stop(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Stop ID"),
)
code = models.CharField(
max_length=255,
verbose_name=_("Stop code"),
blank=True,
)
name = models.CharField(
max_length=255,
verbose_name=_("Stop name"),
)
desc = models.CharField(
max_length=255,
verbose_name=_("Stop description"),
blank=True,
)
lon = models.FloatField(
verbose_name=_("Stop longitude"),
)
lat = models.FloatField(
verbose_name=_("Stop latitude"),
)
zone_id = models.CharField(
max_length=255,
verbose_name=_("Zone ID"),
blank=True,
)
url = models.URLField(
verbose_name=_("Stop URL"),
blank=True,
)
location_type = models.IntegerField(
verbose_name=_("Location type"),
blank=True,
choices=LocationType,
default=LocationType.STOP_PLATFORM,
)
parent_station = models.ForeignKey(
to="Stop",
on_delete=models.PROTECT,
verbose_name=_("Parent station"),
related_name="children",
blank=True,
null=True,
)
timezone = models.CharField(
max_length=255,
verbose_name=_("Stop timezone"),
blank=True,
)
level_id = models.CharField(
max_length=255,
verbose_name=_("Level ID"),
blank=True,
)
wheelchair_boarding = models.IntegerField(
verbose_name=_("Wheelchair boarding"),
blank=True,
choices=AccessInformation,
default=AccessInformation.NO_INFORMATION,
)
platform_code = models.CharField(
max_length=255,
verbose_name=_("Platform code"),
blank=True,
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
@property
def stop_type(self):
train_type = self.id.split('StopPoint:OCE')[-1].split('StopArea:OCE')[-1].split('-')[0]
return train_type
def __str__(self):
return f"{self.name} ({self.id})"
class Meta:
verbose_name = _("Stop")
verbose_name_plural = _("Stops")
ordering = ("id",)
indexes = (models.Index(fields=['name']),
models.Index(fields=['code']),
models.Index(fields=['gtfs_feed']),)
class Route(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("ID"),
)
agency = models.ForeignKey(
to="Agency",
on_delete=models.CASCADE,
verbose_name=_("Agency"),
related_name="routes",
null=True,
blank=True,
default=None,
)
short_name = models.CharField(
max_length=255,
verbose_name=_("Route short name"),
)
long_name = models.CharField(
max_length=255,
verbose_name=_("Route long name"),
blank=True,
)
desc = models.CharField(
max_length=255,
verbose_name=_("Route description"),
blank=True,
)
type = models.IntegerField(
verbose_name=_("Route type"),
choices=RouteType,
)
url = models.URLField(
verbose_name=_("Route URL"),
blank=True,
)
color = models.CharField(
max_length=255,
verbose_name=_("Route color"),
blank=True,
)
text_color = models.CharField(
max_length=255,
verbose_name=_("Route text color"),
blank=True,
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
def __str__(self):
return self.long_name or self.short_name
class Meta:
verbose_name = _("Route")
verbose_name_plural = _("Routes")
ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class Trip(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Trip ID"),
)
route = models.ForeignKey(
to="Route",
on_delete=models.CASCADE,
verbose_name=_("Route"),
related_name="trips",
)
service = models.ForeignKey(
to="Calendar",
on_delete=models.CASCADE,
verbose_name=_("Service"),
related_name="trips",
)
headsign = models.CharField(
max_length=255,
verbose_name=_("Trip headsign"),
blank=True,
)
short_name = models.CharField(
max_length=255,
verbose_name=_("Trip short name"),
blank=True,
)
direction_id = models.IntegerField(
verbose_name=_("Direction"),
choices=Direction,
null=True,
)
block_id = models.CharField(
max_length=255,
verbose_name=_("Block ID"),
blank=True,
)
shape_id = models.CharField(
max_length=255,
verbose_name=_("Shape ID"),
blank=True,
)
wheelchair_accessible = models.IntegerField(
verbose_name=_("Wheelchair accessible"),
choices=AccessInformation,
default=AccessInformation.NO_INFORMATION,
null=True,
)
bikes_allowed = models.IntegerField(
verbose_name=_("Bikes allowed"),
choices=AccessInformation,
default=AccessInformation.NO_INFORMATION,
null=True,
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
@property
def origin(self) -> Stop | None:
return self.stop_times.order_by('stop_sequence').first().stop if self.stop_times.exists() else None
@property
def destination(self) -> Stop | None:
return self.stop_times.order_by('-stop_sequence').first().stop if self.stop_times.exists() else None
@property
def departure_time(self):
if not self.stop_times.exists():
return _("Unknown")
dep_time = self.stop_times.order_by('stop_sequence').first().departure_time
hours = int(dep_time.total_seconds() // 3600)
minutes = int((dep_time.total_seconds() % 3600) // 60)
return f"{hours:02}:{minutes:02}"
@property
def arrival_time(self):
if not self.stop_times.exists():
return _("Unknown")
arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time
hours = int(arr_time.total_seconds() // 3600)
minutes = int((arr_time.total_seconds() % 3600) // 60)
return f"{hours:02}:{minutes:02}"
@property
def train_type(self):
if self.gtfs_feed.code == "FR-IDF-TN":
return self.route.short_name
else:
return self.origin.stop_type
@property
def train_number(self):
if self.gtfs_feed.code == "FR-IDF-TN":
return self.short_name
else:
return self.headsign
@property
def color(self):
if self.route.color:
return self.route.color
elif self.train_type == "OUIGO":
return "E60075"
return "FFFFFF"
@property
def text_color(self):
if self.route.text_color:
return self.route.text_color
elif self.train_type == "OUIGO":
return "FFFFFF"
elif self.train_type == "TGV INOUI":
return "9B2743"
elif self.train_type == "INTER-CITÉS" or self.train_type == "INTER-CITÉS de nuit":
return "404042"
return "000000"
@property
def origin_destination(self):
origin = self.origin
origin = origin.name if origin else _("Unknown")
destination = self.destination
destination = destination.name if destination else _("Unknown")
return f"{origin} {self.departure_time}{destination} {self.arrival_time}"
origin_destination.fget.short_description = _("Origin → Destination")
def __str__(self):
return self.origin_destination
class Meta:
verbose_name = _("Trip")
verbose_name_plural = _("Trips")
indexes = (models.Index(fields=['route']), models.Index(fields=['gtfs_feed']),)
class StopTime(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("ID"),
)
trip = models.ForeignKey(
to="Trip",
on_delete=models.CASCADE,
verbose_name=_("Trip"),
related_name="stop_times",
)
arrival_time = models.DurationField(
verbose_name=_("Arrival time"),
)
departure_time = models.DurationField(
verbose_name=_("Departure time"),
)
stop = models.ForeignKey(
to="Stop",
on_delete=models.CASCADE,
verbose_name=_("Stop ID"),
related_name="stop_times",
)
stop_sequence = models.IntegerField(
verbose_name=_("Stop sequence"),
)
stop_headsign = models.CharField(
max_length=255,
verbose_name=_("Stop headsign"),
blank=True,
)
pickup_type = models.IntegerField(
verbose_name=_("Pickup type"),
choices=PickupType,
default=PickupType.REGULAR,
null=True,
)
drop_off_type = models.IntegerField(
verbose_name=_("Drop off type"),
choices=PickupType,
default=PickupType.REGULAR,
null=True,
)
timepoint = models.BooleanField(
verbose_name=_("Timepoint"),
default=True,
null=True,
)
@property
def pretty_arrival_time(self):
seconds = self.arrival_time.total_seconds()
hours = int(seconds // 3600) % 24
minutes = int((seconds % 3600) // 60)
return f"{hours:02}:{minutes:02}"
@property
def pretty_departure_time(self):
seconds = self.departure_time.total_seconds()
hours = int(seconds // 3600) % 24
minutes = int((seconds % 3600) // 60)
return f"{hours:02}:{minutes:02}"
def __str__(self):
return f"{self.stop.name} - {self.trip_id}"
class Meta:
verbose_name = _("Stop time")
verbose_name_plural = _("Stop times")
indexes = (models.Index(fields=['stop']), models.Index(fields=['trip']),)
class Calendar(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Service ID"),
)
monday = models.BooleanField(
verbose_name=_("Monday"),
)
tuesday = models.BooleanField(
verbose_name=_("Tuesday"),
)
wednesday = models.BooleanField(
verbose_name=_("Wednesday"),
)
thursday = models.BooleanField(
verbose_name=_("Thursday"),
)
friday = models.BooleanField(
verbose_name=_("Friday"),
)
saturday = models.BooleanField(
verbose_name=_("Saturday"),
)
sunday = models.BooleanField(
verbose_name=_("Sunday"),
)
start_date = models.DateField(
verbose_name=_("Start date"),
)
end_date = models.DateField(
verbose_name=_("End date"),
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
def __str__(self):
return self.id
class Meta:
verbose_name = _("Calendar")
verbose_name_plural = _("Calendars")
ordering = ("id",)
indexes = (models.Index(fields=['gtfs_feed']),)
class CalendarDate(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("ID"),
)
service = models.ForeignKey(
to="Calendar",
on_delete=models.CASCADE,
verbose_name=_("Service"),
related_name="dates",
)
date = models.DateField(
verbose_name=_("Date"),
)
exception_type = models.IntegerField(
verbose_name=_("Exception type"),
choices=ExceptionType,
)
def __str__(self):
return f"{self.service.id} - {self.date} - {self.exception_type}"
class Meta:
verbose_name = _("Calendar date")
verbose_name_plural = _("Calendar dates")
ordering = ("id",)
indexes = (models.Index(fields=['service']), models.Index(fields=['date']),)
class Transfer(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("ID"),
)
from_stop = models.ForeignKey(
to="Stop",
on_delete=models.CASCADE,
verbose_name=_("From stop"),
related_name="transfers_from",
)
to_stop = models.ForeignKey(
to="Stop",
on_delete=models.CASCADE,
verbose_name=_("To stop"),
related_name="transfers_to",
)
transfer_type = models.IntegerField(
verbose_name=_("Transfer type"),
choices=TransferType,
default=TransferType.RECOMMENDED,
)
min_transfer_time = models.IntegerField(
verbose_name=_("Minimum transfer time"),
blank=True,
)
class Meta:
verbose_name = _("Transfer")
verbose_name_plural = _("Transfers")
ordering = ("id",)
class FeedInfo(models.Model):
publisher_name = models.CharField(
max_length=255,
verbose_name=_("Feed publisher name"),
)
publisher_url = models.URLField(
verbose_name=_("Feed publisher URL"),
)
lang = models.CharField(
max_length=255,
verbose_name=_("Feed language"),
)
start_date = models.DateField(
verbose_name=_("Feed start date"),
)
end_date = models.DateField(
verbose_name=_("Feed end date"),
)
version = models.CharField(
max_length=255,
verbose_name=_("Feed version"),
)
gtfs_feed = models.ForeignKey(
GTFSFeed,
on_delete=models.CASCADE,
verbose_name=_("GTFS feed"),
)
class Meta:
verbose_name = _("Feed info")
verbose_name_plural = _("Feed infos")
ordering = ("publisher_name",)
indexes = (models.Index(fields=['gtfs_feed']),)
class TripUpdate(models.Model):
trip = models.OneToOneField(
to="Trip",
on_delete=models.CASCADE,
verbose_name=_("Trip"),
related_name="update",
primary_key=True,
)
start_date = models.DateField(
verbose_name=_("Start date"),
)
start_time = models.TimeField(
verbose_name=_("Start time"),
)
schedule_relationship = models.IntegerField(
verbose_name=_("Schedule relationship"),
choices=TripScheduleRelationship,
default=TripScheduleRelationship.SCHEDULED,
)
def __str__(self):
return str(self.trip)
class Meta:
verbose_name = _("Trip update")
verbose_name_plural = _("Trip updates")
ordering = ("start_date", "trip",)
unique_together = ("trip", "start_date", "start_time",)
indexes = (models.Index(fields=['trip']),)
class StopTimeUpdate(models.Model):
trip_update = models.ForeignKey(
to="TripUpdate",
on_delete=models.CASCADE,
verbose_name=_("Trip update"),
related_name="stop_time_updates",
)
stop_time = models.OneToOneField(
to="StopTime",
on_delete=models.CASCADE,
verbose_name=_("Stop time"),
related_name="update",
primary_key=True,
)
arrival_delay = models.DurationField(
verbose_name=_("Arrival delay"),
)
arrival_time = models.DateTimeField(
verbose_name=_("Arrival time"),
)
departure_delay = models.DurationField(
verbose_name=_("Departure delay"),
)
departure_time = models.DateTimeField(
verbose_name=_("Departure time"),
)
schedule_relationship = models.IntegerField(
verbose_name=_("Schedule relationship"),
choices=StopScheduleRelationship,
default=StopScheduleRelationship.SCHEDULED,
)
def __str__(self):
return str(self.trip_update)
class Meta:
verbose_name = _("Stop time update")
verbose_name_plural = _("Stop time updates")
ordering = ("trip_update", "stop_time",)
unique_together = ("trip_update", "stop_time",)
indexes = (models.Index(fields=['trip_update']), models.Index(fields=['stop_time']),)

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,159 +0,0 @@
"""
Django settings for trainvel project.
Generated by 'django-admin startproject' using Django 5.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
from corsheaders.defaults import default_headers
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "CHANGE ME"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"corsheaders",
"django_filters",
"rest_framework",
"trainvel.api",
"trainvel.gtfs",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
]
CORS_ALLOW_HEADERS = (
*default_headers,
"If-Modified-Since",
'Cache-Control',
)
ROOT_URLCONF = "trainvel.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "trainvel.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "api-static/"
STATIC_ROOT = BASE_DIR / "static_files"
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20,
}
OPENTRANSPORTDATA_SWISS_TOKEN = "CHANGE ME"
try:
from .settings_local import *
except ImportError:
pass

View File

@ -1,22 +0,0 @@
SECRET_KEY = "CHANGE ME"
DEBUG = False
ALLOWED_HOSTS = ['sncf.emy.lu']
CORS_ALLOWED_ORIGINS = [
"https://sncf.emy.lu",
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "trainvel",
"USER": "trainvel",
"PASSWORD": "CHANGE ME",
"HOST": "localhost",
"PORT": "5432",
}
}
OPENTRANSPORTDATA_SWISS_TOKEN = "CHANGE ME"

View File

@ -1,44 +0,0 @@
"""
URL configuration for trainvel project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from trainvel.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \
CalendarViewSet, CalendarDateViewSet, TransferViewSet, FeedInfoViewSet, NextDeparturesViewSet, NextArrivalsViewSet, \
TripUpdateViewSet, StopTimeUpdateViewSet
router = routers.DefaultRouter()
router.register("gtfs/agency", AgencyViewSet)
router.register("gtfs/stop", StopViewSet)
router.register("gtfs/route", RouteViewSet)
router.register("gtfs/trip", TripViewSet)
router.register("gtfs/stop_time", StopTimeViewSet)
router.register("gtfs/calendar", CalendarViewSet)
router.register("gtfs/calendar_date", CalendarDateViewSet)
router.register("gtfs/transfer", TransferViewSet)
router.register("gtfs/feed_info", FeedInfoViewSet)
router.register("gtfs-rt/trip_update", TripUpdateViewSet)
router.register("gtfs-rt/stop_time_update", StopTimeUpdateViewSet)
router.register("station/next_departures", NextDeparturesViewSet, basename="next_departures")
router.register("station/next_arrivals", NextArrivalsViewSet, basename="next_arrivals")
urlpatterns = [
path("admin/", admin.site.urls, name="admin"),
path("api/", include(router.urls)),
path("api-auth/", include('rest_framework.urls', namespace='rest_framework')),
]

View File

@ -1,16 +0,0 @@
"""
WSGI config for trainvel project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "trainvel.settings")
application = get_wsgi_application()