Compare commits

...

65 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
f2b6557cb7 Update GTFS Realtime data 2024-02-04 22:20:09 +01:00
8b4761e3b5 Import GTFS Realtime Protobuf format 2024-02-03 09:45:39 +01:00
c46f85f744 Store requirements.txt 2024-02-03 09:36:49 +01:00
d7b9515750 Add stop autocomplete component in station page 2024-02-03 01:00:29 +01:00
6dfd04ae7e Autocomplete stops 2024-02-03 00:52:52 +01:00
62 changed files with 7274 additions and 8099 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
dist
build
__pycache__
*.py[cod]
*$py.class
*.swp
*.egg-info
_build
.tox
.coverage
coverage
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Translations
*.mo
*.pot
# Jupyter Notebook
.ipynb_checkpoints
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea
# VSCode project settings
.vscode
# Local data
secrets.py
settings_local.py
*.log
media/
output/
/static/
/static_files/
# dependencies
/node_modules
/.pnp
.pnp.js
# Virtualenv
.env/
env/
.venv/
venv/
db.sqlite3
db.sqlite3-journal
# testing
/coverage
node_modules/
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "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>
<html lang="fr" data-bs-theme="dark">
<html lang="fr">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
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" />

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,65 +0,0 @@
import {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";
function DateTimeSelector({date, time}) {
return <>
<Box component="form" display="flex" alignItems="center" sx={{'& .MuiTextField-root': { m: 1, width: '25ch' },}}>
<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 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,223 +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
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]
filterset_fields = '__all__'
@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,82 +0,0 @@
from django.contrib import admin
from django.utils.safestring import mark_safe
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
Transfer, FeedInfo
@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__long_name', 'service', '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',)

View File

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

View File

@ -1,492 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-27 10:44+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:65 sncfgtfs/models.py:209
msgid "Agency ID"
msgstr "ID de l'agence"
#: sncfgtfs/models.py:71
msgid "Agency name"
msgstr "Nom de l'agence"
#: sncfgtfs/models.py:75
msgid "Agency URL"
msgstr "URL de l'agence"
#: sncfgtfs/models.py:80
msgid "Agency timezone"
msgstr "Fuseau horaire de l'agence"
#: sncfgtfs/models.py:85
msgid "Agency language"
msgstr "Langue de l'agence"
#: sncfgtfs/models.py:91
msgid "Agency phone"
msgstr "Téléphone de l'agence"
#: sncfgtfs/models.py:96
msgid "Agency email"
msgstr "Adresse email de l'agence"
#: sncfgtfs/models.py:104
msgid "Agency"
msgstr "Agence"
#: sncfgtfs/models.py:105
msgid "Agencies"
msgstr "Agences"
#: sncfgtfs/models.py:112 sncfgtfs/models.py:366
msgid "Stop ID"
msgstr "ID de l'arrêt"
#: sncfgtfs/models.py:117
msgid "Stop code"
msgstr "Code de l'arrêt"
#: sncfgtfs/models.py:123
msgid "Stop name"
msgstr "Nom de l'arrêt"
#: sncfgtfs/models.py:128
msgid "Stop description"
msgstr "Description de l'arrêt"
#: sncfgtfs/models.py:133
msgid "Stop longitude"
msgstr "Longitude de l'arrêt"
#: sncfgtfs/models.py:137
msgid "Stop latitude"
msgstr "Latitude de l'arrêt"
#: sncfgtfs/models.py:142
msgid "Zone ID"
msgstr "ID de la zone"
#: sncfgtfs/models.py:146
msgid "Stop URL"
msgstr "URL de l'arrêt"
#: sncfgtfs/models.py:151
msgid "Location type"
msgstr "Type de localisation"
#: sncfgtfs/models.py:160
msgid "Parent station"
msgstr "Gare parente"
#: sncfgtfs/models.py:168
msgid "Stop timezone"
msgstr "Fuseau horaire de l'arrêt"
#: sncfgtfs/models.py:174
msgid "Level ID"
msgstr "ID du niveau"
#: sncfgtfs/models.py:179
msgid "Wheelchair boarding"
msgstr "Embarquement en fauteuil roulant"
#: sncfgtfs/models.py:187
msgid "Platform code"
msgstr "Code du quai"
#: sncfgtfs/models.py:195
msgid "Stop"
msgstr "Arrêt"
#: sncfgtfs/models.py:196
msgid "Stops"
msgstr "Arrêts"
#: sncfgtfs/models.py:203 sncfgtfs/models.py:340 sncfgtfs/models.py:469
#: sncfgtfs/models.py:506
msgid "ID"
msgstr "Identifiant"
#: sncfgtfs/models.py:215
msgid "Route short name"
msgstr "Nom court de la ligne"
#: sncfgtfs/models.py:220
msgid "Route long name"
msgstr "Nom long de la ligne"
#: sncfgtfs/models.py:225
msgid "Route description"
msgstr "Description de la ligne"
#: sncfgtfs/models.py:230
msgid "Route type"
msgstr "Type de ligne"
#: sncfgtfs/models.py:235
msgid "Route URL"
msgstr "URL de la ligne"
#: sncfgtfs/models.py:241
msgid "Route color"
msgstr "Couleur de la ligne"
#: sncfgtfs/models.py:247
msgid "Route text color"
msgstr "Couleur du texte de la ligne"
#: sncfgtfs/models.py:255 sncfgtfs/models.py:269
msgid "Route"
msgstr "Ligne"
#: sncfgtfs/models.py:256
msgid "Routes"
msgstr "Lignes"
#: sncfgtfs/models.py:263
msgid "Trip ID"
msgstr "ID du trajet"
#: sncfgtfs/models.py:276 sncfgtfs/models.py:475
msgid "Service"
msgstr "Service"
#: sncfgtfs/models.py:282
msgid "Trip headsign"
msgstr "Destination du trajet"
#: sncfgtfs/models.py:288
msgid "Trip short name"
msgstr "Nom court du trajet"
#: sncfgtfs/models.py:293
msgid "Direction"
msgstr "Direction"
#: sncfgtfs/models.py:300
msgid "Block ID"
msgstr "ID du bloc"
#: sncfgtfs/models.py:306
msgid "Shape ID"
msgstr "ID de la forme"
#: sncfgtfs/models.py:311
msgid "Wheelchair accessible"
msgstr "Accessible en fauteuil roulant"
#: sncfgtfs/models.py:318
msgid "Bikes allowed"
msgstr "Vélos autorisés"
#: sncfgtfs/models.py:332 sncfgtfs/models.py:346
msgid "Trip"
msgstr "Trajet"
#: sncfgtfs/models.py:333
msgid "Trips"
msgstr "Trajets"
#: sncfgtfs/models.py:351
msgid "Arrival time"
msgstr "Heure d'arrivée"
#: sncfgtfs/models.py:355
msgid "Departure time"
msgstr "Heure de départ"
#: sncfgtfs/models.py:359
msgid "Arrival next day"
msgstr "Arrivée le jour suivant"
#: sncfgtfs/models.py:371
msgid "Stop sequence"
msgstr "Séquence de l'arrêt"
#: sncfgtfs/models.py:376
msgid "Stop headsign"
msgstr "Destination de l'arrêt"
#: sncfgtfs/models.py:381
msgid "Pickup type"
msgstr "Type de prise en charge"
#: sncfgtfs/models.py:388
msgid "Drop off type"
msgstr "Type de dépose"
#: sncfgtfs/models.py:395
msgid "Timepoint"
msgstr "Ponctualité"
#: sncfgtfs/models.py:404
msgid "Stop time"
msgstr "Heure d'arrêt"
#: sncfgtfs/models.py:405
msgid "Stop times"
msgstr "Heures d'arrêt"
#: sncfgtfs/models.py:412
msgid "Service ID"
msgstr "ID du service"
#: sncfgtfs/models.py:416
msgid "Monday"
msgstr "Lundi"
#: sncfgtfs/models.py:420
msgid "Tuesday"
msgstr "Mardi"
#: sncfgtfs/models.py:424
msgid "Wednesday"
msgstr "Mercredi"
#: sncfgtfs/models.py:428
msgid "Thursday"
msgstr "Jeudi"
#: sncfgtfs/models.py:432
msgid "Friday"
msgstr "Vendredi"
#: sncfgtfs/models.py:436
msgid "Saturday"
msgstr "Samedi"
#: sncfgtfs/models.py:440
msgid "Sunday"
msgstr "Dimanche"
#: sncfgtfs/models.py:444
msgid "Start date"
msgstr "Date de début"
#: sncfgtfs/models.py:448
msgid "End date"
msgstr "Date de fin"
#: sncfgtfs/models.py:453 sncfgtfs/models.py:490
msgid "Transport type"
msgstr "Type de transport"
#: sncfgtfs/models.py:461
msgid "Calendar"
msgstr "Calendrier"
#: sncfgtfs/models.py:462
msgid "Calendars"
msgstr "Calendriers"
#: sncfgtfs/models.py:480
msgid "Date"
msgstr "Date"
#: sncfgtfs/models.py:484
msgid "Exception type"
msgstr "Type d'exception"
#: sncfgtfs/models.py:498
msgid "Calendar date"
msgstr "Date du calendrier"
#: sncfgtfs/models.py:499
msgid "Calendar dates"
msgstr "Dates du calendrier"
#: sncfgtfs/models.py:512
msgid "From stop"
msgstr "Depuis l'arrêt"
#: sncfgtfs/models.py:519
msgid "To stop"
msgstr "Jusqu'à l'arrêt"
#: sncfgtfs/models.py:524
msgid "Transfer type"
msgstr "Type de correspondance"
#: sncfgtfs/models.py:530
msgid "Minimum transfer time"
msgstr "Temps de correspondance minimum"
#: sncfgtfs/models.py:535
msgid "Transfer"
msgstr "Correspondance"
#: sncfgtfs/models.py:536
msgid "Transfers"
msgstr "Correspondances"
#: sncfgtfs/models.py:542
msgid "Feed publisher name"
msgstr "Nom de l'éditeur du flux"
#: sncfgtfs/models.py:546
msgid "Feed publisher URL"
msgstr "URL de l'éditeur du flux"
#: sncfgtfs/models.py:551
msgid "Feed language"
msgstr "Langue du flux"
#: sncfgtfs/models.py:555
msgid "Feed start date"
msgstr "Date de début du flux"
#: sncfgtfs/models.py:559
msgid "Feed end date"
msgstr "Date de fin du flux"
#: sncfgtfs/models.py:564
msgid "Feed version"
msgstr "Version du flux"
#: sncfgtfs/models.py:568
msgid "Feed info"
msgstr "Information du flux"
#: sncfgtfs/models.py:569
msgid "Feed infos"
msgstr "Informations du flux"
#~ msgid "Route ID"
#~ msgstr "ID de la ligne"
#~ msgid "Feed ID"
#~ msgstr "ID du flux"

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,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,631 +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 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,
)
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:
train_type = self.origin.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
@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")
ordering = ("id",)
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",)

View File

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

View File

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

View File

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

20
src/Home.js Normal file
View File

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

194
src/Map.js Normal file
View File

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

90
src/Station.js Normal file
View File

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

318
src/TrainsTable.js Normal file
View File

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

165
src/TripsFilter.js Normal file
View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB