Compare commits

...

60 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
6884084f2a sncf -> trainvel 2024-05-09 22:39:15 +02:00
b4f61308ab Add tqdm in GTFS import, add authorization code for CH-ALL GTF-RT import 2024-05-09 22:37:38 +02:00
12598b88cc Rename project to Trainvel 2024-05-09 19:43:59 +02:00
11949228ee Restructurate GTFS feeds into dedicated models 2024-05-09 19:28:19 +02:00
820fc0cc19 Add debug mode for GTFS scripts 2024-05-08 10:38:28 +02:00
2277d2fe64 Better management of GTFS realtime 2024-05-08 10:30:25 +02:00
9f3c031851 Small fix if there are multiple identical stops in a same trip 2024-02-16 18:05:20 +01:00
d679aadf68 Change stop separator 2024-02-10 22:43:08 +01:00
da82ef7950 Keep arrival trains during 5 minutes 2024-02-10 22:22:17 +01:00
f50391970f Better management for delays displays 2024-02-10 22:18:04 +01:00
dd34b025ca Update site description 2024-02-10 20:58:39 +01:00
0e87ecd8cf Reduce visual transitions when updating delays 2024-02-10 20:54:12 +01:00
7486fb2b4d Add Lyria logo 2024-02-10 20:50:18 +01:00
0d5a62ae7f Fix calendars import 2024-02-10 20:42:55 +01:00
2803cfe062 Fix error message 2024-02-10 20:35:55 +01:00
347f1dfa60 Remove debug code 2024-02-10 20:35:18 +01:00
72e00ae781 Take delay into account in the next departures/arrivals tables 2024-02-10 20:35:00 +01:00
28faf5ad10 Identify stoptimes with departure time to ensure beeing unique 2024-02-10 20:16:47 +01:00
e4bcf9bd2e Update translations 2024-02-10 19:58:31 +01:00
8aff5fbf94 Better calendar import 2024-02-10 19:57:03 +01:00
22de576f53 Better calendar import 2024-02-10 19:54:27 +01:00
fd4157acbd Better calendar import 2024-02-10 19:54:10 +01:00
11cf6bbf55 Better canceled trains display 2024-02-10 19:47:34 +01:00
a67ad3044f Remove debug code 2024-02-10 18:31:02 +01:00
ee5eb13a01 Drop old services before updating GTFS data 2024-02-10 18:30:36 +01:00
6ef1df2757 Fix parent stations for Transilien 2024-02-10 18:28:48 +01:00
16520c3664 Prepare code for Eurostar/Trenitalia France/RENFE/ÖBB data input 2024-02-10 17:33:36 +01:00
77c3ef9e74 Truncate trip id 2024-02-09 23:15:14 +01:00
8d2ffe3014 Manage trip additions and cancels 2024-02-06 08:01:56 +01:00
9ed97df4b5 Display delays 2024-02-04 23:58:27 +01:00
68 changed files with 7231 additions and 10105 deletions

1
.env Normal file
View File

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

64
.gitignore vendored
View File

@ -1,52 +1,26 @@
# Byte-compiled / optimized / DLL files # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
dist
build
__pycache__
*.py[cod]
*$py.class
*.swp
*.egg-info
_build
.tox
.coverage
coverage
# Translations
*.mo
*.pot
# Jupyter Notebook
.ipynb_checkpoints
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea .idea
# VSCode project settings
.vscode .vscode
# Local data # dependencies
secrets.py /node_modules
settings_local.py /.pnp
*.log .pnp.js
media/
output/
/static/
/static_files/
# Virtualenv # testing
.env/ /coverage
env/
.venv/
venv/
db.sqlite3
db.sqlite3-journal
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", "sncf.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"
]
}
}

5
public/bus.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="white" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M7.384 5.25c.008-.04.068-.308.22-.672h19.792c.152.362.213.626.218.663 1.047 4.827 1.612 9.395 1.696 13.604H5.69c.079-4.201.65-8.774 1.694-13.596zm19.178 22.7c-1.398 0-2.532-1.082-2.532-2.418s1.134-2.418 2.532-2.418c1.4 0 2.532 1.082 2.532 2.418s-1.133 2.418-2.532 2.418zm-18.313 0c-1.4 0-2.53-1.082-2.53-2.418s1.13-2.418 2.53-2.418c1.398 0 2.534 1.082 2.534 2.418S9.647 27.95 8.25 27.95zM4.223 10.5c-1.636 11.16-.15 17.16-.076 17.452.186 1.014.67 1.607 1.45 1.82.004.804.006 3.44.006 3.44 0 .987.842 1.788 1.878 1.788h1.467c1.033 0 1.877-.8 1.877-1.789l-.008-3.336H23.91v3.333c0 .989.842 1.792 1.872 1.792l1.746-.005c.97 0 1.601-.7 1.601-1.787v-3.45c.923-.243 1.61-.935 1.741-1.881l-.013.07c.048-.186.692-2.77.692-7.608 0-2.624-.193-5.909-.766-9.836.29.216.665.322 1.055.263.75-.112 1.265-.787 1.146-1.503l-.407-2.456c-.119-.718-.823-1.207-1.573-1.094a1.378 1.378 0 0 0-.946.6c-.11-.56-.224-1.128-.35-1.706V4.6C29.39 3.013 27.735.019 23.736.005H20.68L11.272 0c-4.01.013-5.66 3.022-5.974 4.608-.129.58-.242 1.147-.352 1.707A1.373 1.373 0 0 0 4 5.713c-.75-.113-1.457.376-1.576 1.094l-.407 2.456c-.119.716.397 1.391 1.15 1.503.388.059.767-.05 1.056-.266z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

15
public/eurostar.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.0" width="1140.000000pt" height="260.000000pt" viewBox="0 0 1140.000000 260.000000" preserveAspectRatio="xMidYMid meet" id="svg22" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs26"/>
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none" id="g20" style="fill:#116bfe;fill-opacity:1">
<path d="M1085 2579 c-194 -28 -429 -132 -592 -261 -499 -395 -632 -1129 -306 -1686 186 -318 511 -543 877 -607 128 -22 369 -20 493 4 375 73 671 296 852 642 66 125 124 301 133 399 2 30 0 35 -17 32 -32 -6 -41 -21 -78 -134 -44 -132 -88 -220 -162 -323 -181 -254 -444 -425 -742 -481 -135 -25 -385 -15 -513 21 -382 106 -686 398 -803 770 -40 128 -50 216 -44 387 7 221 56 383 169 566 63 101 232 279 327 342 396 264 889 269 1284 11 146 -96 305 -270 387 -425 17 -32 29 -61 27 -62 -3 -4 -537 -199 -830 -305 l-108 -39 -9 23 c-5 12 -23 180 -40 372 -17 193 -33 358 -36 368 -3 9 -14 17 -24 17 -21 0 -22 -5 -45 -260 -38 -407 -48 -505 -54 -512 -4 -4 -159 35 -346 87 -298 83 -342 93 -355 80 -9 -9 -11 -18 -5 -24 6 -5 133 -71 283 -146 149 -75 272 -141 272 -146 0 -5 -124 -71 -275 -146 -194 -97 -275 -143 -275 -154 0 -9 6 -19 13 -22 6 -2 156 35 332 84 176 49 330 89 341 89 19 0 22 -7 28 -77 7 -85 28 -302 51 -536 8 -81 15 -150 15 -153 0 -3 10 -4 22 -2 22 3 23 10 60 383 20 209 37 381 37 383 4 12 33 5 528 -134 89 -25 167 -43 173 -39 29 18 -14 47 -265 175 -148 76 -270 144 -270 150 0 6 192 105 427 219 235 114 434 213 441 220 13 10 13 18 2 54 -25 85 -77 193 -130 272 -179 270 -466 457 -795 519 -90 18 -351 20 -455 5z" id="path2" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M9060 2026 c-38 -62 -162 -169 -242 -208 -63 -31 -162 -61 -275 -84 -13 -2 -23 -11 -23 -19 0 -12 16 -15 79 -15 44 0 82 -4 85 -9 3 -5 -3 -35 -13 -67 -10 -33 -30 -102 -46 -154 -15 -52 -31 -108 -36 -125 -96 -312 -103 -422 -31 -498 50 -54 98 -71 193 -72 79 0 91 3 160 37 100 50 249 195 249 242 0 14 -10 11 -49 -18 -108 -80 -197 -98 -245 -50 -44 45 -40 68 74 449 23 77 48 164 56 194 8 29 17 58 20 62 3 5 70 9 148 9 141 0 144 0 150 23 15 58 18 57 -134 57 -77 0 -140 3 -140 8 0 4 18 63 40 132 22 69 40 128 40 133 0 4 -9 7 -19 7 -11 0 -28 -15 -41 -34z" id="path4" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M3654 1800 c-122 -18 -274 -91 -362 -174 -91 -85 -163 -224 -182 -349 -37 -249 89 -444 317 -492 232 -49 455 46 604 258 27 39 49 74 49 79 0 18 -24 6 -73 -37 -29 -25 -85 -62 -125 -82 -64 -32 -85 -37 -163 -41 -80 -4 -94 -2 -141 22 -91 45 -131 126 -132 261 0 193 114 424 238 485 23 11 54 20 68 20 30 0 73 -32 92 -68 23 -44 25 -226 4 -255 -8 -10 -77 -35 -173 -63 -258 -74 -244 -83 131 -84 160 0 292 4 307 9 21 8 28 20 37 68 16 75 7 189 -18 246 -68 153 -255 230 -478 197z" id="path6" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M6087 1800 c-108 -28 -197 -102 -336 -283 -45 -59 -87 -105 -92 -101 -11 7 -19 -7 113 191 59 90 108 165 108 168 0 3 -76 5 -169 5 -153 0 -170 -2 -175 -17 -12 -43 -236 -950 -236 -956 0 -4 70 -7 155 -7 l154 0 10 38 c6 20 35 136 65 258 61 245 80 300 131 379 63 99 164 147 232 111 36 -18 43 -33 43 -89 0 -50 -37 -133 -91 -204 -39 -53 -38 -74 3 -52 121 64 288 211 329 290 34 66 33 154 -3 201 -15 19 -42 42 -61 51 -43 21 -131 29 -180 17z" id="path8" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M6900 1803 c-77 -9 -165 -35 -238 -70 -107 -52 -225 -169 -275 -273 -102 -210 -83 -434 47 -564 186 -186 577 -157 798 59 104 101 169 239 183 390 8 85 -13 198 -49 259 -60 103 -173 175 -303 195 -71 11 -96 11 -163 4z m143 -77 c57 -35 77 -133 58 -281 -17 -127 -75 -343 -114 -428 -60 -130 -153 -200 -241 -183 -113 23 -141 143 -91 390 65 322 139 469 256 513 36 13 101 8 132 -11z" id="path10" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M7922 1800 c-224 -31 -351 -143 -354 -312 -1 -46 5 -74 20 -104 40 -79 99 -118 381 -252 30 -14 71 -42 93 -62 33 -33 38 -42 38 -83 0 -39 -6 -53 -35 -85 -41 -46 -90 -63 -184 -63 -82 0 -127 17 -165 62 -25 29 -28 39 -24 89 3 39 12 67 31 95 15 22 27 43 27 47 0 5 -65 8 -145 8 l-144 0 -12 -42 c-35 -133 53 -254 221 -305 78 -24 301 -23 390 1 194 53 310 172 310 318 0 134 -77 200 -390 335 -133 58 -173 154 -99 238 35 40 81 57 149 57 119 -1 196 -92 169 -197 -6 -22 -13 -50 -16 -63 l-5 -23 133 3 134 3 3 42 c8 113 -108 240 -255 278 -71 19 -195 25 -271 15z" id="path12" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M9775 1799 c-224 -29 -363 -132 -381 -282 l-7 -57 162 0 161 0 0 73 c1 137 52 199 172 205 80 5 118 -8 154 -52 25 -29 26 -35 21 -111 -4 -62 -27 -163 -42 -182 -1 -2 -95 -17 -210 -33 -246 -36 -281 -44 -360 -81 -152 -71 -225 -192 -195 -324 21 -94 84 -153 187 -176 71 -16 177 -2 253 34 74 35 185 130 239 206 25 33 50 61 58 61 7 0 13 -3 13 -6 0 -3 -34 -62 -75 -131 -41 -68 -75 -128 -75 -134 0 -5 67 -9 163 -9 l162 0 58 228 c89 345 99 391 99 467 3 227 -217 347 -557 304z m211 -511 c-10 -51 -82 -194 -122 -243 -111 -137 -328 -100 -300 50 10 50 67 109 140 145 62 30 221 78 265 79 21 1 23 -2 17 -31z" id="path14" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M11092 1790 c-29 -10 -68 -28 -87 -40 -44 -27 -154 -146 -230 -248 -33 -45 -66 -81 -74 -82 -20 0 -12 15 99 180 56 83 104 158 107 166 4 12 -21 14 -164 14 l-168 0 -119 -477 c-66 -263 -121 -484 -124 -491 -3 -10 29 -12 154 -10 l159 3 66 265 c74 294 105 375 174 449 76 81 173 103 221 50 41 -46 10 -168 -69 -272 -66 -86 -46 -89 71 -11 113 76 222 180 255 246 91 180 -67 332 -271 258z" id="path16" style="fill:#FCF5D8;fill-opacity:1"/>
<path d="M4288 1428 c-103 -418 -102 -414 -88 -484 27 -131 154 -194 326 -165 132 23 257 108 364 246 40 52 60 67 60 45 0 -5 -34 -65 -75 -132 -41 -67 -75 -126 -75 -130 0 -4 74 -8 163 -8 l164 0 118 478 c65 262 120 483 123 490 3 10 -29 12 -154 10 l-158 -3 -8 -30 c-5 -16 -35 -142 -68 -279 -67 -276 -95 -347 -167 -424 -58 -62 -120 -91 -190 -92 -41 0 -58 5 -80 24 -25 21 -28 31 -26 78 0 29 37 198 82 375 45 178 81 329 81 338 0 13 -23 15 -153 15 l-152 0 -87 -352z" id="path18" style="fill:#FCF5D8;fill-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

7
public/eurostar_mini.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.0" width="260.000000pt" height="260.000000pt" viewBox="0 0 260.000000 260.000000" preserveAspectRatio="xMidYMid meet" id="svg22" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs26"/>
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none" id="g20" style="fill:#116bfe;fill-opacity:1">
<path d="M1085 2579 c-194 -28 -429 -132 -592 -261 -499 -395 -632 -1129 -306 -1686 186 -318 511 -543 877 -607 128 -22 369 -20 493 4 375 73 671 296 852 642 66 125 124 301 133 399 2 30 0 35 -17 32 -32 -6 -41 -21 -78 -134 -44 -132 -88 -220 -162 -323 -181 -254 -444 -425 -742 -481 -135 -25 -385 -15 -513 21 -382 106 -686 398 -803 770 -40 128 -50 216 -44 387 7 221 56 383 169 566 63 101 232 279 327 342 396 264 889 269 1284 11 146 -96 305 -270 387 -425 17 -32 29 -61 27 -62 -3 -4 -537 -199 -830 -305 l-108 -39 -9 23 c-5 12 -23 180 -40 372 -17 193 -33 358 -36 368 -3 9 -14 17 -24 17 -21 0 -22 -5 -45 -260 -38 -407 -48 -505 -54 -512 -4 -4 -159 35 -346 87 -298 83 -342 93 -355 80 -9 -9 -11 -18 -5 -24 6 -5 133 -71 283 -146 149 -75 272 -141 272 -146 0 -5 -124 -71 -275 -146 -194 -97 -275 -143 -275 -154 0 -9 6 -19 13 -22 6 -2 156 35 332 84 176 49 330 89 341 89 19 0 22 -7 28 -77 7 -85 28 -302 51 -536 8 -81 15 -150 15 -153 0 -3 10 -4 22 -2 22 3 23 10 60 383 20 209 37 381 37 383 4 12 33 5 528 -134 89 -25 167 -43 173 -39 29 18 -14 47 -265 175 -148 76 -270 144 -270 150 0 6 192 105 427 219 235 114 434 213 441 220 13 10 13 18 2 54 -25 85 -77 193 -130 272 -179 270 -466 457 -795 519 -90 18 -351 20 -455 5z" id="path2" style="fill:#FCF5D8;fill-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

18
public/frecciarossa.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0.00 0.00 2244.00 378.00">
<path fill="#e41c39" d=" M 1374.31 0.00 L 1778.81 0.00 C 1829.24 0.91 1881.04 4.11 1929.49 15.53 C 1993.49 30.62 2053.11 64.39 2103.31 105.20 C 2142.17 136.79 2173.33 159.00 2208.45 185.82 C 2221.50 195.79 2239.69 209.40 2244.00 223.57 L 2244.00 228.90 Q 2240.70 236.11 2231.46 238.97 C 2213.75 244.45 2196.00 245.52 2177.99 245.48 C 2129.56 245.39 2086.47 225.72 2046.36 200.37 C 2010.11 177.46 1969.60 147.19 1940.81 126.94 C 1892.18 92.73 1840.25 62.02 1783.77 42.71 Q 1726.85 23.26 1668.94 19.58 Q 1658.41 18.91 1619.94 17.54 Q 1495.78 13.11 1372.18 0.63 Q 1371.95 0.60 1372.03 0.38 Q 1372.09 0.22 1372.24 0.25 Q 1373.50 0.45 1374.31 0.00 Z"/>
<path fill="#212733" d=" M 1200.44 115.44 C 1222.72 90.95 1256.33 84.12 1288.49 88.49 C 1313.77 91.92 1325.14 107.52 1318.08 132.66 C 1302.64 187.61 1261.87 224.02 1201.73 216.38 C 1141.50 208.73 1179.24 138.73 1200.44 115.44 Z M 1268.98 116.03 C 1235.51 112.38 1216.56 140.72 1208.28 168.66 C 1204.61 181.05 1207.57 188.14 1221.25 188.99 C 1252.69 190.95 1269.52 165.69 1278.11 139.53 C 1281.29 129.86 1283.04 117.57 1268.98 116.03 Z"/>
<path fill="#212733" d=" M 178.75 138.50 L 251.22 138.50 Q 251.80 138.50 251.56 139.03 L 239.15 166.17 A 1.00 0.99 -77.3 0 1 238.25 166.75 L 165.02 166.75 Q 164.49 166.75 164.27 167.24 L 142.81 214.25 Q 142.58 214.75 142.03 214.75 L 107.23 214.75 Q 106.50 214.75 106.80 214.08 Q 121.35 182.00 136.15 149.92 C 148.48 123.20 161.98 101.63 192.25 93.75 Q 207.26 89.84 223.81 89.67 Q 248.69 89.41 273.69 89.52 A 0.33 0.33 0.0 0 1 273.99 89.98 L 261.40 117.57 A 0.71 0.70 -77.5 0 1 260.76 117.98 Q 238.06 118.04 215.53 118.01 Q 192.00 117.97 178.53 138.09 Q 178.26 138.50 178.75 138.50 Z"/>
<path fill="#212733" d=" M 297.68 89.93 A 0.72 0.70 -77.5 0 1 298.32 89.51 Q 331.91 89.46 365.75 89.54 C 391.20 89.60 409.12 98.13 397.90 127.43 Q 387.32 155.06 358.52 164.68 Q 357.93 164.87 358.03 165.49 L 365.76 214.07 A 0.59 0.59 0.0 0 1 365.18 214.75 L 328.77 214.75 A 0.94 0.94 0.0 0 1 327.84 213.95 L 321.28 169.47 A 0.56 0.55 -4.2 0 0 320.73 169.00 L 297.78 169.00 Q 297.38 169.00 297.22 169.36 L 276.70 214.27 Q 276.48 214.75 275.96 214.75 L 241.49 214.75 A 0.53 0.53 0.0 0 1 241.01 214.00 L 297.68 89.93 Z M 319.95 120.02 L 311.09 139.31 Q 310.81 139.93 311.49 139.94 Q 327.21 140.18 342.94 139.86 Q 358.53 139.54 362.91 125.15 Q 363.54 123.07 363.57 122.84 C 363.96 119.42 354.81 119.01 352.75 119.00 Q 336.67 118.99 320.59 119.01 Q 320.18 119.01 320.03 119.39 Q 319.97 119.53 320.00 119.70 Q 320.02 119.87 319.95 120.02 Z"/>
<path fill="#212733" d=" M 452.24 137.25 L 521.53 137.25 A 0.57 0.56 -77.8 0 1 522.04 138.05 L 509.48 165.52 Q 509.26 166.00 508.72 166.00 L 437.54 166.00 Q 437.14 166.00 436.99 166.37 C 432.13 178.79 432.96 186.25 448.54 186.24 Q 473.99 186.21 499.44 186.27 Q 500.00 186.27 499.76 186.78 L 487.23 214.20 A 0.89 0.88 -77.3 0 1 486.43 214.71 Q 462.14 214.84 438.01 214.71 C 399.97 214.50 387.18 191.81 401.76 157.22 C 418.80 116.77 444.48 90.34 490.81 89.77 Q 517.24 89.44 543.53 89.49 A 0.45 0.44 12.2 0 1 543.94 90.12 L 531.45 117.46 A 0.89 0.88 -77.3 0 1 530.65 117.97 Q 512.54 118.06 494.54 117.99 Q 480.61 117.94 475.08 119.40 Q 460.61 123.21 451.88 136.59 Q 451.46 137.25 452.24 137.25 Z"/>
<path fill="#212733" d=" M 538.00 169.63 C 550.34 125.86 582.70 90.53 630.97 89.66 Q 652.31 89.28 684.00 89.58 Q 684.55 89.59 684.32 90.08 L 671.85 117.30 Q 671.66 117.72 671.19 117.73 Q 652.37 117.80 633.63 117.75 Q 619.99 117.72 614.51 119.20 C 594.66 124.56 584.69 146.60 577.11 163.59 C 574.05 170.46 570.41 180.21 579.55 184.44 Q 583.34 186.19 587.06 186.21 Q 613.46 186.29 639.54 186.24 A 0.53 0.52 12.2 0 1 640.02 186.98 L 627.54 214.31 Q 627.35 214.72 626.90 214.73 Q 602.22 214.81 577.50 214.72 C 549.70 214.61 529.45 199.98 538.00 169.63 Z"/>
<path fill="#212733" d=" M 668.29 166.77 C 681.63 123.97 713.43 90.46 760.96 89.73 Q 787.18 89.33 813.51 89.54 A 0.44 0.44 0.0 0 1 813.90 90.16 L 801.48 117.24 A 0.87 0.87 0.0 0 1 800.69 117.75 Q 781.60 117.76 762.34 117.75 Q 749.65 117.74 744.29 119.25 C 723.27 125.16 712.81 148.71 705.43 167.11 C 701.14 177.80 704.15 186.14 716.82 186.19 Q 743.01 186.31 769.23 186.23 A 0.48 0.48 0.0 0 1 769.67 186.91 L 757.13 214.32 Q 756.93 214.76 756.45 214.76 Q 743.82 214.76 707.76 214.71 C 677.56 214.68 658.33 198.70 668.29 166.77 Z"/>
<path fill="#212733" d=" M 852.81 89.50 L 889.07 89.50 A 0.37 0.37 0.0 0 1 889.41 90.02 L 832.50 214.53 A 0.37 0.37 0.0 0 1 832.16 214.75 L 795.94 214.75 A 0.37 0.37 0.0 0 1 795.60 214.23 L 852.47 89.72 A 0.37 0.37 0.0 0 1 852.81 89.50 Z"/>
<path fill="#212733" d=" M 963.71 89.50 L 997.25 89.50 A 0.33 0.33 0.0 0 1 997.58 89.83 L 997.20 214.42 A 0.33 0.33 0.0 0 1 996.87 214.75 L 959.80 214.75 A 0.33 0.33 0.0 0 1 959.47 214.41 L 960.62 187.09 A 0.33 0.33 0.0 0 0 960.29 186.75 L 913.50 186.75 A 0.33 0.33 0.0 0 0 913.25 186.87 L 889.72 214.63 A 0.33 0.33 0.0 0 1 889.47 214.75 L 851.12 214.75 A 0.33 0.33 0.0 0 1 850.88 214.20 L 963.46 89.61 A 0.33 0.33 0.0 0 1 963.71 89.50 Z M 961.94 130.44 Q 950.71 143.69 939.23 156.88 Q 938.69 157.50 939.51 157.50 L 961.19 157.50 Q 961.88 157.50 961.91 156.81 L 963.07 130.23 A 0.47 0.47 0.0 0 0 962.39 129.79 Q 961.99 129.99 961.94 130.44 Z"/>
<path fill="#212733" d=" M 1093.80 169.00 L 1071.20 169.00 A 1.01 1.01 0.0 0 0 1070.28 169.59 L 1049.83 214.36 Q 1049.65 214.75 1049.22 214.75 L 1014.52 214.75 A 0.45 0.45 0.0 0 1 1014.11 214.11 L 1070.70 90.24 Q 1071.03 89.53 1071.81 89.53 Q 1105.76 89.44 1140.24 89.52 C 1156.64 89.55 1178.71 94.70 1174.28 116.77 C 1169.81 139.04 1154.02 157.88 1131.83 164.51 A 0.92 0.92 0.0 0 0 1131.19 165.53 L 1138.97 214.43 A 0.28 0.28 0.0 0 1 1138.69 214.75 L 1101.56 214.75 A 0.52 0.52 0.0 0 1 1101.05 214.31 L 1094.44 169.56 A 0.65 0.65 0.0 0 0 1093.80 169.00 Z M 1093.31 119.56 L 1084.28 139.28 Q 1083.96 139.96 1084.71 139.97 Q 1094.76 140.07 1104.82 140.02 C 1121.37 139.94 1132.75 140.21 1136.76 122.66 A 1.47 1.45 -70.4 0 0 1135.99 121.02 Q 1131.90 118.99 1126.75 118.99 Q 1110.25 118.97 1094.16 119.02 A 0.94 0.93 12.3 0 0 1093.31 119.56 Z"/>
<path fill="#212733" d=" M 1367.00 166.98 C 1351.35 167.00 1330.18 161.43 1334.95 140.26 C 1342.11 108.47 1367.59 89.66 1399.81 89.56 Q 1438.65 89.44 1477.51 89.52 A 0.49 0.49 0.0 0 1 1477.96 90.22 L 1465.09 118.32 Q 1464.89 118.75 1464.42 118.75 Q 1429.79 118.74 1395.28 118.80 Q 1379.87 118.82 1373.64 132.53 C 1372.11 135.90 1373.91 138.38 1377.44 138.41 C 1393.80 138.56 1410.76 137.63 1427.01 139.20 C 1460.30 142.42 1449.42 172.07 1437.36 189.38 C 1423.62 209.10 1402.88 214.68 1379.94 214.72 Q 1342.39 214.79 1305.22 214.74 Q 1304.38 214.73 1304.73 213.97 L 1317.48 186.02 A 0.91 0.89 12.2 0 1 1318.30 185.50 Q 1354.35 185.57 1390.73 185.29 Q 1405.11 185.17 1409.52 171.80 Q 1410.00 170.36 1409.62 168.97 A 0.99 0.98 -87.0 0 0 1409.03 168.31 Q 1405.99 167.14 1402.69 167.11 Q 1384.84 166.96 1367.00 166.98 Z"/>
<path fill="#212733" d=" M 1494.44 109.44 C 1509.40 93.39 1528.45 89.60 1549.55 89.54 Q 1585.44 89.43 1621.33 89.53 Q 1621.83 89.53 1621.62 89.98 L 1608.68 118.24 Q 1608.46 118.74 1607.91 118.74 Q 1574.07 118.78 1540.25 118.74 C 1529.39 118.73 1519.65 123.79 1516.48 134.77 Q 1516.34 135.24 1516.57 135.67 Q 1518.10 138.52 1520.82 138.50 Q 1550.37 138.32 1565.25 138.69 Q 1571.48 138.84 1579.28 140.88 C 1605.74 147.79 1586.91 182.69 1576.81 194.47 C 1563.36 210.18 1544.20 214.66 1524.39 214.72 Q 1486.47 214.82 1448.54 214.72 A 0.38 0.37 -77.8 0 1 1448.20 214.19 L 1461.03 186.04 Q 1461.28 185.50 1461.87 185.50 Q 1494.43 185.51 1526.91 185.46 C 1540.35 185.44 1550.55 184.27 1553.34 169.97 A 1.71 1.71 0.0 0 0 1552.07 167.99 Q 1549.10 167.25 1546.18 167.21 Q 1528.34 166.97 1510.50 166.99 C 1498.51 167.00 1482.19 164.13 1478.78 150.94 C 1475.39 137.82 1485.34 119.20 1494.44 109.44 Z"/>
<path fill="#212733" d=" M 1690.71 89.50 L 1724.17 89.50 A 0.39 0.39 0.0 0 1 1724.56 89.89 L 1724.18 214.36 A 0.39 0.39 0.0 0 1 1723.79 214.75 L 1686.83 214.75 A 0.39 0.39 0.0 0 1 1686.44 214.34 L 1687.59 187.16 A 0.39 0.39 0.0 0 0 1687.20 186.75 L 1640.52 186.75 A 0.39 0.39 0.0 0 0 1640.22 186.89 L 1616.70 214.61 A 0.39 0.39 0.0 0 1 1616.40 214.75 L 1578.23 214.75 A 0.39 0.39 0.0 0 1 1577.94 214.10 L 1690.42 89.63 A 0.39 0.39 0.0 0 1 1690.71 89.50 Z M 1688.94 130.44 Q 1677.62 143.65 1666.25 156.92 Q 1665.74 157.50 1666.51 157.50 L 1688.18 157.50 Q 1688.86 157.50 1688.89 156.82 L 1690.04 130.22 A 0.46 0.45 -12.7 0 0 1689.37 129.80 Q 1688.99 130.00 1688.94 130.44 Z"/>
<path fill="#e41c39" d=" M 2110.65 378.00 L 2070.19 378.00 Q 2052.67 376.66 2031.50 373.51 C 1721.34 327.28 1406.93 310.07 1093.56 311.19 Q 676.72 312.68 261.93 353.87 Q 164.86 363.51 68.14 376.18 Q 60.11 377.23 52.30 377.25 Q 27.74 377.30 2.22 376.90 Q -0.44 376.85 2.17 376.36 C 204.15 337.88 409.11 314.70 614.07 299.69 Q 949.49 275.13 1285.81 277.48 C 1435.94 278.53 1586.36 283.86 1736.07 295.29 Q 1852.62 304.18 1968.28 321.03 Q 2002.91 326.08 2037.74 329.32 C 2084.15 333.63 2131.41 335.52 2177.74 329.51 C 2196.45 327.08 2236.13 318.94 2243.35 298.05 Q 2243.49 297.64 2243.76 297.98 Q 2243.94 298.22 2243.81 298.53 C 2220.62 351.89 2165.09 374.20 2110.65 378.00 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

9
public/ice.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1.8cm" height="1cm" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 18 10" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Ebene_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path fill="#B4B4B4" d="M4.3579 4.9781c-0.4772,2.8024 0.6168,4.1979 3.1041,4.2241 0.8818,0.0092 1.8783,-0.2956 1.9975,-0.3663l0.229 -1.2432c-0.555,0.1967 -1.0499,0.3536 -1.7118,0.3412 -0.8359,-0.0157 -1.6762,-0.2365 -1.6981,-1.7361 -0.0315,-2.1452 0.7768,-3.9344 2.7285,-3.9707 0.6681,-0.0125 0.9846,0.1048 1.5886,0.3435l0.241 -1.3081c-0.4988,-0.2078 -0.8895,-0.3933 -2.0459,-0.4144 -2.4648,-0.0449 -4.0281,1.7523 -4.4329,4.13zm-3.6799 4.12l1.9109 -0.0011 1.4863 -8.0684 -1.9371 0 -1.4601 8.0695zm11.2907 -8.0695l-1.5412 8.0695 5.6172 -0.0006 0.2395 -1.3003 -3.7164 0 0.337 -2.0365 2.7499 0.0005 0.2488 -1.3508 -2.7181 -0.0018 0.4078 -1.9854 3.5066 -0.0002 0.2568 -1.3943 -5.3879 -0.0001z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr" data-bs-theme="dark"> <html lang="fr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Écrans en gare affichant les horaires des trains des gares."
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

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

17
public/lyria.svg Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="424.089px" height="153.688px" viewBox="0 0 424.089 153.688" enable-background="new 0 0 424.089 153.688" xml:space="preserve">
<rect fill="#E5162B" width="424.089" height="153.688"/>
<rect x="323.143" y="29.171" fill="#FFFFFF" width="14.464" height="14.467"/>
<path fill="#FFFFFF" d="M323.143,49.187c-1.373-0.006-15.067-1.635-20.262,8.879l0.006-7.587H279.74l-12.806,38.417l-13.145-38.417 h-15.531l14.562,38.557h-28.956V29.164h-16.076v73.066h49.842c0,0,0.505,1.194,0.613,1.566c0.944,3.174-0.875,8.292-6.044,8.292 c-2.933,0-5.86,0.011-8.702,0.011v12.427c2.639,0,9.778-0.026,12.817-0.026c6.381,0,12.116-4.628,15.368-13.271l17.387-46.92v37.915 h14.538V80.15c0-20.61,17.899-17.467,19.53-17.439v39.52h14.46V58.09h-14.456V49.187z"/>
<polygon fill="#FFFFFF" points="354.447,35.021 355.527,35.021 355.527,32.632 357.921,32.632 357.921,31.555 355.527,31.555 355.527,29.17 354.447,29.17 354.447,31.555 352.07,31.555 352.07,32.632 354.447,32.632 "/>
<polygon fill="#FFFFFF" points="354.447,43.637 355.527,43.637 355.527,41.25 357.921,41.25 357.921,40.175 355.527,40.175 355.527,37.795 354.447,37.795 354.447,40.175 352.07,40.175 352.07,41.25 354.447,41.25 "/>
<polygon fill="#FFFFFF" points="371.695,35.021 372.795,35.021 372.795,32.632 375.168,32.632 375.168,31.555 372.795,31.555 372.795,29.17 371.695,29.17 371.695,31.555 369.32,31.555 369.32,32.632 371.695,32.632 "/>
<polygon fill="#FFFFFF" points="363.088,35.021 364.153,35.021 364.153,32.632 366.532,32.632 366.532,31.555 364.153,31.555 364.153,29.17 363.088,29.17 363.088,31.555 360.69,31.555 360.69,32.632 363.088,32.632 "/>
<polygon fill="#FFFFFF" points="363.088,43.637 364.153,43.637 364.153,41.25 366.532,41.25 366.532,40.175 364.153,40.175 364.153,37.795 363.088,37.795 363.088,40.175 360.69,40.175 360.69,41.25 363.088,41.25 "/>
<polygon fill="#FFFFFF" points="46.873,53.057 28.896,53.057 28.896,43.614 75.29,43.614 75.29,53.057 57.312,53.057 57.312,102.268 46.873,102.268 "/>
<path fill="#FFFFFF" d="M133.688,97.96c-7.125,3.81-15.242,5.798-24.275,5.798c-18.059,0-30.983-12.262-30.983-30.567 c0-18.808,12.924-31.068,30.983-31.068c8.949,0,16.985,1.905,23.115,7.373l-7.706,7.788c-3.725-3.647-9.442-5.717-15.325-5.717 c-12.097,0-20.131,9.278-20.131,20.959c0,12.512,8.034,21.79,20.131,21.79c5.304,0,10.106-0.995,13.751-3.065V77.415h-12.011V67.97 h22.451V97.96z"/>
<polygon fill="#FFFFFF" points="137.57,43.615 149.583,43.615 165.571,87.937 182.057,43.615 193.325,43.615 169.628,102.268 160.601,102.268 "/>
<path fill="#FFFFFF" d="M347.324,57.896c1.477-2.216,3.362-3.993,5.649-5.338c2.289-1.341,4.863-2.298,7.725-2.87 c2.858-0.567,5.737-0.855,8.628-0.855c2.627,0,5.285,0.186,7.978,0.555c2.689,0.369,5.146,1.092,7.367,2.163 c2.223,1.075,4.04,2.568,5.452,4.483c1.414,1.913,2.119,4.446,2.119,7.601v27.083c0,2.35,0.136,4.6,0.404,6.745 c0.27,2.151,0.74,3.761,1.414,4.835h-14.536c-0.27-0.807-0.489-1.627-0.657-2.468c-0.169-0.839-0.284-1.695-0.353-2.567 c-2.288,2.351-4.981,3.994-8.076,4.932c-3.096,0.94-6.257,1.411-9.487,1.411c-2.493,0-4.812-0.302-6.968-0.906 c-2.154-0.605-4.038-1.543-5.651-2.821c-1.615-1.274-2.876-2.886-3.787-4.832c-0.907-1.948-1.361-4.262-1.361-6.948 c0-2.952,0.521-5.386,1.564-7.299c1.044-1.912,2.387-3.438,4.038-4.581c1.647-1.142,3.532-1.997,5.653-2.568 c2.12-0.57,4.256-1.022,6.41-1.359c2.151-0.334,4.273-0.604,6.358-0.804c2.086-0.202,3.937-0.504,5.555-0.906 c1.613-0.404,2.889-0.989,3.835-1.763c0.941-0.77,1.378-1.896,1.31-3.374c0-1.543-0.251-2.768-0.756-3.673 c-0.505-0.908-1.177-1.611-2.02-2.116c-0.839-0.503-1.815-0.837-2.926-1.006c-1.112-0.167-2.307-0.251-3.584-0.251 c-2.827,0-5.048,0.603-6.661,1.812c-1.618,1.207-2.56,3.221-2.829,6.04h-14.333C345,62.896,345.84,60.11,347.324,57.896 M375.637,78.082c-0.907,0.304-1.884,0.553-2.928,0.755c-1.044,0.201-2.136,0.372-3.278,0.503c-1.145,0.137-2.291,0.304-3.433,0.503 c-1.078,0.203-2.137,0.475-3.182,0.807c-1.044,0.338-1.951,0.79-2.726,1.359c-0.772,0.572-1.396,1.293-1.868,2.166 c-0.469,0.874-0.704,1.978-0.704,3.322c0,1.276,0.235,2.35,0.704,3.222c0.473,0.871,1.112,1.561,1.919,2.063 c0.81,0.503,1.751,0.856,2.829,1.057c1.072,0.204,2.185,0.302,3.33,0.302c2.826,0,5.014-0.469,6.561-1.409 c1.547-0.94,2.691-2.065,3.432-3.374c0.742-1.308,1.194-2.633,1.363-3.977c0.167-1.342,0.251-2.417,0.251-3.222v-5.336 C377.304,77.363,376.548,77.781,375.637,78.082"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

18
public/nightjet.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
style="overflow:visible"
enable-background="new 0 0 1371.57 281.204"
overflow="visible"
viewBox="0 0 947.40521 151.96"
height="151.96001"
width="947.40515"
id="svg3336"
version="1.1">
<g id="g7806" transform="translate(68.810197,566.846)">
<path
id="path7808"
d="m 461.858,-518.956 -3.649,15.83 -6.74,29.12 -5.62,24.36 c -0.3,1.29 -3.7,2.76 -8.46,2.76 h -22.96 c -4.68,-0.03 -7.33,-1.48 -7.05,-2.76 l 5.62,-24.36 6.72,-29.12 1.04,-4.5 c 0.53,-2.25 -3.53,-3.6 -7.92,-3.6 h -52.7 c -0.149,0.62 -0.25,1.11 -0.3,1.35 l -13.61,60.23 c 0,0 0,0.02 0,0.03 -0.199,0.771 -1.21,1.35 -2.55,1.79 -0.13,0.04 -0.25,0.08 -0.37,0.12 -0.569,0.16 -1.149,0.3 -1.779,0.42 -0.44,0.07 -0.91,0.15 -1.41,0.2 -0.92,0.12 -1.79,0.18 -2.57,0.19 -0.05,0 -0.1,0.01 -0.16,0.01 h -0.3 -1.37 -22.52 -0.431 c -4.699,-0.03 -7.34,-1.48 -7.04,-2.76 l 18.29,-81.15 c 0,-0.02 0.021,-0.02 0.021,-0.02 l 7.52,-33.39 c 0.28,-1.19 3.88,-2.62 8.87,-2.62 h 23.73 c 5.26,0 8.189,1.47 7.92,2.62 l -6.9,30.68 c 0,0 -0.04,0.09 -0.04,0.17 20.24,0.07 69.32,0.28 69.32,0.28 3.649,0 7.06,0.29 10.14,0.83 11.548,1.98 18.639,7.31 17.258,13.29 m 90.86,-11.97 -3.319,14.66 c -0.28,1.29 -3.841,2.77 -8.851,2.77 l -9.8,-0.01 c -5.12,0.03 -9.99,1.35 -10.5,3.62 l -13.61,60.24 c -0.28,1.3 -3.84,2.78 -8.85,2.78 h -24.19 c -4.939,-0.03 -7.779,-1.49 -7.47,-2.78 l 25.84,-114.58 c 0.271,-1.18 3.89,-2.62 8.88,-2.62 h 23.721 c 5.239,0 8.189,1.48 7.939,2.62 l -6.92,30.69 h 19.18 c 5.261,0 8.23,1.41 7.95,2.61 m 325.86,0.2 -3.311,14.61 c -0.29,1.29 -3.83,2.76 -8.819,2.76 h -9.761 c -5.109,0.02 -9.96,1.33 -10.47,3.59 l -13.56,60.04 c -0.29,1.3 -3.83,2.77 -8.82,2.77 h -24.11 c -4.939,-0.02 -7.75,-1.48 -7.449,-2.77 l 25.76,-114.2 c 0.27,-1.16 3.87,-2.59 8.83,-2.59 h 23.66 c 5.22,0 8.159,1.47 7.899,2.59 l -6.899,30.6 h 19.14 c 5.21,0 8.18,1.4 7.91,2.6 m -269.179,-34.37 c 4.84,0 7.76,1.38 7.5,2.55 l -3.2,14.24 c -0.45,1.93 -5.96,2.729 -8.99,2.71 h -23.43 c -4.841,-0.02 -7.601,-1.48 -7.33,-2.71 l 3.22,-14.24 c 0.32,-1.15 3.8,-2.55 8.67,-2.55 z m -22.731,136.48 c -1.61,7.189 -16.99,13.73 -34.811,13.73 -4.8,-0.011 -7.75,-1.17 -7.449,-2.531 l 25.37,-112.37 c 0.31,-1.16 3.81,-2.56 8.68,-2.56 l 23.26,0.01 c 5.14,-0.01 8.04,1.4 7.79,2.55 z m 159.02,-80.72 c 0.62,-2.73 -3.55,-3.99 -8.05,-4.02 h -55.069 c -4.75,0 -9.65,1.29 -10.141,3.53 l -2.479,10.93 h 73.39 z m 30.7,43.17 -3.729,16.54 c -0.271,1.24 -3.82,2.71 -8.83,2.71 h -114.58 c -17.72,0 -29.97,-6.56 -28.3,-13.86 l 13.25,-58.73 c 1.63,-7.22 17.43,-13.69 35.26,-13.69 h 90.14 c 17.69,0.07 30.53,6.18 28.84,13.69 l -7.76,34.3 c -0.34,1.53 -3.899,2.99 -8.89,2.99 l -105.601,0.01 -2.149,9.57 c -0.57,2.49 3.71,3.82 8.34,3.82 h 96.39 c 5.229,0.08 7.88,1.52 7.619,2.65 m -628.42,-97.63 c 4.59,0 7.34,1.37 7.07,2.53 l -3.271,14.14 c -0.439,1.92 -5.68,2.72 -8.529,2.7 h -22.181 c -4.609,-0.02 -7.189,-1.48 -6.91,-2.7 l 3.261,-14.14 c 0.319,-1.15 3.649,-2.53 8.26,-2.53 z m -7.79,32.54 c 4.87,-0.01 7.61,1.39 7.351,2.54 l -18.261,79.05 c -0.439,1.91 -5.68,2.72 -8.54,2.7 h -22.19 c -4.58,-0.03 -7.19,-1.45 -6.87,-2.7 l 18.23,-79.05 c 0.319,-1.15 3.649,-2.55 8.26,-2.55 z m -52.46,12.08 -3.01,13.03 -6.74,29.19 -6.29,27.26 c -0.16,0.67 -1.16,1.4 -2.77,1.94 -1.471,0.49 -3.44,0.82 -5.7,0.82 h -23.03 c -2.21,-0.01 -3.97,-0.35 -5.189,-0.82 -1.37,-0.54 -2.021,-1.27 -1.87,-1.94 l 6.29,-27.26 6.74,-29.19 0.859,-3.74 c 0.53,-2.27 -3.56,-3.59 -7.96,-3.59 h -54.56 l -1.681,7.33 -6.75,29.19 -6.279,27.26 c -0.16,0.67 -1.181,1.4 -2.78,1.94 -1.47,0.49 -3.42,0.82 -5.72,0.82 h -23.011 c -2.21,-0.01 -3.97,-0.35 -5.189,-0.82 -1.351,-0.54 -2.04,-1.27 -1.86,-1.94 l 6.28,-27.26 6.74,-29.19 5.68,-24.58 c 0.33,-1.18 3.729,-2.61 8.47,-2.61 h 101.88 c 17.32,0 29.21,6.55 27.45,14.16 m 165.88,4.84 c 4.17,0 8.05,1.37 7.62,3.35 l -0.56,2.45 -0.28,1.22 -6.44,27.89 -1.01,4.37 c -0.47,2.02 -4.979,3.37 -9.22,3.37 h -43.54 c -4.23,0 -8.13,-1.35 -7.66,-3.37 l 1,-4.37 6.45,-27.89 0.84,-3.64 c 0.46,-2.01 4.99,-3.38 9.16,-3.38 z m 20.28,-18.96 h -75.44 c -16.56,0 -30.8,6.26 -32.479,13.51 l -2.87,12.47 -0.83,3.55 -0.13,0.22 -6.46,27.9 -3.49,15.15 c -1.32,5.74 5.47,10.84 16.53,12.74 2.95,0.52 6.22,0.79 9.72,0.79 h 68.01 l -2.27,9.79 c -0.47,2.05 -5.01,3.27 -9.18,3.27 h -81.5 c -4.391,0.01 -7.58,1.33 -7.99,2.44 l -0.641,2.67 c -1.68,7.331 9.671,13.581 26.24,13.581 h 75.79 c 16.36,0 30.49,-6.17 32.16,-13.34 l 0.69,-2.91 3.739,-16.29 3.83,-16.58 c 0,0 0.011,-0.01 0.011,-0.03 l 3.479,-15.06 6.43,-27.89 2.88,-12.47 c 1.681,-7.251 -9.679,-13.511 -26.229,-13.511"
style="fill:white;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

52
public/ouigo.svg Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="230.88875" height="230.86626" id="svg2" version="1.1" inkscape:version="0.48.4 r9939" sodipodi:docname="Nouveau document 1">
<defs id="defs4">
<clipPath id="clipPath3001" clipPathUnits="userSpaceOnUse">
<path id="path3003" d="m 120.688,3.172 353.899,0 0,365.373 -353.899,0 0,-365.373 z" inkscape:connector-curvature="0"/>
</clipPath>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="122.4405" inkscape:cy="412.81147" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="1" fit-margin-left="1" fit-margin-right="1" fit-margin-bottom="1" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1"/>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Calque 1" inkscape:groupmode="layer" id="layer1" transform="translate(-252.5595,-714.3074)">
<g id="g2997" transform="matrix(1.25,0,0,-1.25,2.953125,1058.5434)">
<g id="g2999" clip-path="url(#clipPath3001)">
<g id="g3005" transform="translate(292.7454,251.5881)">
<path inkscape:connector-curvature="0" d="m 0,0 c -6.325,0 -10.821,5.154 -10.821,11.29 0,6.126 4.391,11.181 10.725,11.181 6.339,0 10.825,-5.164 10.825,-11.299 C 10.729,5.06 6.337,0 0,0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3007"/>
</g>
<g id="g3009" transform="translate(292.7454,91.7101)">
<path inkscape:connector-curvature="0" d="m 0,0 c -6.325,0 -10.821,5.147 -10.821,11.284 0,6.123 4.391,11.172 10.725,11.172 6.339,0 10.825,-5.145 10.825,-11.292 C 10.729,5.053 6.337,0 0,0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3011"/>
</g>
<g id="g3013" transform="translate(383.5961,183.0498)">
<path inkscape:connector-curvature="0" d="m 0,0 c 0,-50.582 -40.979,-91.554 -91.548,-91.554 -50.59,0 -91.563,40.972 -91.563,91.554 0,50.518 40.973,91.539 91.563,91.539 C -40.979,91.539 0,50.518 0,0" style="fill:#e3006a;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3015"/>
</g>
<g id="g3017" transform="translate(250.5055,179.1495)">
<path inkscape:connector-curvature="0" d="m 0,0 0,16.799 c 0,2.19 1.729,3.934 3.932,3.934 2.191,0 3.936,-1.744 3.936,-3.934 l 0,-16.585 c 0,-5.83 2.906,-8.839 7.705,-8.839 4.801,0 7.711,2.904 7.711,8.559 l 0,16.865 c 0,2.19 1.734,3.934 3.928,3.934 2.202,0 3.932,-1.744 3.932,-3.934 l 0,-16.551 c 0,-10.821 -6.07,-16.123 -15.676,-16.123 C 5.879,-15.875 0,-10.528 0,0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3019"/>
</g>
<g id="g3021" transform="translate(286.9556,195.9439)">
<path inkscape:connector-curvature="0" d="M 0,0 C 0,2.2 1.744,3.938 3.938,3.938 6.136,3.938 7.874,2.2 7.874,0 l 0,-28.49 c 0,-2.2 -1.738,-3.938 -3.936,-3.938 C 1.744,-32.428 0,-30.69 0,-28.49 L 0,0 z" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3023"/>
</g>
<g id="g3025" transform="translate(295.6633,209.2582)">
<path inkscape:connector-curvature="0" d="m 0,0 c 0,-2.633 -2.138,-4.771 -4.771,-4.771 -2.632,0 -4.764,2.138 -4.764,4.771 0,2.635 2.132,4.766 4.764,4.766 C -2.138,4.766 0,2.635 0,0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3027"/>
</g>
<g id="g3029" transform="translate(330.6357,184.4189)">
<path inkscape:connector-curvature="0" d="m 0,0 -9.197,0 c -1.89,0 -3.418,-1.541 -3.418,-3.429 0,-1.892 1.528,-3.367 3.418,-3.367 l 5.747,0 0,-4.89 c -1.817,-1.685 -4.401,-2.363 -8.065,-2.363 -6.322,0 -11.123,4.852 -11.123,11.38 0,6.085 4.85,11.194 10.567,11.194 3.375,0 5.667,-0.923 7.82,-2.451 0.561,-0.406 1.267,-0.809 2.395,-0.809 2.149,0 3.885,1.734 3.885,3.878 0,1.522 -0.873,2.601 -1.633,3.162 -3.223,2.251 -6.8,3.471 -12.206,3.471 -10.828,0 -19.053,-8.379 -19.053,-18.541 0,-10.563 7.97,-18.433 19.108,-18.433 5.531,0 10.201,1.687 13.591,4.326 1.743,1.335 2.106,2.391 2.106,4.696 l 0,8.251 C 3.942,-1.747 2.2,0 0,0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3031"/>
</g>
<g id="g3033" transform="translate(355.9935,170.4649)">
<path inkscape:connector-curvature="0" d="m 0,0 c -6.333,0 -10.819,5.154 -10.819,11.288 0,6.128 4.392,11.183 10.725,11.183 6.335,0 10.826,-5.154 10.826,-11.29 C 10.732,5.062 6.331,0 0,0 m 0,29.714 c -11.027,0 -19.047,-8.371 -19.047,-18.533 0,-10.16 7.919,-18.43 18.953,-18.43 11.02,0 19.04,8.373 19.04,18.537 0,10.16 -7.911,18.426 -18.946,18.426" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3035"/>
</g>
<g id="g3037" transform="translate(228.1901,170.4649)">
<path inkscape:connector-curvature="0" d="m 0,0 c -6.34,0 -10.826,5.154 -10.826,11.288 0,6.128 4.388,11.183 10.725,11.183 6.331,0 10.822,-5.154 10.822,-11.29 C 10.721,5.062 6.325,0 0,0 m 0,29.714 c -11.033,0 -19.055,-8.371 -19.055,-18.533 0,-10.16 7.917,-18.43 18.954,-18.43 11.027,0 19.041,8.373 19.041,18.537 0,10.16 -7.915,18.426 -18.94,18.426" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path3039"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

23
public/renfe.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400.1" height="152.9" version="1.1" viewBox="0 0 400.1 152.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style type="text/css">
.st0{clip-path:url(#vlpa);fill:#830065;}
</style>
<title>Renfe</title>
<g transform="matrix(.1488 0 0 .1488 -8.704 -6.964)">
<defs>
<rect id="vlpb" x="58.5" y="46.7" width="2689" height="1028"/>
</defs>
<clipPath id="vlpa">
<use width="100%" height="100%" xlink:href="#vlpb"/>
</clipPath>
<path class="st0" d="m1622 355.2h-391c-20.6 115.9-54.7 314.2-71.6 411.1h166.1c12.7-72.9 47.5-272.8 62.9-360.3h124.6c52.3 0 89.9 35.3 81.2 95.8-8.3 47.9-28.4 163.3-46.1 264.5h166.1c11.8-68 34.8-199.8 45.6-261.9 14.8-84.2-29.7-149.2-137.8-149.2" clip-path="url(#vlpa)"/>
<path class="st0" d="m773.5 717.3c-77.2-12.7-62.8-105-62.8-105 68.5 8.2 154.9 6.3 213.9 0.4 51.1-5 151.9-17.7 180.7-96.3 28.5-77.9 6.8-140.9-87.2-158.5-69.2-12.8-133.5-13.5-217.5-2.2-119.9 16.1-185.9 56.7-215.2 128-16.1 39.3-25.8 86-30.1 128.4-7.6 77.1 6.9 147.8 103.4 155.5 129.4 10.4 231.2 8.1 339-8.8l33.6-47.9c-102.3 17-218.9 12.9-257.8 6.4m-32.5-229c20.6-49 50.9-79.8 107.6-92.8 25.1-5.8 46.5-5.7 76.5 0.8 44.5 9.5 58.2 61.4 38.6 101.9-21.8 45.2-51 55.3-89.2 63.1-41.4 8.6-95.9 9.8-155.9 5.7 4.2-25.1 14.7-60.4 22.4-78.7" clip-path="url(#vlpa)"/>
<path class="st0" d="m2404 717.3c-77.2-12.7-62.8-105-62.8-105 68.6 8.2 154.9 6.3 213.9 0.4 51.1-5 151.9-17.7 180.6-96.3 28.5-77.9 6.9-140.9-87.1-158.5-69.3-12.8-133.6-13.5-217.6-2.2-119.9 16.1-185.7 56.7-215.1 128-16.2 39.3-25.9 86-30.1 128.4-7.6 77.1 6.9 147.8 103.4 155.5 129.3 10.4 231.1 8.1 339.1-8.8l33.5-47.9c-102.3 17-218.8 12.9-257.8 6.4m-32.4-229c20.5-49 50.8-79.8 107.4-92.8 25.1-5.8 46.6-5.7 76.6 0.8 44.5 9.5 58.1 61.4 38.7 101.9-21.9 45.2-51 55.3-89.2 63.1-41.4 8.6-95.9 9.8-155.9 5.7 4.1-25.1 14.6-60.4 22.4-78.7" clip-path="url(#vlpa)"/>
<path class="st0" d="m272.3 492.2c8.8-50.2 26.6-79.2 81.1-89.1 48.8-8.9 139.1-3 192.1 2.9l35.6-50.9c-80.3-5.8-215.9-12.4-296.3-0.7-114.6 16.6-161.3 40-174.3 113.3-7.7 43.9-42.3 242.3-52.1 298.6h166.1c11.4-65.4 39.7-227.7 47.8-274.1" clip-path="url(#vlpa)"/>
<path class="st0" d="m2065 103.3c-94.4 66.2-126.4 147.4-143.8 247.2l-70.4 402.7c-15.8 91.6-22.8 139.4-38.4 184.4-12.5 37.7-53.6 93.1-122 94l-29.9 42.6c88.3 4.3 158.9-21.2 208.8-56.1 94.4-66.1 129.2-160.7 146.7-260.3 1.2-6.8 31.9-183.1 46.6-266.6 10.7-59.3 41.8-91.8 107.3-92.7l30.4-43.3h-114c16.2-92.4 22-132.3 35.3-170.7 13-37.7 53.7-93.1 121.9-93.9l30.4-43.3c-88.5-4.4-159 21.1-208.9 56" clip-path="url(#vlpa)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

6
public/ter.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="398.9px" height="193.4px" viewBox="0 0 398.9 193.4" enable-background="new 0 0 398.9 193.4" xml:space="preserve">
<path id="path11" fill="white" d="M143.2,144.6c0-1.2,0.1-2.4,0.4-3.6c1.5-7,6.9-14.3,13.9-18.6c22.2-13.6,40.6-9.6,43.3-8.1 c0.3,0.2,0.1,0.4-0.1,0.9c-2.5,6.4-25.6,26.7-55.2,37.5C144.1,150.4,143.2,147.3,143.2,144.6 M220,107.4c-2.8-3.1-10.2-6.5-20.2-8.1 c-9.7-1.5-29.8-2.6-51.6,8.6c-22.8,11.7-37.5,33.4-30.9,54c0.1,0-19.2,7.1-33.6,4.2c-7.8-1.6-15.1-7.3-17.4-15 c-3.6-11.8-0.6-32,19.6-50.4c26.7-24.4,66.8-33.1,83.9-36.8c7.9-1.7,8.9-2,9.1-3.1c0.1-0.5,0.4-2.3-1.2-4.2 c-2.4-2.8-9.3-5.8-28.3-3.8C129.8,55,103,62.9,95,65.4c5.5-11.5,22.5-42.1,25.5-47.9c4.9-9.4,6.7-13.4,6.7-15.6 c0-0.6-0.2-1.1-0.5-1.4c-2.8-2.6-18.8,6.1-31,20.5C83.4,35.4,76.9,47,67.2,62.2c-10.7,17-44.3,25.6-60.5,30c-4.3,1.2-5.6,1.4-6.2,2 c-0.5,0.6-0.7,1.4-0.5,2.2c1.4,4.8,9.6,9.9,18.1,10.3c6.2,0.3,13.5-0.7,25-3.9c-6.1,9.7-16.5,31.2-16.5,51.6 c0,15.9,7.3,34.8,38.4,37.8c24.5,2.3,48.2-6.4,64.9-12.8c18.8,14.6,44.4,13.8,44.5,13.8c65.7,0,96.2-52.6,101.8-62.4 c0,0.1,6,19.6,17.5,35.4c15.7,21.7,37.5,28.5,59.8,26.4c26.5-2.5,43.6-23,45.2-26.8c0.8-1.9,0-3.9-4.9-1.3 c-4.1,2.2-8.6,5.5-19.6,7.6c-49.2,9.3-63.9-41.8-74.4-63.7c-1.4-3.1-2.7-5.4-4.1-6c-3-1.2-26.2,2.9-28.7,7.8 c-25.8,48-59.4,58.7-84.8,59.1c-8.5,0.1-15.7-1.6-21.4-4.6c10-4.9,17-8.7,24.2-13.5c26.4-17.6,36-36.4,36-42.5 C220.8,108.3,220.5,107.9,220,107.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

2
public/tgv_inoui.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

17
public/trenitalia.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 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": "sncf-station",
"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,76 +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) {
navigate(`/station/${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} = 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,230 +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} 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} />
</CSSTransition>)
return <>
<TableBody>
<TransitionGroup component={null}>
{table_rows}
</TransitionGroup>
</TableBody>
</>
}
function TrainRow({train, tableType}) {
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, route)
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…"}
let stopsFilter
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={`#${getBackgroundColor(train, route)}`}
color={`#${getTextColor(train, route)}`}>
{trainType}
</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" fontWeight="bold" color="#FFED02" fontSize={24}>
{getDisplayTime(train, tableType)}
</Box>
</TableCell>
<TableCell>
<Typography fontSize={24} fontWeight="bold" data-stop-id={headline.id}>{headline.name}</Typography>
<span className="stops">{stopsNames}</span>
</TableCell>
</StyledTableRow>
</>
}
function getTrainType(train, route) {
if (train.id.startsWith("IDFM"))
return route.short_name
else {
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
if (trainType === "Train TER")
trainType = "TER"
else if (trainType === "INTERCITES")
trainType = "INTER-CITÉS"
else if (trainType === "INTERCITES de nuit")
trainType = "INTER-CITÉS de nuit"
return trainType
}
}
function getBackgroundColor(train, route) {
if (route.color)
return route.color
else if (getTrainType(train, route) === "OUIGO")
return "E60075"
return "FFFFFF"
}
function getTextColor(train, route) {
if (route.text_color)
return route.text_color
else {
let trainType = getTrainType(train, 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)
}
export default TrainsTable;

View File

View File

View File

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

View File

@ -1,63 +0,0 @@
from rest_framework import serializers
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo
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)
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__'

View File

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

View File

@ -1,224 +0,0 @@
from datetime import datetime, timedelta, date
from django.db.models import F, Q, Value
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 sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \
FeedInfoSerializer
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo
CACHE_CONTROL = cache_control(max_age=7200)
LAST_MODIFIED = last_modified(lambda *args, **kwargs: datetime.fromisoformat(FeedInfo.objects.get().version))
@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__'
@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',]
@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__'
@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__'
@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', ]
@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__'
@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__'
@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]
@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__'
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:
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'))
qs_today = StopTime.objects.filter(stop_filter) \
.filter(Q(departure_time__gte=query_time, pickup_type=0), calendar_filter(query_date)) \
.annotate(departure_date=Value(query_date)) \
.annotate(departure_time_24h=F('departure_time'))
qs_yesterday = StopTime.objects.filter(stop_filter) \
.filter(Q(departure_time__gte=time_yesterday, pickup_type=0), 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) \
.filter(Q(departure_time__gte=timedelta(0), pickup_type=0), 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))
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:
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'))
qs_today = StopTime.objects.filter(stop_filter) \
.filter(Q(arrival_time__gte=query_time, drop_off_type=0), calendar_filter(query_date)) \
.annotate(arrival_date=Value(query_date)) \
.annotate(arrival_time_24h=F('arrival_time'))
qs_yesterday = StopTime.objects.filter(stop_filter) \
.filter(Q(arrival_time__gte=time_yesterday, drop_off_type=0), 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) \
.filter(Q(arrival_time__gte=timedelta(0), drop_off_type=0), 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 sncf 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", "sncf.settings")
application = get_asgi_application()

View File

@ -1,157 +0,0 @@
"""
Django settings for sncf 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",
"sncf.api",
"sncfgtfs",
]
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 = "sncf.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 = "sncf.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,
}
try:
from .settings_local import *
except ImportError:
pass

View File

@ -1,20 +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": "sncf",
"USER": "sncf",
"PASSWORD": "CHANGE ME",
"HOST": "localhost",
"PORT": "5432",
}
}

View File

@ -1,41 +0,0 @@
"""
URL configuration for sncf 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 sncf.api.views import AgencyViewSet, StopViewSet, RouteViewSet, TripViewSet, StopTimeViewSet, \
CalendarViewSet, CalendarDateViewSet, TransferViewSet, FeedInfoViewSet, NextDeparturesViewSet, NextArrivalsViewSet
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("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 sncf 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", "sncf.settings")
application = get_wsgi_application()

View File

View File

@ -1,98 +0,0 @@
from django.contrib import admin
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo, StopTimeUpdate, TripUpdate
@admin.register(Agency)
class AgencyAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'url', 'timezone',)
search_fields = ('name',)
@admin.register(Stop)
class StopAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'lat', 'lon', 'location_type',)
list_filter = ('location_type',)
search_fields = ('name', 'id',)
ordering = ('name',)
autocomplete_fields = ('parent_station',)
@admin.register(Route)
class RouteAdmin(admin.ModelAdmin):
list_display = ('long_name', 'short_name', 'id', 'type',)
list_filter = ('type',)
search_fields = ('long_name', 'short_name', 'id',)
ordering = ('long_name',)
autocomplete_fields = ('agency',)
@admin.register(Trip)
class TripAdmin(admin.ModelAdmin):
list_display = ('id', 'route', 'service', 'headsign', 'direction_id',)
list_filter = ('direction_id', 'service__transport_type',)
search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',)
ordering = ('route', 'service',)
@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__service__transport_type',)
search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip', 'stop_sequence',)
autocomplete_fields = ('trip', 'stop',)
@admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin):
list_display = ('id', 'transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'sunday', 'start_date', 'end_date',)
list_filter = ('transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
'start_date', 'end_date',)
search_fields = ('id', 'start_date', 'end_date',)
ordering = ('transport_type', 'id',)
@admin.register(CalendarDate)
class CalendarDateAdmin(admin.ModelAdmin):
list_display = ('id', 'service_id', 'date', 'exception_type',)
list_filter = ('exception_type', 'date', 'service__transport_type',)
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',)
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',)
search_fields = ('trip_update__trip_id', 'stop_time__stop__name', 'arrival_time', 'departure_time',)
ordering = ('trip_update', 'stop_time',)
@admin.register(TripUpdate)
class TripUpdateAdmin(admin.ModelAdmin):
list_display = ('trip_id', 'start_date', 'start_time',)
search_fields = ('trip_id', 'start_date', 'start_time',)
ordering = ('trip_id', 'start_date', 'start_time',)
autocomplete_fields = ('trip',)

View File

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

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,530 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-04 22:16+0100\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"
#: sncfgtfs/models.py:6
msgid "TGV"
msgstr "TGV"
#: sncfgtfs/models.py:7
msgid "TER"
msgstr "TER"
#: sncfgtfs/models.py:8
msgid "Intercités"
msgstr "Intercités"
#: sncfgtfs/models.py:9
msgid "Transilien"
msgstr "Transilien"
#: sncfgtfs/models.py:13
msgid "Stop/platform"
msgstr "Arrêt / quai"
#: sncfgtfs/models.py:14
msgid "Station"
msgstr "Gare"
#: sncfgtfs/models.py:15
msgid "Entrance/exit"
msgstr "Entrée / sortie"
#: sncfgtfs/models.py:16
msgid "Generic node"
msgstr "Nœud générique"
#: sncfgtfs/models.py:17
msgid "Boarding area"
msgstr "Zone d'embarquement"
#: sncfgtfs/models.py:21
msgid "No information"
msgstr "Pas d'information"
#: sncfgtfs/models.py:22
msgid "Possible"
msgstr "Possible"
#: sncfgtfs/models.py:23 sncfgtfs/models.py:53
msgid "Not possible"
msgstr "Impossible"
#: sncfgtfs/models.py:27
msgid "Regular"
msgstr "Régulier"
#: sncfgtfs/models.py:28
msgid "None"
msgstr "Aucun"
#: sncfgtfs/models.py:29
msgid "Must phone agency"
msgstr "Doit téléphoner à l'agence"
#: sncfgtfs/models.py:30
msgid "Must coordinate with driver"
msgstr "Doit se coordonner avec læ conducteurice"
#: sncfgtfs/models.py:34
msgid "Tram"
msgstr "Tram"
#: sncfgtfs/models.py:35
msgid "Metro"
msgstr "Métro"
#: sncfgtfs/models.py:36
msgid "Rail"
msgstr "Rail"
#: sncfgtfs/models.py:37
msgid "Bus"
msgstr "Bus"
#: sncfgtfs/models.py:38
msgid "Ferry"
msgstr "Ferry"
#: sncfgtfs/models.py:39
msgid "Cable car"
msgstr "Câble"
#: sncfgtfs/models.py:40
msgid "Gondola"
msgstr "Gondole"
#: sncfgtfs/models.py:41
msgid "Funicular"
msgstr "Funiculaire"
#: sncfgtfs/models.py:45
msgid "Outbound"
msgstr "Vers l'extérieur"
#: sncfgtfs/models.py:46
msgid "Inbound"
msgstr "Vers l'intérieur"
#: sncfgtfs/models.py:50
msgid "Recommended"
msgstr "Recommandé"
#: sncfgtfs/models.py:51
msgid "Timed"
msgstr "Correspondance programmée"
#: sncfgtfs/models.py:52
msgid "Minimum time"
msgstr "Temps de correspondance minimum requis"
#: sncfgtfs/models.py:57
msgid "Added"
msgstr "Ajouté"
#: sncfgtfs/models.py:58
msgid "Removed"
msgstr "Supprimé"
#: sncfgtfs/models.py:62
msgid "Scheduled"
msgstr "Planifié"
#: sncfgtfs/models.py:63
msgid "Skipped"
msgstr "Sauté"
#: sncfgtfs/models.py:64
msgid "No data"
msgstr "Pas de données"
#: sncfgtfs/models.py:65
msgid "Unscheduled"
msgstr "Non planifié"
#: sncfgtfs/models.py:72 sncfgtfs/models.py:229
msgid "Agency ID"
msgstr "ID de l'agence"
#: sncfgtfs/models.py:78
msgid "Agency name"
msgstr "Nom de l'agence"
#: sncfgtfs/models.py:82
msgid "Agency URL"
msgstr "URL de l'agence"
#: sncfgtfs/models.py:87
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: sncfgtfs/models.py:92
msgid "Agency language"
msgstr "Langue de l'agence"
#: sncfgtfs/models.py:98
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: sncfgtfs/models.py:103
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: sncfgtfs/models.py:111
msgid "Agency"
msgstr "Agence"
#: sncfgtfs/models.py:112
msgid "Agencies"
msgstr "Agences"
#: sncfgtfs/models.py:120 sncfgtfs/models.py:420
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: sncfgtfs/models.py:125
msgid "Stop code"
msgstr "Code de l'arrêt"
#: sncfgtfs/models.py:131
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: sncfgtfs/models.py:136
msgid "Stop description"
msgstr "Description de l'arrêt"
#: sncfgtfs/models.py:141
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: sncfgtfs/models.py:145
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: sncfgtfs/models.py:150
msgid "Zone ID"
msgstr "ID de la zone"
#: sncfgtfs/models.py:154
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: sncfgtfs/models.py:159
msgid "Location type"
msgstr "Type de localisation"
#: sncfgtfs/models.py:168
msgid "Parent station"
msgstr "Gare parente"
#: sncfgtfs/models.py:176
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: sncfgtfs/models.py:182
msgid "Level ID"
msgstr "ID du niveau"
#: sncfgtfs/models.py:187
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: sncfgtfs/models.py:195
msgid "Platform code"
msgstr "Code du quai"
#: sncfgtfs/models.py:214
msgid "Stop"
msgstr "Arrêt"
#: sncfgtfs/models.py:215
msgid "Stops"
msgstr "Arrêts"
#: sncfgtfs/models.py:223 sncfgtfs/models.py:399 sncfgtfs/models.py:538
#: sncfgtfs/models.py:576
msgid "ID"
msgstr "Identifiant"
#: sncfgtfs/models.py:235
msgid "Route short name"
msgstr "Nom court de la ligne"
#: sncfgtfs/models.py:240
msgid "Route long name"
msgstr "Nom long de la ligne"
#: sncfgtfs/models.py:245
msgid "Route description"
msgstr "Description de la ligne"
#: sncfgtfs/models.py:250
msgid "Route type"
msgstr "Type de ligne"
#: sncfgtfs/models.py:255
msgid "Route URL"
msgstr "URL de la ligne"
#: sncfgtfs/models.py:261
msgid "Route color"
msgstr "Couleur de la ligne"
#: sncfgtfs/models.py:267
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: sncfgtfs/models.py:275 sncfgtfs/models.py:290
msgid "Route"
msgstr "Ligne"
#: sncfgtfs/models.py:276
msgid "Routes"
msgstr "Lignes"
#: sncfgtfs/models.py:284
msgid "Trip ID"
msgstr "ID du trajet"
#: sncfgtfs/models.py:297 sncfgtfs/models.py:544
msgid "Service"
msgstr "Service"
#: sncfgtfs/models.py:303
msgid "Trip headsign"
msgstr "Destination du trajet"
#: sncfgtfs/models.py:309
msgid "Trip short name"
msgstr "Nom court du trajet"
#: sncfgtfs/models.py:314
msgid "Direction"
msgstr "Direction"
#: sncfgtfs/models.py:321
msgid "Block ID"
msgstr "ID du bloc"
#: sncfgtfs/models.py:327
msgid "Shape ID"
msgstr "ID de la forme"
#: sncfgtfs/models.py:332
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: sncfgtfs/models.py:339
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: sncfgtfs/models.py:391 sncfgtfs/models.py:405 sncfgtfs/models.py:648
msgid "Trip"
msgstr "Trajet"
#: sncfgtfs/models.py:392
msgid "Trips"
msgstr "Trajets"
#: sncfgtfs/models.py:410 sncfgtfs/models.py:698
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: sncfgtfs/models.py:414 sncfgtfs/models.py:706
msgid "Departure time"
msgstr "Heure de départ"
#: sncfgtfs/models.py:425
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: sncfgtfs/models.py:430
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: sncfgtfs/models.py:435
msgid "Pickup type"
msgstr "Type de prise en charge"
#: sncfgtfs/models.py:442
msgid "Drop off type"
msgstr "Type de dépose"
#: sncfgtfs/models.py:449
msgid "Timepoint"
msgstr "Ponctualité"
#: sncfgtfs/models.py:472 sncfgtfs/models.py:688
msgid "Stop time"
msgstr "Heure d'arrêt"
#: sncfgtfs/models.py:473
msgid "Stop times"
msgstr "Heures d'arrêt"
#: sncfgtfs/models.py:480
msgid "Service ID"
msgstr "ID du service"
#: sncfgtfs/models.py:484
msgid "Monday"
msgstr "Lundi"
#: sncfgtfs/models.py:488
msgid "Tuesday"
msgstr "Mardi"
#: sncfgtfs/models.py:492
msgid "Wednesday"
msgstr "Mercredi"
#: sncfgtfs/models.py:496
msgid "Thursday"
msgstr "Jeudi"
#: sncfgtfs/models.py:500
msgid "Friday"
msgstr "Vendredi"
#: sncfgtfs/models.py:504
msgid "Saturday"
msgstr "Samedi"
#: sncfgtfs/models.py:508
msgid "Sunday"
msgstr "Dimanche"
#: sncfgtfs/models.py:512 sncfgtfs/models.py:654
msgid "Start date"
msgstr "Date de début"
#: sncfgtfs/models.py:516
msgid "End date"
msgstr "Date de fin"
#: sncfgtfs/models.py:521 sncfgtfs/models.py:559
msgid "Transport type"
msgstr "Type de transport"
#: sncfgtfs/models.py:529
msgid "Calendar"
msgstr "Calendrier"
#: sncfgtfs/models.py:530
msgid "Calendars"
msgstr "Calendriers"
#: sncfgtfs/models.py:549
msgid "Date"
msgstr "Date"
#: sncfgtfs/models.py:553
msgid "Exception type"
msgstr "Type d'exception"
#: sncfgtfs/models.py:567
msgid "Calendar date"
msgstr "Date du calendrier"
#: sncfgtfs/models.py:568
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: sncfgtfs/models.py:582
msgid "From stop"
msgstr "Depuis l'arrêt"
#: sncfgtfs/models.py:589
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: sncfgtfs/models.py:594
msgid "Transfer type"
msgstr "Type de correspondance"
#: sncfgtfs/models.py:600
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: sncfgtfs/models.py:605
msgid "Transfer"
msgstr "Correspondance"
#: sncfgtfs/models.py:606
msgid "Transfers"
msgstr "Correspondances"
#: sncfgtfs/models.py:613
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: sncfgtfs/models.py:617
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: sncfgtfs/models.py:622
msgid "Feed language"
msgstr "Langue du flux"
#: sncfgtfs/models.py:626
msgid "Feed start date"
msgstr "Date de début du flux"
#: sncfgtfs/models.py:630
msgid "Feed end date"
msgstr "Date de fin du flux"
#: sncfgtfs/models.py:635
msgid "Feed version"
msgstr "Version du flux"
#: sncfgtfs/models.py:639
msgid "Feed info"
msgstr "Information du flux"
#: sncfgtfs/models.py:640
msgid "Feed infos"
msgstr "Informations du flux"
#: sncfgtfs/models.py:658
msgid "Start time"
msgstr "Heure de début"
#: sncfgtfs/models.py:662 sncfgtfs/models.py:710
msgid "Schedule relationship"
msgstr "Relation de la planification"
#: sncfgtfs/models.py:671 sncfgtfs/models.py:681
msgid "Trip update"
msgstr "Mise à jour du trajet"
#: sncfgtfs/models.py:672
msgid "Trip updates"
msgstr "Mises à jour des trajets"
#: sncfgtfs/models.py:694
msgid "Arrival delay"
msgstr "Retard à l'arrivée"
#: sncfgtfs/models.py:702
msgid "Departure delay"
msgstr "Retard au départ"
#: sncfgtfs/models.py:719
msgid "Stop time update"
msgstr "Mise à jour du temps d'arrêt"
#: sncfgtfs/models.py:720
msgid "Stop time updates"
msgstr "Mises à jour des temps d'arrêt"

View File

@ -1,336 +0,0 @@
import csv
from datetime import datetime, timedelta
from io import BytesIO
from zipfile import ZipFile
import requests
from django.core.management import BaseCommand
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, Route, Stop, StopTime, Transfer, Trip
class Command(BaseCommand):
help = "Update the SNCF GTFS database."
GTFS_FEEDS = {
"TGV": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip",
"IC": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip",
"TER": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip",
"TN": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip",
}
def add_arguments(self, parser):
parser.add_argument('--bulk_size', type=int, default=1000, help='Number of objects to create in bulk.')
def handle(self, *args, **options):
bulk_size = options['bulk_size']
if not FeedInfo.objects.exists():
last_update_date = "1970-01-01"
else:
last_update_date = FeedInfo.objects.get().version
for url in self.GTFS_FEEDS.values():
last_modified = requests.head(url).headers["Last-Modified"]
last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z")
if last_modified.date().isoformat() > last_update_date:
break
else:
self.stdout.write(self.style.WARNING("Database already up-to-date."))
return
self.stdout.write("Updating database...")
for transport_type, feed_url in self.GTFS_FEEDS.items():
self.stdout.write(f"Downloading {transport_type} GTFS feed...")
with ZipFile(BytesIO(requests.get(feed_url).content)) as zipfile:
agencies = []
for agency_dict in csv.DictReader(zipfile.read("agency.txt").decode().splitlines()):
agency_dict: dict
agency = Agency(
id=agency_dict['agency_id'],
name=agency_dict['agency_name'],
url=agency_dict['agency_url'],
timezone=agency_dict['agency_timezone'],
lang=agency_dict['agency_lang'],
phone=agency_dict.get('agency_phone', ""),
email=agency_dict.get('agency_email', ""),
)
agencies.append(agency)
if agencies:
Agency.objects.bulk_create(agencies,
update_conflicts=True,
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'],
unique_fields=['id'])
agencies.clear()
stops = []
for stop_dict in csv.DictReader(zipfile.read("stops.txt").decode().splitlines()):
stop_dict: dict
stop = Stop(
id=stop_dict["stop_id"],
name=stop_dict['stop_name'],
desc=stop_dict['stop_desc'],
lat=stop_dict['stop_lat'],
lon=stop_dict['stop_lon'],
zone_id=stop_dict['zone_id'],
url=stop_dict['stop_url'],
location_type=stop_dict['location_type'],
parent_station_id=stop_dict['parent_station'] or None
if last_update_date != "1970-01-01" or transport_type != "TN" else None,
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', ""),
)
stops.append(stop)
if len(stops) >= bulk_size:
Stop.objects.bulk_create(stops,
update_conflicts=True,
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
'location_type', 'parent_station_id', 'timezone',
'wheelchair_boarding', 'level_id', 'platform_code'],
unique_fields=['id'])
stops.clear()
if stops:
Stop.objects.bulk_create(stops,
update_conflicts=True,
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
'location_type', 'parent_station_id', 'timezone',
'wheelchair_boarding', 'level_id', 'platform_code'],
unique_fields=['id'])
stops.clear()
routes = []
for route_dict in csv.DictReader(zipfile.read("routes.txt").decode().splitlines()):
route_dict: dict
route = Route(
id=route_dict['route_id'],
agency_id=route_dict['agency_id'],
short_name=route_dict['route_short_name'],
long_name=route_dict['route_long_name'],
desc=route_dict['route_desc'],
type=route_dict['route_type'],
url=route_dict['route_url'],
color=route_dict['route_color'],
text_color=route_dict['route_text_color'],
)
routes.append(route)
if len(routes) >= bulk_size:
Route.objects.bulk_create(routes,
update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color'],
unique_fields=['id'])
routes.clear()
if routes:
Route.objects.bulk_create(routes,
update_conflicts=True,
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
'type', 'url', 'color', 'text_color'],
unique_fields=['id'])
routes.clear()
calendar_ids = []
if "calendar.txt" in zipfile.namelist():
calendars = []
for calendar_dict in csv.DictReader(zipfile.read("calendar.txt").decode().splitlines()):
calendar_dict: dict
calendar = Calendar(
id=f"{transport_type}-{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'],
transport_type=transport_type,
)
calendars.append(calendar)
calendar_ids.append(calendar.id)
if len(calendars) >= bulk_size:
Calendar.objects.bulk_create(calendars,
update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'],
unique_fields=['id'])
calendars.clear()
if calendars:
Calendar.objects.bulk_create(calendars, update_conflicts=True,
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday', 'sunday', 'start_date',
'end_date', 'transport_type'],
unique_fields=['id'])
calendars.clear()
calendars = []
calendar_dates = []
for calendar_date_dict in csv.DictReader(zipfile.read("calendar_dates.txt").decode().splitlines()):
calendar_date_dict: dict
calendar_date = CalendarDate(
id=f"{transport_type}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}",
service_id=f"{transport_type}-{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 calendar_ids:
calendar = Calendar(
id=f"{transport_type}-{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'],
transport_type=transport_type,
)
calendars.append(calendar)
if len(calendar_dates) >= bulk_size:
Calendar.objects.bulk_create(calendars,
update_conflicts=True,
update_fields=['end_date'],
unique_fields=['id'])
CalendarDate.objects.bulk_create(calendar_dates,
update_conflicts=True,
update_fields=['service_id', 'date', 'exception_type'],
unique_fields=['id'])
calendars.clear()
calendar_dates.clear()
if calendar_dates:
Calendar.objects.bulk_create(calendars,
update_conflicts=True,
update_fields=['end_date'],
unique_fields=['id'])
CalendarDate.objects.bulk_create(calendar_dates,
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(zipfile.read("trips.txt").decode().splitlines()):
trip_dict: dict
trip = Trip(
id=trip_dict['trip_id'],
route_id=trip_dict['route_id'],
service_id=f"{transport_type}-{trip_dict['service_id']}",
headsign=trip_dict['trip_headsign'],
short_name=trip_dict.get('trip_short_name', ""),
direction_id=trip_dict['direction_id'] or None,
block_id=trip_dict['block_id'],
shape_id=trip_dict['shape_id'],
wheelchair_accessible=trip_dict.get('wheelchair_accessible', None),
bikes_allowed=trip_dict.get('bikes_allowed', None),
)
trips.append(trip)
if len(trips) >= bulk_size:
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'],
unique_fields=['id'])
trips.clear()
if trips:
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'],
unique_fields=['id'])
trips.clear()
stop_times = []
for stop_time_dict in csv.DictReader(zipfile.read("stop_times.txt").decode().splitlines()):
stop_time_dict: dict
arr_time = stop_time_dict['arrival_time']
arr_time = int(arr_time[:2]) * 3600 + int(arr_time[3:5]) * 60 + int(arr_time[6:])
dep_time = stop_time_dict['departure_time']
dep_time = int(dep_time[:2]) * 3600 + int(dep_time[3:5]) * 60 + int(dep_time[6:])
st = StopTime(
id=f"{stop_time_dict['trip_id']}-{stop_time_dict['stop_sequence']}",
trip_id=stop_time_dict['trip_id'],
arrival_time=timedelta(seconds=arr_time),
departure_time=timedelta(seconds=dep_time),
stop_id=stop_time_dict['stop_id'],
stop_sequence=stop_time_dict['stop_sequence'],
stop_headsign=stop_time_dict['stop_headsign'],
pickup_type=stop_time_dict['pickup_type'],
drop_off_type=stop_time_dict['drop_off_type'],
timepoint=stop_time_dict.get('timepoint', None),
)
stop_times.append(st)
if len(stop_times) >= bulk_size:
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:
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()
transfers = []
for transfer_dict in csv.DictReader(zipfile.read("transfers.txt").decode().splitlines()):
transfer_dict: dict
transfer = Transfer(
id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}",
from_stop_id=transfer_dict['from_stop_id'],
to_stop_id=transfer_dict['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:
Transfer.objects.bulk_create(transfers,
update_conflicts=True,
update_fields=['transfer_type', 'min_transfer_time'],
unique_fields=['id'])
transfers.clear()
if transfers:
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():
for feed_info_dict in csv.DictReader(zipfile.read("feed_info.txt").decode().splitlines()):
feed_info_dict: dict
FeedInfo.objects.update_or_create(
publisher_name=feed_info_dict['feed_publisher_name'],
defaults=dict(
publisher_url=feed_info_dict['feed_publisher_url'],
lang=feed_info_dict['feed_lang'],
start_date=feed_info_dict['feed_start_date'],
end_date=feed_info_dict['feed_end_date'],
version=feed_info_dict['feed_version'],
)
)

View File

@ -1,158 +0,0 @@
from datetime import timedelta, datetime, date, time
from zoneinfo import ZoneInfo
import requests
from django.core.management import BaseCommand
from sncfgtfs.gtfs_realtime_pb2 import FeedMessage
from sncfgtfs.models import Calendar, CalendarDate, StopTime, StopTimeUpdate, Trip, TripUpdate, Stop
class Command(BaseCommand):
help = "Update the SNCF GTFS Realtime database."
GTFS_RT_FEEDS = {
"TGV": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates",
"IC": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates",
"TER": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates",
}
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
for feed_type, feed_url in self.GTFS_RT_FEEDS.items():
self.stdout.write(f"Updating {feed_type} feed...")
feed_message = FeedMessage()
feed_message.ParseFromString(requests.get(feed_url).content)
stop_times_updates = []
for entity in feed_message.entity:
if entity.HasField("trip_update"):
trip_update = entity.trip_update
trip_id = trip_update.trip.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 == 1:
headsign = trip_id[5:-1]
trip_qs = Trip.objects.all()
trip_ids = trip_qs.values_list('id', flat=True)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
st_queryset = StopTime.objects.filter(stop__parent_station_id=stop_id)
if stop_sequence == 0:
st_queryset = st_queryset.filter(stop_sequence=0)
trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id'))
if trip_ids_restrict:
trip_ids = trip_ids_restrict
route_ids = set(Trip.objects.filter(id__in=trip_ids).values_list('route_id', flat=True))
self.stdout.write(f"{len(route_ids)} routes found on trip for train {headsign}")
if not route_ids:
self.stdout.write(f"Route not found for trip {trip_id}.")
continue
elif len(route_ids) > 1:
self.stdout.write(f"Multiple routes found for trip {trip_id}.")
continue
route_id = route_ids.pop()
Calendar.objects.update_or_create(
id=f"{feed_type}-new-{headsign}",
defaults={
"transport_type": feed_type,
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"start_date": start_date,
"end_date": start_date,
}
)
CalendarDate.objects.update_or_create(
id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}",
defaults={
"service_id": f"{feed_type}-new-{headsign}",
"date": trip_update.trip.start_date,
"exception_type": 1,
"transport_type": feed_type,
}
)
Trip.objects.update_or_create(
id=trip_id,
defaults={
"route_id": route_id,
"service_id": f"{feed_type}-new-{headsign}",
"headsign": headsign,
"direction_id": trip_update.trip.direction_id,
}
)
sample_trip = Trip.objects.filter(id__in=trip_ids).first()
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
stop_id = stop_time_update.stop_id
stop = Stop.objects.get(id=stop_id)
if stop.location_type == 1:
if stop_sequence == 0:
stop = sample_trip.stop_times.get(stop_sequence=0).stop
else:
previous_stop = sample_trip.stop_times.get(stop_sequence=stop_sequence - 1).stop
stop = next(s for s in stop.children.all() \
if s.location_type == 0 and s.stop_type == previous_stop.stop_type)
stop_id = stop.id
StopTime.objects.update_or_create(
id=f"{trip_id}-{stop_sequence}",
trip_id=trip_id,
defaults={
"stop_id": stop_id,
"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": 0 if stop_time_update.departure.time else 1,
"drop_off_type": 0 if stop_time_update.arrival.time else 1,
}
)
if not Trip.objects.filter(id=trip_id).exists():
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
continue
tu, _created = TripUpdate.objects.get_or_create(
trip_id=trip_id,
start_date=trip_update.trip.start_date,
start_time=trip_update.trip.start_time,
schedule_relationship=trip_update.trip.schedule_relationship,
)
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
if not StopTime.objects.filter(trip_id=trip_id, stop_sequence=stop_sequence).exists():
self.stdout.write(f"Stop {stop_sequence} does not exist in GTFS feed for trip {trip_id}.")
continue
st = StopTime.objects.get(trip_id=trip_id, stop_sequence=stop_sequence)
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 0,
)
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'])

View File

@ -1,581 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-27 14:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
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, unique=True, 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"
),
),
],
options={
"verbose_name": "Agency",
"verbose_name_plural": "Agencies",
"ordering": ("name",),
},
),
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")),
(
"transport_type",
models.CharField(
choices=[
("TGV", "TGV"),
("TER", "TER"),
("IC", "Intercités"),
("TN", "Transilien"),
],
max_length=255,
verbose_name="Transport type",
),
),
],
options={
"verbose_name": "Calendar",
"verbose_name_plural": "Calendars",
"ordering": ("id",),
},
),
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"),
),
],
options={
"verbose_name": "Feed info",
"verbose_name_plural": "Feed infos",
"ordering": ("publisher_name",),
},
),
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",
),
),
(
"transport_type",
models.CharField(
choices=[
("TGV", "TGV"),
("TER", "TER"),
("IC", "Intercités"),
("TN", "Transilien"),
],
max_length=255,
verbose_name="Transport type",
),
),
(
"service",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="dates",
to="sncfgtfs.calendar",
verbose_name="Service",
),
),
],
options={
"verbose_name": "Calendar date",
"verbose_name_plural": "Calendar dates",
"ordering": ("id",),
},
),
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(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(
on_delete=django.db.models.deletion.CASCADE,
related_name="routes",
to="sncfgtfs.agency",
verbose_name="Agency ID",
),
),
],
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(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"
),
),
(
"parent_station",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="children",
to="sncfgtfs.stop",
verbose_name="Parent station",
),
),
],
options={
"verbose_name": "Stop",
"verbose_name_plural": "Stops",
"ordering": ("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="sncfgtfs.stop",
verbose_name="From stop",
),
),
(
"to_stop",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="transfers_to",
to="sncfgtfs.stop",
verbose_name="To stop",
),
),
],
options={
"verbose_name": "Transfer",
"verbose_name_plural": "Transfers",
"ordering": ("id",),
},
),
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",
),
),
(
"route",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="trips",
to="sncfgtfs.route",
verbose_name="Route",
),
),
(
"service",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="trips",
to="sncfgtfs.calendar",
verbose_name="Service",
),
),
],
options={
"verbose_name": "Trip",
"verbose_name_plural": "Trips",
"ordering": ("id",),
},
),
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"
),
),
(
"stop",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_times",
to="sncfgtfs.stop",
verbose_name="Stop ID",
),
),
(
"trip",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_times",
to="sncfgtfs.trip",
verbose_name="Trip",
),
),
],
options={
"verbose_name": "Stop time",
"verbose_name_plural": "Stop times",
},
),
]

View File

@ -1,105 +0,0 @@
# Generated by Django 5.0.1 on 2024-02-04 19:58
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("sncfgtfs", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="trip",
options={"verbose_name": "Trip", "verbose_name_plural": "Trips"},
),
migrations.CreateModel(
name="TripUpdate",
fields=[
(
"trip",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="update",
serialize=False,
to="sncfgtfs.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, "Skipped"),
(2, "No data"),
(3, "Unscheduled"),
],
default=0,
verbose_name="Schedule relationship",
),
),
],
options={
"verbose_name": "Trip update",
"verbose_name_plural": "Trip updates",
"ordering": ("start_date", "trip"),
"unique_together": {("trip", "start_date", "start_time")},
},
),
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="sncfgtfs.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",
),
),
(
"trip_update",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="stop_time_updates",
to="sncfgtfs.tripupdate",
verbose_name="Trip update",
),
),
],
options={
"verbose_name": "Stop time update",
"verbose_name_plural": "Stop time updates",
"ordering": ("trip_update", "stop_time"),
"unique_together": {("trip_update", "stop_time")},
},
),
]

View File

@ -1,722 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class TransportType(models.TextChoices):
TGV = "TGV", _("TGV")
TER = "TER", _("TER")
INTERCITES = "IC", _("Intercités")
TRANSILIEN = "TN", _("Transilien")
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 ScheduleRelationship(models.IntegerChoices):
SCHEDULED = 0, _("Scheduled")
SKIPPED = 1, _("Skipped")
NO_DATA = 2, _("No data")
UNSCHEDULED = 3, _("Unscheduled")
class Agency(models.Model):
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Agency ID"),
)
name = models.CharField(
max_length=255,
unique=True,
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,
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Agency")
verbose_name_plural = _("Agencies")
ordering = ("name",)
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"),
)
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,
)
@property
def stop_type(self):
train_type = self.id.split('StopPoint:OCE')[1].split('-')[0]
if train_type == "Train TER":
train_type = "TER"
elif train_type == "INTERCITES":
train_type = "INTER-CITÉS"
elif train_type == "INTERCITES de nuit":
train_type = "INTER-CITÉS de nuit"
return train_type
def __str__(self):
return f"{self.name} ({self.id})"
class Meta:
verbose_name = _("Stop")
verbose_name_plural = _("Stops")
ordering = ("id",)
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 ID"),
related_name="routes",
)
short_name = models.CharField(
max_length=255,
verbose_name=_("Route short name"),
)
long_name = models.CharField(
max_length=255,
verbose_name=_("Route long name"),
)
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,
)
def __str__(self):
return f"{self.long_name}"
class Meta:
verbose_name = _("Route")
verbose_name_plural = _("Routes")
ordering = ("id",)
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,
)
@property
def origin(self):
return self.stop_times.order_by('stop_sequence').first().stop
@property
def destination(self):
return self.stop_times.order_by('-stop_sequence').first().stop
@property
def train_type(self):
if self.service.transport_type == TransportType.TRANSILIEN:
return self.route.short_name
else:
return self.origin.stop_type
@property
def train_number(self):
if self.service.transport_type == TransportType.TRANSILIEN:
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"
def __str__(self):
return f"{self.route.long_name} - {self.id}"
class Meta:
verbose_name = _("Trip")
verbose_name_plural = _("Trips")
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.trip.route.long_name} - {self.trip_id} - {self.stop.name}"
class Meta:
verbose_name = _("Stop time")
verbose_name_plural = _("Stop times")
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"),
)
transport_type = models.CharField(
max_length=255,
verbose_name=_("Transport type"),
choices=TransportType,
)
def __str__(self):
return self.id
class Meta:
verbose_name = _("Calendar")
verbose_name_plural = _("Calendars")
ordering = ("id",)
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,
)
transport_type = models.CharField(
max_length=255,
verbose_name=_("Transport type"),
choices=TransportType,
)
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",)
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"),
)
class Meta:
verbose_name = _("Feed info")
verbose_name_plural = _("Feed infos")
ordering = ("publisher_name",)
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=ScheduleRelationship,
default=ScheduleRelationship.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",)
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=ScheduleRelationship,
default=ScheduleRelationship.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",)

View File

@ -1,6 +0,0 @@
Django>=5.0,<6.0
django-cors-headers
django-filter
djangorestframework
protobuf
requests

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import {Autocomplete, TextField} from "@mui/material"; import {Autocomplete, TextField} from "@mui/material";
import {useRef, useState} from "react"; import {useState} from "react";
function AutocompleteStop(params) { function AutocompleteStation(params) {
const [options, setOptions] = useState([]) const [options, setOptions] = useState([])
const previousController = useRef()
function onInputChange(event, value) { function onInputChange(event, value) {
if (!value) { if (!value) {
@ -11,17 +10,9 @@ function AutocompleteStop(params) {
return return
} }
if (previousController.current) fetch(`${process.env.REACT_APP_MOTIS_SERVER}/api/v1/geocode?language=fr&text=${value}`)
previousController.current.abort()
const controller = new AbortController()
const signal = controller.signal
previousController.current = controller
fetch("/api/gtfs/stop/?location_type=1&search=" + value, {signal})
.then(response => response.json()) .then(response => response.json())
.then(data => data.results)
.then(setOptions) .then(setOptions)
.catch()
} }
return <> return <>
@ -29,14 +20,18 @@ function AutocompleteStop(params) {
id="stop" id="stop"
options={options} options={options}
onInputChange={onInputChange} onInputChange={onInputChange}
filterOptions={(x) => x} filterOptions={(x) => x.filter(stop => stop.type === "STOP").filter(stop => !stop.id.startsWith("node/"))}
getOptionKey={option => option.id} getOptionKey={option => option.id}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
groupBy={option => option.id.startsWith("IDFM") ? "Transilien" : "TER/TGV/Intercités"} groupBy={option => getOptionGroup(option)}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => <TextField {...params} label="Arrêt" />} renderInput={(params) => <TextField {...params} label="Arrêt" />}
{...params} /> {...params} />
</> </>
} }
export default AutocompleteStop; function getOptionGroup(option) {
return option.id.split('_')[0]
}
export default AutocompleteStation;

View File

@ -1,19 +1,19 @@
import AutocompleteStop from "./AutocompleteStop" import AutocompleteStation from "./AutocompleteStation"
import {useNavigate} from "react-router-dom" import {useNavigate} from "react-router-dom"
function Home() { function Home() {
const navigate = useNavigate() const navigate = useNavigate()
function onStationSelected(event, stop) { function onStationSelected(event, station) {
navigate(`/station/${stop.id}/`) navigate(`/station/sncf/${station.id}/`)
} }
return <> return <>
<h1>Horaires SNCF</h1> <h1>Horaires des trains</h1>
<h2> <h2>
Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées : Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées :
</h2> </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 ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB