Restructurate GTFS feeds into dedicated models
This commit is contained in:
parent
820fc0cc19
commit
11949228ee
1
.gitignore
vendored
1
.gitignore
vendored
@ -35,6 +35,7 @@ coverage
|
||||
secrets.py
|
||||
settings_local.py
|
||||
*.log
|
||||
*.txt
|
||||
media/
|
||||
output/
|
||||
/static/
|
||||
|
@ -40,23 +40,23 @@ function AutocompleteStop(params) {
|
||||
}
|
||||
|
||||
function getOptionGroup(option) {
|
||||
switch (option.transport_type) {
|
||||
case "TGV":
|
||||
case "IC":
|
||||
case "TER":
|
||||
switch (option.gtfs_feed) {
|
||||
case "FR-SNCF-TGV":
|
||||
case "FR-SNCF-IC":
|
||||
case "FR-SNCF-TER":
|
||||
return "TGV/TER/Intercités"
|
||||
case "TN":
|
||||
case "FR-IDF-TN":
|
||||
return "Transilien"
|
||||
case "ES":
|
||||
case "FR-EUROSTAR":
|
||||
return "Eurostar"
|
||||
case "TI":
|
||||
case "IT-FRA-TI":
|
||||
return "Trenitalia France"
|
||||
case "RENFE":
|
||||
case "ES-RENFE":
|
||||
return "RENFE"
|
||||
case "OBB":
|
||||
case "AT-OBB":
|
||||
return "ÖBB"
|
||||
default:
|
||||
return option.transport_type
|
||||
return option.gtfs_feed
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,10 +229,10 @@ function TrainRow({train, tableType, date, time}) {
|
||||
}
|
||||
|
||||
function getTrainType(train, trip, route) {
|
||||
switch (route.transport_type) {
|
||||
case "TGV":
|
||||
case "TER":
|
||||
case "IC":
|
||||
switch (route.gtfs_feed) {
|
||||
case "FR-SNCF-TGV":
|
||||
case "FR-SNCF-IC":
|
||||
case "FR-SNCF-TER":
|
||||
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
|
||||
switch (trainType) {
|
||||
case "Train TER":
|
||||
@ -244,20 +244,20 @@ function getTrainType(train, trip, route) {
|
||||
default:
|
||||
return trainType
|
||||
}
|
||||
case "TN":
|
||||
case "FR-IDF-TN":
|
||||
return route.short_name
|
||||
case "ES":
|
||||
case "FR-EUROSTAR":
|
||||
return "Eurostar"
|
||||
case "TI":
|
||||
return "Trenitalia"
|
||||
case "RENFE":
|
||||
case "IT-FRA-TI":
|
||||
return "Trenitalia France"
|
||||
case "ES-RENFE":
|
||||
return "RENFE"
|
||||
case "OBB":
|
||||
if (trip.short_name.startsWith("NJ"))
|
||||
case "AT-OBB":
|
||||
if (trip.short_name?.startsWith("NJ"))
|
||||
return "NJ"
|
||||
return "ÖBB"
|
||||
default:
|
||||
return ""
|
||||
return trip.short_name?.split(" ")[0]
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,6 +280,7 @@ function getTrainTypeDisplay(trainType) {
|
||||
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%" />
|
||||
|
@ -11,12 +11,11 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from sncf.api.serializers import AgencySerializer, StopSerializer, RouteSerializer, TripSerializer, \
|
||||
StopTimeSerializer, CalendarSerializer, CalendarDateSerializer, TransferSerializer, \
|
||||
FeedInfoSerializer, TripUpdateSerializer, StopTimeUpdateSerializer
|
||||
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
|
||||
Transfer, FeedInfo, TripUpdate, StopTimeUpdate
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, StopTimeUpdate, \
|
||||
Transfer, Trip, TripUpdate
|
||||
|
||||
CACHE_CONTROL = cache_control(max_age=7200)
|
||||
LAST_MODIFIED = last_modified(lambda *args, **kwargs: datetime.fromisoformat(
|
||||
FeedInfo.objects.get(publisher_name="SNCF_default").version))
|
||||
LAST_MODIFIED = last_modified(lambda *args, **kwargs: GTFSFeed.objects.order_by('-last_modified').first().last_modified)
|
||||
LOOKUP_VALUE_REGEX = r"[\w.: |-]+"
|
||||
|
||||
|
||||
|
@ -1,26 +1,38 @@
|
||||
from django.contrib import admin
|
||||
from django.forms import BaseInlineFormSet
|
||||
|
||||
from sncfgtfs.models import Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, \
|
||||
Transfer, FeedInfo, StopTimeUpdate, TripUpdate
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, \
|
||||
Route, Stop, StopTime, StopTimeUpdate, Transfer, Trip, TripUpdate
|
||||
|
||||
|
||||
class LimitModelFormset(BaseInlineFormSet):
|
||||
""" Base Inline formset to limit inline Model query results. """
|
||||
def get_queryset(self):
|
||||
return super(LimitModelFormset, self).get_queryset()[:50]
|
||||
|
||||
|
||||
class CalendarDateInline(admin.TabularInline):
|
||||
model = CalendarDate
|
||||
extra = 0
|
||||
formset = LimitModelFormset
|
||||
|
||||
|
||||
class TripInline(admin.TabularInline):
|
||||
model = Trip
|
||||
extra = 0
|
||||
formset = LimitModelFormset
|
||||
autocomplete_fields = ('route', 'service',)
|
||||
show_change_link = True
|
||||
ordering = ('service',)
|
||||
readonly_fields = ('gtfs_feed',)
|
||||
|
||||
|
||||
class StopTimeInline(admin.TabularInline):
|
||||
model = StopTime
|
||||
extra = 0
|
||||
formset = LimitModelFormset
|
||||
autocomplete_fields = ('stop',)
|
||||
readonly_fields = ('id',)
|
||||
show_change_link = True
|
||||
ordering = ('stop_sequence',)
|
||||
|
||||
@ -28,47 +40,59 @@ class StopTimeInline(admin.TabularInline):
|
||||
class TripUpdateInline(admin.StackedInline):
|
||||
model = TripUpdate
|
||||
extra = 0
|
||||
formset = LimitModelFormset
|
||||
autocomplete_fields = ('trip',)
|
||||
|
||||
|
||||
class StopTimeUpdateInline(admin.StackedInline):
|
||||
model = StopTimeUpdate
|
||||
extra = 0
|
||||
formset = LimitModelFormset
|
||||
autocomplete_fields = ('trip_update', 'stop_time',)
|
||||
|
||||
|
||||
@admin.register(GTFSFeed)
|
||||
class GTFSFeedAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'code', 'country', 'last_modified',)
|
||||
list_filter = ('country', 'last_modified',)
|
||||
search_fields = ('name', 'code',)
|
||||
readonly_fields = ('code',)
|
||||
|
||||
|
||||
@admin.register(Agency)
|
||||
class AgencyAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'id', 'url', 'timezone',)
|
||||
list_display = ('name', 'id', 'url', 'timezone', 'gtfs_feed',)
|
||||
list_filter = ('gtfs_feed', 'timezone',)
|
||||
search_fields = ('name',)
|
||||
autocomplete_fields = ('gtfs_feed',)
|
||||
|
||||
|
||||
@admin.register(Stop)
|
||||
class StopAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'id', 'lat', 'lon', 'location_type',)
|
||||
list_filter = ('location_type', 'transport_type',)
|
||||
list_filter = ('location_type', 'gtfs_feed',)
|
||||
search_fields = ('name', 'id',)
|
||||
ordering = ('name',)
|
||||
autocomplete_fields = ('parent_station',)
|
||||
autocomplete_fields = ('parent_station', 'gtfs_feed',)
|
||||
|
||||
|
||||
@admin.register(Route)
|
||||
class RouteAdmin(admin.ModelAdmin):
|
||||
list_display = ('short_name', 'long_name', 'id', 'type',)
|
||||
list_filter = ('transport_type', 'type', 'agency',)
|
||||
list_display = ('__str__', 'id', 'type', 'gtfs_feed',)
|
||||
list_filter = ('gtfs_feed', 'type', 'agency',)
|
||||
search_fields = ('long_name', 'short_name', 'id',)
|
||||
ordering = ('long_name',)
|
||||
autocomplete_fields = ('agency',)
|
||||
autocomplete_fields = ('agency', 'gtfs_feed',)
|
||||
inlines = (TripInline,)
|
||||
|
||||
|
||||
@admin.register(Trip)
|
||||
class TripAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'route', 'service', 'headsign', 'direction_id',)
|
||||
list_filter = ('direction_id', 'route__transport_type',)
|
||||
list_display = ('id', 'origin_destination', 'route', 'service', 'headsign', 'direction_id',)
|
||||
list_filter = ('direction_id', 'route__gtfs_feed',)
|
||||
search_fields = ('id', 'route__id', 'route__long_name', 'service__id', 'headsign',)
|
||||
ordering = ('route', 'service',)
|
||||
autocomplete_fields = ('route', 'service',)
|
||||
autocomplete_fields = ('route', 'service', 'gtfs_feed',)
|
||||
inlines = (StopTimeInline, TripUpdateInline,)
|
||||
|
||||
|
||||
@ -76,28 +100,30 @@ class TripAdmin(admin.ModelAdmin):
|
||||
class StopTimeAdmin(admin.ModelAdmin):
|
||||
list_display = ('trip', 'stop', 'arrival_time', 'departure_time',
|
||||
'stop_sequence', 'pickup_type', 'drop_off_type',)
|
||||
list_filter = ('pickup_type', 'drop_off_type', 'trip__route__transport_type',)
|
||||
list_filter = ('pickup_type', 'drop_off_type', 'trip__route__gtfs_feed',)
|
||||
search_fields = ('trip__id', 'stop__name', 'arrival_time', 'departure_time',)
|
||||
ordering = ('trip', 'stop_sequence',)
|
||||
autocomplete_fields = ('trip', 'stop',)
|
||||
readonly_fields = ('id',)
|
||||
inlines = (StopTimeUpdateInline,)
|
||||
|
||||
|
||||
@admin.register(Calendar)
|
||||
class CalendarAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
|
||||
list_display = ('id', 'gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
|
||||
'saturday', 'sunday', 'start_date', 'end_date',)
|
||||
list_filter = ('transport_type', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
||||
list_filter = ('gtfs_feed', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
||||
'start_date', 'end_date',)
|
||||
search_fields = ('id', 'start_date', 'end_date',)
|
||||
ordering = ('transport_type', 'id',)
|
||||
autocomplete_fields = ('gtfs_feed',)
|
||||
ordering = ('gtfs_feed', 'id',)
|
||||
inlines = (CalendarDateInline, TripInline,)
|
||||
|
||||
|
||||
@admin.register(CalendarDate)
|
||||
class CalendarDateAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'service_id', 'date', 'exception_type',)
|
||||
list_filter = ('exception_type', 'date', 'service__transport_type',)
|
||||
list_filter = ('exception_type', 'date', 'service__gtfs_feed',)
|
||||
search_fields = ('id', 'date',)
|
||||
ordering = ('date', 'service_id',)
|
||||
|
||||
@ -116,6 +142,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
|
||||
'end_date', 'version',)
|
||||
search_fields = ('publisher_name', 'publisher_url', 'lang', 'start_date',
|
||||
'end_date', 'version',)
|
||||
autocomplete_fields = ('gtfs_feed',)
|
||||
ordering = ('publisher_name',)
|
||||
|
||||
|
||||
@ -123,7 +150,7 @@ class FeedInfoAdmin(admin.ModelAdmin):
|
||||
class StopTimeUpdateAdmin(admin.ModelAdmin):
|
||||
list_display = ('trip_update', 'stop_time', 'arrival_delay', 'arrival_time',
|
||||
'departure_delay', 'departure_time', 'schedule_relationship',)
|
||||
list_filter = ('schedule_relationship',)
|
||||
list_filter = ('schedule_relationship', 'trip_update__trip__gtfs_feed',)
|
||||
search_fields = ('trip_update__trip__id', 'stop_time__stop__name', 'arrival_time', 'departure_time',)
|
||||
ordering = ('trip_update', 'stop_time',)
|
||||
autocomplete_fields = ('trip_update', 'stop_time',)
|
||||
|
92
sncfgtfs/fixtures/gtfs_feeds.json
Normal file
92
sncfgtfs/fixtures/gtfs_feeds.json
Normal file
@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "FR-SNCF-TGV",
|
||||
"fields": {
|
||||
"name": "SNCF - TGV",
|
||||
"country": "FR",
|
||||
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export_gtfs_voyages.zip",
|
||||
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "FR-SNCF-IC",
|
||||
"fields": {
|
||||
"name": "SNCF - Intercités",
|
||||
"country": "FR",
|
||||
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-intercites-gtfs-last.zip",
|
||||
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "FR-SNCF-TER",
|
||||
"fields": {
|
||||
"name": "SNCF - TER",
|
||||
"country": "FR",
|
||||
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip",
|
||||
"rt_feed_url": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "FR-IDF-TN",
|
||||
"fields": {
|
||||
"name": "SNCF - Transilien",
|
||||
"country": "FR",
|
||||
"feed_url": "https://eu.ftp.opendatasoft.com/sncf/gtfs/transilien-gtfs.zip",
|
||||
"rt_feed_url": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "FR-EUROSTAR",
|
||||
"fields": {
|
||||
"name": "Eurostar",
|
||||
"country": "FR",
|
||||
"feed_url": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292",
|
||||
"rt_feed_url": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "IT-FRA-TI",
|
||||
"fields": {
|
||||
"name": "Trenitalia France",
|
||||
"country": "FR",
|
||||
"feed_url": "https://thello.axelor.com/public/gtfs/gtfs.zip",
|
||||
"rt_feed_url": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "ES-RENFE",
|
||||
"fields": {
|
||||
"name": "Renfe",
|
||||
"country": "ES",
|
||||
"feed_url": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip",
|
||||
"rt_feed_url": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "AT-ÖBB",
|
||||
"fields": {
|
||||
"name": "ÖBB",
|
||||
"country": "AT",
|
||||
"feed_url": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
|
||||
"rt_feed_url": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "sncfgtfs.gtfsfeed",
|
||||
"pk": "CH-ALL",
|
||||
"fields": {
|
||||
"name": "Transports suisses",
|
||||
"country": "CH",
|
||||
"feed_url": "https://opentransportdata.swiss/fr/dataset/timetable-2024-gtfs2020/permalink",
|
||||
"rt_feed_url": "https://api.opentransportdata.swiss/gtfsrt2020"
|
||||
}
|
||||
}
|
||||
]
|
@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-10 19:57+0100\n"
|
||||
"POT-Creation-Date: 2024-05-09 19:27+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Emmy D'Anello <ynerant@emy.lu>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -12,555 +12,808 @@ msgstr ""
|
||||
"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:10
|
||||
msgid "Eurostar"
|
||||
msgstr "Eurostar"
|
||||
|
||||
#: sncfgtfs/models.py:11
|
||||
msgid "Trenitalia"
|
||||
msgstr "Trenitalia"
|
||||
msgid "Albania"
|
||||
msgstr "Albanie"
|
||||
|
||||
#: sncfgtfs/models.py:12
|
||||
msgid "Renfe"
|
||||
msgstr "Renfe"
|
||||
msgid "Andorra"
|
||||
msgstr "Andorre"
|
||||
|
||||
#: sncfgtfs/models.py:13
|
||||
msgid "ÖBB"
|
||||
msgstr "ÖBB"
|
||||
msgid "Armenia"
|
||||
msgstr "Arménie"
|
||||
|
||||
#: sncfgtfs/models.py:14
|
||||
msgid "Austria"
|
||||
msgstr "Autriche"
|
||||
|
||||
#: sncfgtfs/models.py:15
|
||||
msgid "Azerbaijan"
|
||||
msgstr "Azerbaijan"
|
||||
|
||||
#: sncfgtfs/models.py:16
|
||||
msgid "Belgium"
|
||||
msgstr "Belgique"
|
||||
|
||||
#: sncfgtfs/models.py:17
|
||||
msgid "Bosnia and Herzegovina"
|
||||
msgstr " Bosnie-Herzégovine"
|
||||
|
||||
#: sncfgtfs/models.py:18
|
||||
msgid "Bulgaria"
|
||||
msgstr "Bulgarie"
|
||||
|
||||
#: sncfgtfs/models.py:19
|
||||
msgid "Croatia"
|
||||
msgstr "Croatie"
|
||||
|
||||
#: sncfgtfs/models.py:20
|
||||
msgid "Cyprus"
|
||||
msgstr "Chypre"
|
||||
|
||||
#: sncfgtfs/models.py:21
|
||||
msgid "Czech Republic"
|
||||
msgstr "République Tchèque"
|
||||
|
||||
#: sncfgtfs/models.py:22
|
||||
msgid "Denmark"
|
||||
msgstr "Danemark"
|
||||
|
||||
#: sncfgtfs/models.py:23
|
||||
msgid "Estonia"
|
||||
msgstr "Estonie"
|
||||
|
||||
#: sncfgtfs/models.py:24
|
||||
msgid "Finland"
|
||||
msgstr "Finlande"
|
||||
|
||||
#: sncfgtfs/models.py:25
|
||||
msgid "France"
|
||||
msgstr "France"
|
||||
|
||||
#: sncfgtfs/models.py:26
|
||||
msgid "Georgia"
|
||||
msgstr "Géorgie"
|
||||
|
||||
#: sncfgtfs/models.py:27
|
||||
msgid "Germany"
|
||||
msgstr "Allemagne"
|
||||
|
||||
#: sncfgtfs/models.py:28
|
||||
msgid "Greece"
|
||||
msgstr "Grèce"
|
||||
|
||||
#: sncfgtfs/models.py:29
|
||||
msgid "Hungary"
|
||||
msgstr "Hongrie"
|
||||
|
||||
#: sncfgtfs/models.py:30
|
||||
msgid "Iceland"
|
||||
msgstr "Islande"
|
||||
|
||||
#: sncfgtfs/models.py:31
|
||||
msgid "Ireland"
|
||||
msgstr "Irlande"
|
||||
|
||||
#: sncfgtfs/models.py:32
|
||||
msgid "Italy"
|
||||
msgstr "Italie"
|
||||
|
||||
#: sncfgtfs/models.py:33
|
||||
msgid "Latvia"
|
||||
msgstr "Lettonie"
|
||||
|
||||
#: sncfgtfs/models.py:34
|
||||
msgid "Liechtenstein"
|
||||
msgstr "Liechtenstein"
|
||||
|
||||
#: sncfgtfs/models.py:35
|
||||
msgid "Lithuania"
|
||||
msgstr "Lituanie"
|
||||
|
||||
#: sncfgtfs/models.py:36
|
||||
msgid "Luxembourg"
|
||||
msgstr "Luxembourg"
|
||||
|
||||
#: sncfgtfs/models.py:37
|
||||
msgid "Malta"
|
||||
msgstr "Malte"
|
||||
|
||||
#: sncfgtfs/models.py:38
|
||||
msgid "Moldova"
|
||||
msgstr "Moldavie"
|
||||
|
||||
#: sncfgtfs/models.py:39
|
||||
msgid "Monaco"
|
||||
msgstr "Monaco"
|
||||
|
||||
#: sncfgtfs/models.py:40
|
||||
msgid "Montenegro"
|
||||
msgstr "Monténégro"
|
||||
|
||||
#: sncfgtfs/models.py:41
|
||||
msgid "Netherlands"
|
||||
msgstr "Pays-Bas"
|
||||
|
||||
#: sncfgtfs/models.py:42
|
||||
msgid "North Macedonia"
|
||||
msgstr "Macédoine du Nord"
|
||||
|
||||
#: sncfgtfs/models.py:43
|
||||
msgid "Norway"
|
||||
msgstr "Norvège"
|
||||
|
||||
#: sncfgtfs/models.py:44
|
||||
msgid "Poland"
|
||||
msgstr "Pologne"
|
||||
|
||||
#: sncfgtfs/models.py:45
|
||||
msgid "Portugal"
|
||||
msgstr "Portugal"
|
||||
|
||||
#: sncfgtfs/models.py:46
|
||||
msgid "Romania"
|
||||
msgstr "Roumanie"
|
||||
|
||||
#: sncfgtfs/models.py:47
|
||||
msgid "San Marino"
|
||||
msgstr "Saint-Marin"
|
||||
|
||||
#: sncfgtfs/models.py:48
|
||||
msgid "Serbia"
|
||||
msgstr "Serbie"
|
||||
|
||||
#: sncfgtfs/models.py:49
|
||||
msgid "Slovakia"
|
||||
msgstr "Slovaquie"
|
||||
|
||||
#: sncfgtfs/models.py:50
|
||||
msgid "Slovenia"
|
||||
msgstr "Slovénie"
|
||||
|
||||
#: sncfgtfs/models.py:51
|
||||
msgid "Spain"
|
||||
msgstr "Espagne"
|
||||
|
||||
#: sncfgtfs/models.py:52
|
||||
msgid "Sweden"
|
||||
msgstr "Suède"
|
||||
|
||||
#: sncfgtfs/models.py:53
|
||||
msgid "Switzerland"
|
||||
msgstr "Suisse"
|
||||
|
||||
#: sncfgtfs/models.py:54
|
||||
msgid "Turkey"
|
||||
msgstr "Turquie"
|
||||
|
||||
#: sncfgtfs/models.py:55
|
||||
msgid "United Kingdom"
|
||||
msgstr "Royaume-Uni"
|
||||
|
||||
#: sncfgtfs/models.py:56
|
||||
msgid "Ukraine"
|
||||
msgstr "Ukraine"
|
||||
|
||||
#: sncfgtfs/models.py:60
|
||||
msgid "Stop/platform"
|
||||
msgstr "Arrêt / quai"
|
||||
|
||||
#: sncfgtfs/models.py:18
|
||||
#: sncfgtfs/models.py:61
|
||||
msgid "Station"
|
||||
msgstr "Gare"
|
||||
|
||||
#: sncfgtfs/models.py:19
|
||||
#: sncfgtfs/models.py:62
|
||||
msgid "Entrance/exit"
|
||||
msgstr "Entrée / sortie"
|
||||
|
||||
#: sncfgtfs/models.py:20
|
||||
#: sncfgtfs/models.py:63
|
||||
msgid "Generic node"
|
||||
msgstr "Nœud générique"
|
||||
|
||||
#: sncfgtfs/models.py:21
|
||||
#: sncfgtfs/models.py:64
|
||||
msgid "Boarding area"
|
||||
msgstr "Zone d'embarquement"
|
||||
|
||||
#: sncfgtfs/models.py:25
|
||||
#: sncfgtfs/models.py:68
|
||||
msgid "No information"
|
||||
msgstr "Pas d'information"
|
||||
|
||||
#: sncfgtfs/models.py:26
|
||||
#: sncfgtfs/models.py:69
|
||||
msgid "Possible"
|
||||
msgstr "Possible"
|
||||
|
||||
#: sncfgtfs/models.py:27 sncfgtfs/models.py:57
|
||||
#: sncfgtfs/models.py:70 sncfgtfs/models.py:100
|
||||
msgid "Not possible"
|
||||
msgstr "Impossible"
|
||||
|
||||
#: sncfgtfs/models.py:31
|
||||
#: sncfgtfs/models.py:74
|
||||
msgid "Regular"
|
||||
msgstr "Régulier"
|
||||
|
||||
#: sncfgtfs/models.py:32
|
||||
#: sncfgtfs/models.py:75
|
||||
msgid "None"
|
||||
msgstr "Aucun"
|
||||
|
||||
#: sncfgtfs/models.py:33
|
||||
#: sncfgtfs/models.py:76
|
||||
msgid "Must phone agency"
|
||||
msgstr "Doit téléphoner à l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:34
|
||||
#: sncfgtfs/models.py:77
|
||||
msgid "Must coordinate with driver"
|
||||
msgstr "Doit se coordonner avec læ conducteurice"
|
||||
|
||||
#: sncfgtfs/models.py:38
|
||||
#: sncfgtfs/models.py:81
|
||||
msgid "Tram"
|
||||
msgstr "Tram"
|
||||
|
||||
#: sncfgtfs/models.py:39
|
||||
#: sncfgtfs/models.py:82
|
||||
msgid "Metro"
|
||||
msgstr "Métro"
|
||||
|
||||
#: sncfgtfs/models.py:40
|
||||
#: sncfgtfs/models.py:83
|
||||
msgid "Rail"
|
||||
msgstr "Rail"
|
||||
|
||||
#: sncfgtfs/models.py:41
|
||||
#: sncfgtfs/models.py:84
|
||||
msgid "Bus"
|
||||
msgstr "Bus"
|
||||
|
||||
#: sncfgtfs/models.py:42
|
||||
#: sncfgtfs/models.py:85
|
||||
msgid "Ferry"
|
||||
msgstr "Ferry"
|
||||
|
||||
#: sncfgtfs/models.py:43
|
||||
#: sncfgtfs/models.py:86
|
||||
msgid "Cable car"
|
||||
msgstr "Câble"
|
||||
|
||||
#: sncfgtfs/models.py:44
|
||||
#: sncfgtfs/models.py:87
|
||||
msgid "Gondola"
|
||||
msgstr "Gondole"
|
||||
|
||||
#: sncfgtfs/models.py:45
|
||||
#: sncfgtfs/models.py:88
|
||||
msgid "Funicular"
|
||||
msgstr "Funiculaire"
|
||||
|
||||
#: sncfgtfs/models.py:49
|
||||
#: sncfgtfs/models.py:92
|
||||
msgid "Outbound"
|
||||
msgstr "Vers l'extérieur"
|
||||
|
||||
#: sncfgtfs/models.py:50
|
||||
#: sncfgtfs/models.py:93
|
||||
msgid "Inbound"
|
||||
msgstr "Vers l'intérieur"
|
||||
|
||||
#: sncfgtfs/models.py:54
|
||||
#: sncfgtfs/models.py:97
|
||||
msgid "Recommended"
|
||||
msgstr "Recommandé"
|
||||
|
||||
#: sncfgtfs/models.py:55
|
||||
#: sncfgtfs/models.py:98
|
||||
msgid "Timed"
|
||||
msgstr "Correspondance programmée"
|
||||
|
||||
#: sncfgtfs/models.py:56
|
||||
#: sncfgtfs/models.py:99
|
||||
msgid "Minimum time"
|
||||
msgstr "Temps de correspondance minimum requis"
|
||||
|
||||
#: sncfgtfs/models.py:61 sncfgtfs/models.py:67
|
||||
#: sncfgtfs/models.py:104 sncfgtfs/models.py:110
|
||||
msgid "Added"
|
||||
msgstr "Ajouté"
|
||||
|
||||
#: sncfgtfs/models.py:62
|
||||
#: sncfgtfs/models.py:105
|
||||
msgid "Removed"
|
||||
msgstr "Supprimé"
|
||||
|
||||
#: sncfgtfs/models.py:66 sncfgtfs/models.py:76
|
||||
#: sncfgtfs/models.py:109 sncfgtfs/models.py:119
|
||||
msgid "Scheduled"
|
||||
msgstr "Planifié"
|
||||
|
||||
#: sncfgtfs/models.py:68 sncfgtfs/models.py:79
|
||||
#: sncfgtfs/models.py:111 sncfgtfs/models.py:122
|
||||
msgid "Unscheduled"
|
||||
msgstr "Non planifié"
|
||||
|
||||
#: sncfgtfs/models.py:69
|
||||
#: sncfgtfs/models.py:112
|
||||
msgid "Canceled"
|
||||
msgstr "Annulé"
|
||||
|
||||
#: sncfgtfs/models.py:70
|
||||
#: sncfgtfs/models.py:113
|
||||
msgid "Replacement"
|
||||
msgstr "Remplacé"
|
||||
|
||||
#: sncfgtfs/models.py:71
|
||||
#: sncfgtfs/models.py:114
|
||||
msgid "Duplicated"
|
||||
msgstr "Dupliqué"
|
||||
|
||||
#: sncfgtfs/models.py:72
|
||||
#: sncfgtfs/models.py:115
|
||||
msgid "Deleted"
|
||||
msgstr "Supprimé"
|
||||
|
||||
#: sncfgtfs/models.py:77
|
||||
#: sncfgtfs/models.py:120
|
||||
msgid "Skipped"
|
||||
msgstr "Sauté"
|
||||
|
||||
#: sncfgtfs/models.py:78
|
||||
#: sncfgtfs/models.py:121
|
||||
msgid "No data"
|
||||
msgstr "Pas de données"
|
||||
|
||||
#: sncfgtfs/models.py:86
|
||||
#: sncfgtfs/models.py:129
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: sncfgtfs/models.py:130
|
||||
msgid "Unique code of the feed."
|
||||
msgstr "Code unique du flux."
|
||||
|
||||
#: sncfgtfs/models.py:135
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: sncfgtfs/models.py:137
|
||||
msgid "Full name that describes the feed."
|
||||
msgstr "Nom complet qui décrit le flux."
|
||||
|
||||
#: sncfgtfs/models.py:142
|
||||
msgid "country"
|
||||
msgstr "pays"
|
||||
|
||||
#: sncfgtfs/models.py:147
|
||||
msgid "feed URL"
|
||||
msgstr "URL du flux"
|
||||
|
||||
#: sncfgtfs/models.py:148
|
||||
msgid ""
|
||||
"URL to download the GTFS feed. Must point to a ZIP archive. See https://gtfs."
|
||||
"org/schedule/ for more information."
|
||||
msgstr ""
|
||||
"URL où télécharger le flux GTFS. Doit pointer vers une archive ZIP. Voir "
|
||||
"https://gtfs.org/fr/schedule/ pour plus d'informations."
|
||||
|
||||
#: sncfgtfs/models.py:153
|
||||
msgid "realtime feed URL"
|
||||
msgstr "URL du flux temps réel"
|
||||
|
||||
#: sncfgtfs/models.py:156
|
||||
msgid ""
|
||||
"URL to download the GTFS-Realtime feed, in the GTFS-RT format. See https://"
|
||||
"gtfs.org/realtime/ for more information."
|
||||
msgstr ""
|
||||
"URL où télécharger le flux GTFS-Temps réel, au format GTFS-RT. Voir https://"
|
||||
"gtfs.org/fr/realtime/ pour plus d'informations."
|
||||
|
||||
#: sncfgtfs/models.py:161
|
||||
msgid "last modified date"
|
||||
msgstr "Date de dernière modification"
|
||||
|
||||
#: sncfgtfs/models.py:168
|
||||
msgid "ETag"
|
||||
msgstr "ETag"
|
||||
|
||||
#: sncfgtfs/models.py:171
|
||||
msgid ""
|
||||
"If applicable, corresponds to the tag of the last downloaded file. If it is "
|
||||
"not modified, the file is the same."
|
||||
msgstr ""
|
||||
"Si applicable, correspond au tag du dernier fichier téléchargé. S'il n'est "
|
||||
"pas modifié, le fichier est considéré comme identique."
|
||||
|
||||
#: sncfgtfs/models.py:179 sncfgtfs/models.py:226 sncfgtfs/models.py:326
|
||||
#: sncfgtfs/models.py:405 sncfgtfs/models.py:486 sncfgtfs/models.py:696
|
||||
#: sncfgtfs/models.py:811
|
||||
msgid "GTFS feed"
|
||||
msgstr "flux GTFS"
|
||||
|
||||
#: sncfgtfs/models.py:180
|
||||
msgid "GTFS feeds"
|
||||
msgstr "flux GTFS"
|
||||
|
||||
#: sncfgtfs/models.py:189
|
||||
msgid "Agency ID"
|
||||
msgstr "ID de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:91
|
||||
#: sncfgtfs/models.py:194
|
||||
msgid "Agency name"
|
||||
msgstr "Nom de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:95
|
||||
#: sncfgtfs/models.py:198
|
||||
msgid "Agency URL"
|
||||
msgstr "URL de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:100
|
||||
#: sncfgtfs/models.py:203
|
||||
msgid "Agency timezone"
|
||||
msgstr "Fuseau horaire de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:105
|
||||
#: sncfgtfs/models.py:208
|
||||
msgid "Agency language"
|
||||
msgstr "Langue de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:111
|
||||
#: sncfgtfs/models.py:214
|
||||
msgid "Agency phone"
|
||||
msgstr "Téléphone de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:116
|
||||
#: sncfgtfs/models.py:219
|
||||
msgid "Agency email"
|
||||
msgstr "Adresse email de l'agence"
|
||||
|
||||
#: sncfgtfs/models.py:124 sncfgtfs/models.py:242
|
||||
#: sncfgtfs/models.py:233 sncfgtfs/models.py:356
|
||||
msgid "Agency"
|
||||
msgstr "Agence"
|
||||
|
||||
#: sncfgtfs/models.py:125
|
||||
#: sncfgtfs/models.py:234
|
||||
msgid "Agencies"
|
||||
msgstr "Agences"
|
||||
|
||||
#: sncfgtfs/models.py:133 sncfgtfs/models.py:459
|
||||
#: sncfgtfs/models.py:243 sncfgtfs/models.py:593
|
||||
msgid "Stop ID"
|
||||
msgstr "ID de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:138
|
||||
#: sncfgtfs/models.py:248
|
||||
msgid "Stop code"
|
||||
msgstr "Code de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:144
|
||||
#: sncfgtfs/models.py:254
|
||||
msgid "Stop name"
|
||||
msgstr "Nom de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:149
|
||||
#: sncfgtfs/models.py:259
|
||||
msgid "Stop description"
|
||||
msgstr "Description de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:154
|
||||
#: sncfgtfs/models.py:264
|
||||
msgid "Stop longitude"
|
||||
msgstr "Longitude de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:158
|
||||
#: sncfgtfs/models.py:268
|
||||
msgid "Stop latitude"
|
||||
msgstr "Latitude de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:163
|
||||
#: sncfgtfs/models.py:273
|
||||
msgid "Zone ID"
|
||||
msgstr "ID de la zone"
|
||||
|
||||
#: sncfgtfs/models.py:167
|
||||
#: sncfgtfs/models.py:278
|
||||
msgid "Stop URL"
|
||||
msgstr "URL de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:172
|
||||
#: sncfgtfs/models.py:283
|
||||
msgid "Location type"
|
||||
msgstr "Type de localisation"
|
||||
|
||||
#: sncfgtfs/models.py:181
|
||||
#: sncfgtfs/models.py:292
|
||||
msgid "Parent station"
|
||||
msgstr "Gare parente"
|
||||
|
||||
#: sncfgtfs/models.py:189
|
||||
#: sncfgtfs/models.py:300
|
||||
msgid "Stop timezone"
|
||||
msgstr "Fuseau horaire de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:195
|
||||
#: sncfgtfs/models.py:306
|
||||
msgid "Level ID"
|
||||
msgstr "ID du niveau"
|
||||
|
||||
#: sncfgtfs/models.py:200
|
||||
#: sncfgtfs/models.py:311
|
||||
msgid "Wheelchair boarding"
|
||||
msgstr "Embarquement en fauteuil roulant"
|
||||
|
||||
#: sncfgtfs/models.py:208
|
||||
#: sncfgtfs/models.py:319
|
||||
msgid "Platform code"
|
||||
msgstr "Code du quai"
|
||||
|
||||
#: sncfgtfs/models.py:214 sncfgtfs/models.py:286 sncfgtfs/models.py:560
|
||||
msgid "Transport type"
|
||||
msgstr "Type de transport"
|
||||
|
||||
#: sncfgtfs/models.py:227
|
||||
#: sncfgtfs/models.py:338
|
||||
msgid "Stop"
|
||||
msgstr "Arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:228
|
||||
#: sncfgtfs/models.py:339
|
||||
msgid "Stops"
|
||||
msgstr "Arrêts"
|
||||
|
||||
#: sncfgtfs/models.py:236 sncfgtfs/models.py:438 sncfgtfs/models.py:577
|
||||
#: sncfgtfs/models.py:609
|
||||
#: sncfgtfs/models.py:350 sncfgtfs/models.py:572 sncfgtfs/models.py:713
|
||||
#: sncfgtfs/models.py:746
|
||||
msgid "ID"
|
||||
msgstr "Identifiant"
|
||||
|
||||
#: sncfgtfs/models.py:248
|
||||
#: sncfgtfs/models.py:365
|
||||
msgid "Route short name"
|
||||
msgstr "Nom court de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:253
|
||||
#: sncfgtfs/models.py:370
|
||||
msgid "Route long name"
|
||||
msgstr "Nom long de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:258
|
||||
#: sncfgtfs/models.py:376
|
||||
msgid "Route description"
|
||||
msgstr "Description de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:263
|
||||
#: sncfgtfs/models.py:381
|
||||
msgid "Route type"
|
||||
msgstr "Type de ligne"
|
||||
|
||||
#: sncfgtfs/models.py:268
|
||||
#: sncfgtfs/models.py:386
|
||||
msgid "Route URL"
|
||||
msgstr "URL de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:274
|
||||
#: sncfgtfs/models.py:392
|
||||
msgid "Route color"
|
||||
msgstr "Couleur de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:280
|
||||
#: sncfgtfs/models.py:398
|
||||
msgid "Route text color"
|
||||
msgstr "Couleur du texte de la ligne"
|
||||
|
||||
#: sncfgtfs/models.py:294 sncfgtfs/models.py:309
|
||||
#: sncfgtfs/models.py:412 sncfgtfs/models.py:428
|
||||
msgid "Route"
|
||||
msgstr "Ligne"
|
||||
|
||||
#: sncfgtfs/models.py:295
|
||||
#: sncfgtfs/models.py:413
|
||||
msgid "Routes"
|
||||
msgstr "Lignes"
|
||||
|
||||
#: sncfgtfs/models.py:303
|
||||
#: sncfgtfs/models.py:422
|
||||
msgid "Trip ID"
|
||||
msgstr "ID du trajet"
|
||||
|
||||
#: sncfgtfs/models.py:316 sncfgtfs/models.py:583
|
||||
#: sncfgtfs/models.py:435 sncfgtfs/models.py:719
|
||||
msgid "Service"
|
||||
msgstr "Service"
|
||||
|
||||
#: sncfgtfs/models.py:322
|
||||
#: sncfgtfs/models.py:441
|
||||
msgid "Trip headsign"
|
||||
msgstr "Destination du trajet"
|
||||
|
||||
#: sncfgtfs/models.py:328
|
||||
#: sncfgtfs/models.py:447
|
||||
msgid "Trip short name"
|
||||
msgstr "Nom court du trajet"
|
||||
|
||||
#: sncfgtfs/models.py:333
|
||||
#: sncfgtfs/models.py:452
|
||||
msgid "Direction"
|
||||
msgstr "Direction"
|
||||
|
||||
#: sncfgtfs/models.py:340
|
||||
#: sncfgtfs/models.py:459
|
||||
msgid "Block ID"
|
||||
msgstr "ID du bloc"
|
||||
|
||||
#: sncfgtfs/models.py:346
|
||||
#: sncfgtfs/models.py:465
|
||||
msgid "Shape ID"
|
||||
msgstr "ID de la forme"
|
||||
|
||||
#: sncfgtfs/models.py:351
|
||||
#: sncfgtfs/models.py:470
|
||||
msgid "Wheelchair accessible"
|
||||
msgstr "Accessible en fauteuil roulant"
|
||||
|
||||
#: sncfgtfs/models.py:358
|
||||
#: sncfgtfs/models.py:477
|
||||
msgid "Bikes allowed"
|
||||
msgstr "Vélos autorisés"
|
||||
|
||||
#: sncfgtfs/models.py:365
|
||||
msgid "Last update"
|
||||
msgstr "Dernière mise à jour"
|
||||
#: sncfgtfs/models.py:500 sncfgtfs/models.py:509 sncfgtfs/models.py:552
|
||||
#: sncfgtfs/models.py:554
|
||||
msgid "Unknown"
|
||||
msgstr "Inconnu"
|
||||
|
||||
#: sncfgtfs/models.py:430 sncfgtfs/models.py:444 sncfgtfs/models.py:681
|
||||
#: sncfgtfs/models.py:557
|
||||
msgid "Origin → Destination"
|
||||
msgstr "Origine → Destination"
|
||||
|
||||
#: sncfgtfs/models.py:563 sncfgtfs/models.py:578 sncfgtfs/models.py:825
|
||||
msgid "Trip"
|
||||
msgstr "Trajet"
|
||||
|
||||
#: sncfgtfs/models.py:431
|
||||
#: sncfgtfs/models.py:564
|
||||
msgid "Trips"
|
||||
msgstr "Trajets"
|
||||
|
||||
#: sncfgtfs/models.py:449 sncfgtfs/models.py:731
|
||||
#: sncfgtfs/models.py:583 sncfgtfs/models.py:876
|
||||
msgid "Arrival time"
|
||||
msgstr "Heure d'arrivée"
|
||||
|
||||
#: sncfgtfs/models.py:453 sncfgtfs/models.py:739
|
||||
#: sncfgtfs/models.py:587 sncfgtfs/models.py:884
|
||||
msgid "Departure time"
|
||||
msgstr "Heure de départ"
|
||||
|
||||
#: sncfgtfs/models.py:464
|
||||
#: sncfgtfs/models.py:598
|
||||
msgid "Stop sequence"
|
||||
msgstr "Séquence de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:469
|
||||
#: sncfgtfs/models.py:603
|
||||
msgid "Stop headsign"
|
||||
msgstr "Destination de l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:474
|
||||
#: sncfgtfs/models.py:608
|
||||
msgid "Pickup type"
|
||||
msgstr "Type de prise en charge"
|
||||
|
||||
#: sncfgtfs/models.py:481
|
||||
#: sncfgtfs/models.py:615
|
||||
msgid "Drop off type"
|
||||
msgstr "Type de dépose"
|
||||
|
||||
#: sncfgtfs/models.py:488
|
||||
#: sncfgtfs/models.py:622
|
||||
msgid "Timepoint"
|
||||
msgstr "Ponctualité"
|
||||
|
||||
#: sncfgtfs/models.py:511 sncfgtfs/models.py:721
|
||||
#: sncfgtfs/models.py:645 sncfgtfs/models.py:866
|
||||
msgid "Stop time"
|
||||
msgstr "Heure d'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:512
|
||||
#: sncfgtfs/models.py:646
|
||||
msgid "Stop times"
|
||||
msgstr "Heures d'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:519
|
||||
#: sncfgtfs/models.py:654
|
||||
msgid "Service ID"
|
||||
msgstr "ID du service"
|
||||
|
||||
#: sncfgtfs/models.py:523
|
||||
#: sncfgtfs/models.py:658
|
||||
msgid "Monday"
|
||||
msgstr "Lundi"
|
||||
|
||||
#: sncfgtfs/models.py:527
|
||||
#: sncfgtfs/models.py:662
|
||||
msgid "Tuesday"
|
||||
msgstr "Mardi"
|
||||
|
||||
#: sncfgtfs/models.py:531
|
||||
#: sncfgtfs/models.py:666
|
||||
msgid "Wednesday"
|
||||
msgstr "Mercredi"
|
||||
|
||||
#: sncfgtfs/models.py:535
|
||||
#: sncfgtfs/models.py:670
|
||||
msgid "Thursday"
|
||||
msgstr "Jeudi"
|
||||
|
||||
#: sncfgtfs/models.py:539
|
||||
#: sncfgtfs/models.py:674
|
||||
msgid "Friday"
|
||||
msgstr "Vendredi"
|
||||
|
||||
#: sncfgtfs/models.py:543
|
||||
#: sncfgtfs/models.py:678
|
||||
msgid "Saturday"
|
||||
msgstr "Samedi"
|
||||
|
||||
#: sncfgtfs/models.py:547
|
||||
#: sncfgtfs/models.py:682
|
||||
msgid "Sunday"
|
||||
msgstr "Dimanche"
|
||||
|
||||
#: sncfgtfs/models.py:551 sncfgtfs/models.py:687
|
||||
#: sncfgtfs/models.py:686 sncfgtfs/models.py:831
|
||||
msgid "Start date"
|
||||
msgstr "Date de début"
|
||||
|
||||
#: sncfgtfs/models.py:555
|
||||
#: sncfgtfs/models.py:690
|
||||
msgid "End date"
|
||||
msgstr "Date de fin"
|
||||
|
||||
#: sncfgtfs/models.py:568
|
||||
#: sncfgtfs/models.py:703
|
||||
msgid "Calendar"
|
||||
msgstr "Calendrier"
|
||||
|
||||
#: sncfgtfs/models.py:569
|
||||
#: sncfgtfs/models.py:704
|
||||
msgid "Calendars"
|
||||
msgstr "Calendriers"
|
||||
|
||||
#: sncfgtfs/models.py:588
|
||||
#: sncfgtfs/models.py:724
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
#: sncfgtfs/models.py:592
|
||||
#: sncfgtfs/models.py:728
|
||||
msgid "Exception type"
|
||||
msgstr "Type d'exception"
|
||||
|
||||
#: sncfgtfs/models.py:600
|
||||
#: sncfgtfs/models.py:736
|
||||
msgid "Calendar date"
|
||||
msgstr "Date du calendrier"
|
||||
|
||||
#: sncfgtfs/models.py:601
|
||||
#: sncfgtfs/models.py:737
|
||||
msgid "Calendar dates"
|
||||
msgstr "Dates du calendrier"
|
||||
|
||||
#: sncfgtfs/models.py:615
|
||||
#: sncfgtfs/models.py:752
|
||||
msgid "From stop"
|
||||
msgstr "Depuis l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:622
|
||||
#: sncfgtfs/models.py:759
|
||||
msgid "To stop"
|
||||
msgstr "Jusqu'à l'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:627
|
||||
#: sncfgtfs/models.py:764
|
||||
msgid "Transfer type"
|
||||
msgstr "Type de correspondance"
|
||||
|
||||
#: sncfgtfs/models.py:633
|
||||
#: sncfgtfs/models.py:770
|
||||
msgid "Minimum transfer time"
|
||||
msgstr "Temps de correspondance minimum"
|
||||
|
||||
#: sncfgtfs/models.py:638
|
||||
#: sncfgtfs/models.py:775
|
||||
msgid "Transfer"
|
||||
msgstr "Correspondance"
|
||||
|
||||
#: sncfgtfs/models.py:639
|
||||
#: sncfgtfs/models.py:776
|
||||
msgid "Transfers"
|
||||
msgstr "Correspondances"
|
||||
|
||||
#: sncfgtfs/models.py:646
|
||||
#: sncfgtfs/models.py:783
|
||||
msgid "Feed publisher name"
|
||||
msgstr "Nom de l'éditeur du flux"
|
||||
|
||||
#: sncfgtfs/models.py:650
|
||||
#: sncfgtfs/models.py:787
|
||||
msgid "Feed publisher URL"
|
||||
msgstr "URL de l'éditeur du flux"
|
||||
|
||||
#: sncfgtfs/models.py:655
|
||||
#: sncfgtfs/models.py:792
|
||||
msgid "Feed language"
|
||||
msgstr "Langue du flux"
|
||||
|
||||
#: sncfgtfs/models.py:659
|
||||
#: sncfgtfs/models.py:796
|
||||
msgid "Feed start date"
|
||||
msgstr "Date de début du flux"
|
||||
|
||||
#: sncfgtfs/models.py:663
|
||||
#: sncfgtfs/models.py:800
|
||||
msgid "Feed end date"
|
||||
msgstr "Date de fin du flux"
|
||||
|
||||
#: sncfgtfs/models.py:668
|
||||
#: sncfgtfs/models.py:805
|
||||
msgid "Feed version"
|
||||
msgstr "Version du flux"
|
||||
|
||||
#: sncfgtfs/models.py:672
|
||||
#: sncfgtfs/models.py:815
|
||||
msgid "Feed info"
|
||||
msgstr "Information du flux"
|
||||
|
||||
#: sncfgtfs/models.py:673
|
||||
#: sncfgtfs/models.py:816
|
||||
msgid "Feed infos"
|
||||
msgstr "Informations du flux"
|
||||
|
||||
#: sncfgtfs/models.py:691
|
||||
#: sncfgtfs/models.py:835
|
||||
msgid "Start time"
|
||||
msgstr "Heure de début"
|
||||
|
||||
#: sncfgtfs/models.py:695 sncfgtfs/models.py:743
|
||||
#: sncfgtfs/models.py:839 sncfgtfs/models.py:888
|
||||
msgid "Schedule relationship"
|
||||
msgstr "Relation de la planification"
|
||||
|
||||
#: sncfgtfs/models.py:704 sncfgtfs/models.py:714
|
||||
#: sncfgtfs/models.py:848 sncfgtfs/models.py:859
|
||||
msgid "Trip update"
|
||||
msgstr "Mise à jour du trajet"
|
||||
|
||||
#: sncfgtfs/models.py:705
|
||||
#: sncfgtfs/models.py:849
|
||||
msgid "Trip updates"
|
||||
msgstr "Mises à jour des trajets"
|
||||
|
||||
#: sncfgtfs/models.py:727
|
||||
#: sncfgtfs/models.py:872
|
||||
msgid "Arrival delay"
|
||||
msgstr "Retard à l'arrivée"
|
||||
|
||||
#: sncfgtfs/models.py:735
|
||||
#: sncfgtfs/models.py:880
|
||||
msgid "Departure delay"
|
||||
msgstr "Retard au départ"
|
||||
|
||||
#: sncfgtfs/models.py:752
|
||||
#: sncfgtfs/models.py:897
|
||||
msgid "Stop time update"
|
||||
msgstr "Mise à jour du temps d'arrêt"
|
||||
|
||||
#: sncfgtfs/models.py:753
|
||||
#: sncfgtfs/models.py:898
|
||||
msgid "Stop time updates"
|
||||
msgstr "Mises à jour des temps d'arrêt"
|
||||
|
||||
#~ msgid "TGV"
|
||||
#~ msgstr "TGV"
|
||||
|
||||
#~ msgid "TER"
|
||||
#~ msgstr "TER"
|
||||
|
||||
#~ msgid "Intercités"
|
||||
#~ msgstr "Intercités"
|
||||
|
||||
#~ msgid "Transilien"
|
||||
#~ msgstr "Transilien"
|
||||
|
||||
#~ msgid "Eurostar"
|
||||
#~ msgstr "Eurostar"
|
||||
|
||||
#~ msgid "Trenitalia"
|
||||
#~ msgstr "Trenitalia"
|
||||
|
||||
#~ msgid "Renfe"
|
||||
#~ msgstr "Renfe"
|
||||
|
||||
#~ msgid "ÖBB"
|
||||
#~ msgstr "ÖBB"
|
||||
|
||||
#~ msgid "Last update"
|
||||
#~ msgstr "Dernière mise à jour"
|
||||
|
||||
#~ msgid "Transport type"
|
||||
#~ msgstr "Type de transport"
|
||||
|
@ -2,27 +2,18 @@ import csv
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import requests
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, Route, Stop, StopTime, Transfer, Trip
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, FeedInfo, GTFSFeed, Route, Stop, StopTime, Transfer, Trip, \
|
||||
PickupType
|
||||
|
||||
|
||||
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",
|
||||
"ES": "https://www.data.gouv.fr/fr/datasets/r/9089b550-696e-4ae0-87b5-40ea55a14292",
|
||||
"TI": "https://thello.axelor.com/public/gtfs/gtfs.zip",
|
||||
"RENFE": "https://ssl.renfe.com/gtransit/Fichero_AV_LD/google_transit.zip",
|
||||
"OBB": "https://static.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_OP_2024_obb.zip",
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
|
||||
parser.add_argument('--bulk_size', type=int, default=1000, help="Number of objects to create in bulk.")
|
||||
@ -30,36 +21,36 @@ class Command(BaseCommand):
|
||||
help="Do not update the database, only print what would be done.")
|
||||
parser.add_argument('--force', '-f', action='store_true', help="Force the update of the database.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
bulk_size = options['bulk_size']
|
||||
dry_run = options['dry_run']
|
||||
force = options['force']
|
||||
def handle(self, debug: bool = False, bulk_size: int = 100, dry_run: bool = False, force: bool = False,
|
||||
verbosity: int = 1, *args, **options):
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING("Dry run mode activated."))
|
||||
|
||||
if not FeedInfo.objects.exists():
|
||||
last_update_date = "1970-01-01"
|
||||
else:
|
||||
last_update_date = FeedInfo.objects.get(publisher_name='SNCF_default').version
|
||||
|
||||
for url in self.GTFS_FEEDS.values():
|
||||
resp = requests.head(url)
|
||||
if "Last-Modified" not in resp.headers:
|
||||
continue
|
||||
last_modified = resp.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:
|
||||
if not force:
|
||||
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:
|
||||
for gtfs_feed in GTFSFeed.objects.all():
|
||||
gtfs_code = gtfs_feed.code
|
||||
|
||||
if not force:
|
||||
# Check if the source file was updated
|
||||
resp = requests.head(gtfs_feed.feed_url, allow_redirects=True)
|
||||
if 'ETag' in resp.headers and gtfs_feed.etag:
|
||||
if resp.headers['ETag'] == gtfs_feed.etag:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
|
||||
continue
|
||||
if 'Last-Modified' in resp.headers and gtfs_feed.last_modified:
|
||||
last_modified = resp.headers['Last-Modified']
|
||||
last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
|
||||
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
|
||||
if last_modified <= gtfs_feed.last_modified:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write(self.style.WARNING(f"Database is already up-to-date for {gtfs_feed}."))
|
||||
continue
|
||||
|
||||
self.stdout.write(f"Downloading GTFS feed for {gtfs_feed}...")
|
||||
resp = requests.get(gtfs_feed.feed_url, allow_redirects=True, stream=True)
|
||||
with ZipFile(BytesIO(resp.content)) as zipfile:
|
||||
def read_file(filename):
|
||||
lines = zipfile.read(filename).decode().replace('\ufeff', '').splitlines()
|
||||
return [line.strip() for line in lines]
|
||||
@ -67,23 +58,25 @@ class Command(BaseCommand):
|
||||
agencies = []
|
||||
for agency_dict in csv.DictReader(read_file("agency.txt")):
|
||||
agency_dict: dict
|
||||
if transport_type == "ES" \
|
||||
and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER':
|
||||
continue
|
||||
# if gtfs_code == "FR-EUROSTAR" \
|
||||
# and agency_dict['agency_id'] != 'ES' and agency_dict['agency_id'] != 'ER':
|
||||
# continue
|
||||
agency = Agency(
|
||||
id=agency_dict['agency_id'],
|
||||
id=f"{gtfs_code}-{agency_dict['agency_id']}",
|
||||
name=agency_dict['agency_name'],
|
||||
url=agency_dict['agency_url'],
|
||||
timezone=agency_dict['agency_timezone'],
|
||||
lang=agency_dict.get('agency_lang', "fr"),
|
||||
phone=agency_dict.get('agency_phone', ""),
|
||||
email=agency_dict.get('agency_email', ""),
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
agencies.append(agency)
|
||||
if agencies and not dry_run:
|
||||
Agency.objects.bulk_create(agencies,
|
||||
update_conflicts=True,
|
||||
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email'],
|
||||
update_fields=['name', 'url', 'timezone', 'lang', 'phone', 'email',
|
||||
'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
agencies.clear()
|
||||
|
||||
@ -91,8 +84,10 @@ class Command(BaseCommand):
|
||||
for stop_dict in csv.DictReader(read_file("stops.txt")):
|
||||
stop_dict: dict
|
||||
stop_id = stop_dict['stop_id']
|
||||
if transport_type in ["ES", "TI", "RENFE"]:
|
||||
stop_id = f"{transport_type}-{stop_id}"
|
||||
stop_id = f"{gtfs_code}-{stop_id}"
|
||||
|
||||
parent_station_id = stop_dict.get('parent_station', None)
|
||||
parent_station_id = f"{gtfs_code}-{parent_station_id}" if parent_station_id else None
|
||||
|
||||
stop = Stop(
|
||||
id=stop_id,
|
||||
@ -102,13 +97,13 @@ class Command(BaseCommand):
|
||||
lon=stop_dict['stop_lon'],
|
||||
zone_id=stop_dict.get('zone_id', ""),
|
||||
url=stop_dict.get('stop_url', ""),
|
||||
location_type=stop_dict.get('location_type', 1) or 1,
|
||||
parent_station_id=stop_dict.get('parent_station', None) or None,
|
||||
location_type=stop_dict.get('location_type', 0) or 0,
|
||||
parent_station_id=parent_station_id,
|
||||
timezone=stop_dict.get('stop_timezone', ""),
|
||||
wheelchair_boarding=stop_dict.get('wheelchair_boarding', 0),
|
||||
level_id=stop_dict.get('level_id', ""),
|
||||
platform_code=stop_dict.get('platform_code', ""),
|
||||
transport_type=transport_type,
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
stops.append(stop)
|
||||
|
||||
@ -119,7 +114,7 @@ class Command(BaseCommand):
|
||||
update_fields=['name', 'desc', 'lat', 'lon', 'zone_id', 'url',
|
||||
'location_type', 'parent_station_id', 'timezone',
|
||||
'wheelchair_boarding', 'level_id', 'platform_code',
|
||||
'transport_type'],
|
||||
'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
stops.clear()
|
||||
|
||||
@ -127,11 +122,10 @@ class Command(BaseCommand):
|
||||
for route_dict in csv.DictReader(read_file("routes.txt")):
|
||||
route_dict: dict
|
||||
route_id = route_dict['route_id']
|
||||
if transport_type == "TI":
|
||||
route_id = f"{transport_type}-{route_id}"
|
||||
route_id = f"{gtfs_code}-{route_id}"
|
||||
route = Route(
|
||||
id=route_id,
|
||||
agency_id=route_dict['agency_id'],
|
||||
agency_id=f"{gtfs_code}-{route_dict['agency_id']}",
|
||||
short_name=route_dict['route_short_name'],
|
||||
long_name=route_dict['route_long_name'],
|
||||
desc=route_dict.get('route_desc', ""),
|
||||
@ -139,7 +133,7 @@ class Command(BaseCommand):
|
||||
url=route_dict.get('route_url', ""),
|
||||
color=route_dict.get('route_color', ""),
|
||||
text_color=route_dict.get('route_text_color', ""),
|
||||
transport_type=transport_type,
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
routes.append(route)
|
||||
|
||||
@ -148,7 +142,7 @@ class Command(BaseCommand):
|
||||
update_conflicts=True,
|
||||
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
|
||||
'type', 'url', 'color', 'text_color',
|
||||
'transport_type'],
|
||||
'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
routes.clear()
|
||||
if routes and not dry_run:
|
||||
@ -156,17 +150,17 @@ class Command(BaseCommand):
|
||||
update_conflicts=True,
|
||||
update_fields=['agency_id', 'short_name', 'long_name', 'desc',
|
||||
'type', 'url', 'color', 'text_color',
|
||||
'transport_type'],
|
||||
'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
routes.clear()
|
||||
|
||||
Calendar.objects.filter(transport_type=transport_type).delete()
|
||||
Calendar.objects.filter(gtfs_feed=gtfs_feed).delete()
|
||||
calendars = {}
|
||||
if "calendar.txt" in zipfile.namelist():
|
||||
for calendar_dict in csv.DictReader(read_file("calendar.txt")):
|
||||
calendar_dict: dict
|
||||
calendar = Calendar(
|
||||
id=f"{transport_type}-{calendar_dict['service_id']}",
|
||||
id=f"{gtfs_code}-{calendar_dict['service_id']}",
|
||||
monday=calendar_dict['monday'],
|
||||
tuesday=calendar_dict['tuesday'],
|
||||
wednesday=calendar_dict['wednesday'],
|
||||
@ -176,7 +170,7 @@ class Command(BaseCommand):
|
||||
sunday=calendar_dict['sunday'],
|
||||
start_date=calendar_dict['start_date'],
|
||||
end_date=calendar_dict['end_date'],
|
||||
transport_type=transport_type,
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
calendars[calendar.id] = calendar
|
||||
|
||||
@ -185,14 +179,14 @@ class Command(BaseCommand):
|
||||
update_conflicts=True,
|
||||
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday', 'start_date',
|
||||
'end_date', 'transport_type'],
|
||||
'end_date', 'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
calendars.clear()
|
||||
if calendars and not dry_run:
|
||||
Calendar.objects.bulk_create(calendars.values(), update_conflicts=True,
|
||||
update_fields=['monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday', 'start_date',
|
||||
'end_date', 'transport_type'],
|
||||
'end_date', 'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
calendars.clear()
|
||||
|
||||
@ -200,8 +194,8 @@ class Command(BaseCommand):
|
||||
for calendar_date_dict in csv.DictReader(read_file("calendar_dates.txt")):
|
||||
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']}",
|
||||
id=f"{gtfs_code}-{calendar_date_dict['service_id']}-{calendar_date_dict['date']}",
|
||||
service_id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
|
||||
date=calendar_date_dict['date'],
|
||||
exception_type=calendar_date_dict['exception_type'],
|
||||
)
|
||||
@ -209,7 +203,7 @@ class Command(BaseCommand):
|
||||
|
||||
if calendar_date.service_id not in calendars:
|
||||
calendar = Calendar(
|
||||
id=f"{transport_type}-{calendar_date_dict['service_id']}",
|
||||
id=f"{gtfs_code}-{calendar_date_dict['service_id']}",
|
||||
monday=False,
|
||||
tuesday=False,
|
||||
wednesday=False,
|
||||
@ -219,11 +213,11 @@ class Command(BaseCommand):
|
||||
sunday=False,
|
||||
start_date=calendar_date_dict['date'],
|
||||
end_date=calendar_date_dict['date'],
|
||||
transport_type=transport_type,
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
calendars[calendar.id] = calendar
|
||||
else:
|
||||
calendar = calendars[f"{transport_type}-{calendar_date_dict['service_id']}"]
|
||||
calendar = calendars[f"{gtfs_code}-{calendar_date_dict['service_id']}"]
|
||||
if calendar.start_date > calendar_date.date:
|
||||
calendar.start_date = calendar_date.date
|
||||
if calendar.end_date < calendar_date.date:
|
||||
@ -233,7 +227,7 @@ class Command(BaseCommand):
|
||||
Calendar.objects.bulk_create(calendars.values(),
|
||||
batch_size=bulk_size,
|
||||
update_conflicts=True,
|
||||
update_fields=['start_date', 'end_date'],
|
||||
update_fields=['start_date', 'end_date', 'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
CalendarDate.objects.bulk_create(calendar_dates,
|
||||
batch_size=bulk_size,
|
||||
@ -248,22 +242,12 @@ class Command(BaseCommand):
|
||||
trip_dict: dict
|
||||
trip_id = trip_dict['trip_id']
|
||||
route_id = trip_dict['route_id']
|
||||
if transport_type in ["TGV", "IC", "TER"]:
|
||||
trip_id, last_update = trip_id.split(':', 1)
|
||||
last_update = datetime.fromisoformat(last_update)
|
||||
elif transport_type in ["ES", "RENFE"]:
|
||||
trip_id = f"{transport_type}-{trip_id}"
|
||||
last_update = None
|
||||
elif transport_type == "TI":
|
||||
trip_id = f"{transport_type}-{trip_id}"
|
||||
route_id = f"{transport_type}-{route_id}"
|
||||
last_update = None
|
||||
else:
|
||||
last_update = None
|
||||
trip_id = f"{gtfs_code}-{trip_id}"
|
||||
route_id = f"{gtfs_code}-{route_id}"
|
||||
trip = Trip(
|
||||
id=trip_id,
|
||||
route_id=route_id,
|
||||
service_id=f"{transport_type}-{trip_dict['service_id']}",
|
||||
service_id=f"{gtfs_code}-{trip_dict['service_id']}",
|
||||
headsign=trip_dict.get('trip_headsign', ""),
|
||||
short_name=trip_dict.get('trip_short_name', ""),
|
||||
direction_id=trip_dict.get('direction_id', None) or None,
|
||||
@ -271,7 +255,7 @@ class Command(BaseCommand):
|
||||
shape_id=trip_dict.get('shape_id', ""),
|
||||
wheelchair_accessible=trip_dict.get('wheelchair_accessible', None),
|
||||
bikes_allowed=trip_dict.get('bikes_allowed', None),
|
||||
last_update=last_update,
|
||||
gtfs_feed=gtfs_feed,
|
||||
)
|
||||
trips.append(trip)
|
||||
|
||||
@ -280,7 +264,7 @@ class Command(BaseCommand):
|
||||
update_conflicts=True,
|
||||
update_fields=['route_id', 'service_id', 'headsign', 'short_name',
|
||||
'direction_id', 'block_id', 'shape_id',
|
||||
'wheelchair_accessible', 'bikes_allowed'],
|
||||
'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
trips.clear()
|
||||
if trips and not dry_run:
|
||||
@ -288,7 +272,7 @@ class Command(BaseCommand):
|
||||
update_conflicts=True,
|
||||
update_fields=['route_id', 'service_id', 'headsign', 'short_name',
|
||||
'direction_id', 'block_id', 'shape_id',
|
||||
'wheelchair_accessible', 'bikes_allowed'],
|
||||
'wheelchair_accessible', 'bikes_allowed', 'gtfs_feed'],
|
||||
unique_fields=['id'])
|
||||
trips.clear()
|
||||
|
||||
@ -297,14 +281,10 @@ class Command(BaseCommand):
|
||||
stop_time_dict: dict
|
||||
|
||||
stop_id = stop_time_dict['stop_id']
|
||||
if transport_type in ["ES", "TI", "RENFE"]:
|
||||
stop_id = f"{transport_type}-{stop_id}"
|
||||
stop_id = f"{gtfs_code}-{stop_id}"
|
||||
|
||||
trip_id = stop_time_dict['trip_id']
|
||||
if transport_type in ["TGV", "IC", "TER"]:
|
||||
trip_id = trip_id.split(':', 1)[0]
|
||||
elif transport_type in ["ES", "TI", "RENFE"]:
|
||||
trip_id = f"{transport_type}-{trip_id}"
|
||||
trip_id = f"{gtfs_code}-{trip_id}"
|
||||
|
||||
arr_time = stop_time_dict['arrival_time']
|
||||
arr_h, arr_m, arr_s = map(int, arr_time.split(':'))
|
||||
@ -315,19 +295,16 @@ class Command(BaseCommand):
|
||||
|
||||
pickup_type = stop_time_dict.get('pickup_type', 0)
|
||||
drop_off_type = stop_time_dict.get('drop_off_type', 0)
|
||||
if transport_type in ["ES", "RENFE", "OBB"]:
|
||||
if stop_time_dict['stop_sequence'] == "1":
|
||||
drop_off_type = 1
|
||||
elif arr_time == dep_time:
|
||||
pickup_type = 1
|
||||
elif transport_type == "TI":
|
||||
if stop_time_dict['stop_sequence'] == "0":
|
||||
drop_off_type = 1
|
||||
elif arr_time == dep_time:
|
||||
pickup_type = 1
|
||||
if stop_time_dict['stop_sequence'] == "1":
|
||||
# First stop
|
||||
drop_off_type = PickupType.NONE
|
||||
elif arr_time == dep_time:
|
||||
# Last stop
|
||||
pickup_type = PickupType.NONE
|
||||
|
||||
st = StopTime(
|
||||
id=f"{trip_id}-{stop_id}-{stop_time_dict['departure_time']}",
|
||||
id=f"{gtfs_code}-{stop_time_dict['trip_id']}-{stop_time_dict['stop_id']}"
|
||||
f"-{stop_time_dict['departure_time']}",
|
||||
trip_id=trip_id,
|
||||
arrival_time=timedelta(seconds=arr_time),
|
||||
departure_time=timedelta(seconds=dep_time),
|
||||
@ -363,14 +340,13 @@ class Command(BaseCommand):
|
||||
transfer_dict: dict
|
||||
from_stop_id = transfer_dict['from_stop_id']
|
||||
to_stop_id = transfer_dict['to_stop_id']
|
||||
if transport_type in ["ES", "RENFE", "OBB"]:
|
||||
from_stop_id = f"{transport_type}-{from_stop_id}"
|
||||
to_stop_id = f"{transport_type}-{to_stop_id}"
|
||||
from_stop_id = f"{gtfs_code}-{from_stop_id}"
|
||||
to_stop_id = f"{gtfs_code}-{to_stop_id}"
|
||||
|
||||
transfer = Transfer(
|
||||
id=f"{from_stop_id}-{to_stop_id}",
|
||||
from_stop_id=transfer_dict['from_stop_id'],
|
||||
to_stop_id=transfer_dict['to_stop_id'],
|
||||
id=f"{transfer_dict['from_stop_id']}-{transfer_dict['to_stop_id']}",
|
||||
from_stop_id=from_stop_id,
|
||||
to_stop_id=to_stop_id,
|
||||
transfer_type=transfer_dict['transfer_type'],
|
||||
min_transfer_time=transfer_dict['min_transfer_time'],
|
||||
)
|
||||
@ -395,6 +371,7 @@ class Command(BaseCommand):
|
||||
feed_info_dict: dict
|
||||
FeedInfo.objects.update_or_create(
|
||||
publisher_name=feed_info_dict['feed_publisher_name'],
|
||||
gtfs_feed=gtfs_feed,
|
||||
defaults=dict(
|
||||
publisher_url=feed_info_dict['feed_publisher_url'],
|
||||
lang=feed_info_dict['feed_lang'],
|
||||
@ -403,3 +380,12 @@ class Command(BaseCommand):
|
||||
version=feed_info_dict.get('feed_version', 1),
|
||||
)
|
||||
)
|
||||
|
||||
if 'ETag' in resp.headers:
|
||||
gtfs_feed.etag = resp.headers['ETag']
|
||||
gtfs_feed.save()
|
||||
if 'Last-Modified' in resp.headers:
|
||||
last_modified = resp.headers['Last-Modified']
|
||||
gtfs_feed.last_modified = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z") \
|
||||
.replace(tzinfo=ZoneInfo(last_modified.split(' ')[-1]))
|
||||
gtfs_feed.save()
|
||||
|
@ -5,8 +5,8 @@ import requests
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Q
|
||||
|
||||
from sncfgtfs.gtfs_realtime_pb2 import FeedMessage
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, LocationType, PickupType, \
|
||||
from sncfgtfs.gtfs_realtime_pb2 import FeedMessage, TripUpdate as GTFSTripUpdate
|
||||
from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, GTFSFeed, LocationType, PickupType, \
|
||||
Route, RouteType, Stop, StopScheduleRelationship, StopTime, StopTimeUpdate, \
|
||||
Trip, TripUpdate, TripScheduleRelationship
|
||||
|
||||
@ -14,34 +14,33 @@ from sncfgtfs.models import Agency, Calendar, CalendarDate, ExceptionType, Locat
|
||||
class Command(BaseCommand):
|
||||
help = "Update the SNCF GTFS Realtime database."
|
||||
|
||||
GTFS_RT_FEEDS = {
|
||||
"TGV": "https://proxy.transport.data.gouv.fr/resource/sncf-tgv-gtfs-rt-trip-updates",
|
||||
"IC": "https://proxy.transport.data.gouv.fr/resource/sncf-ic-gtfs-rt-trip-updates",
|
||||
"TER": "https://proxy.transport.data.gouv.fr/resource/sncf-ter-gtfs-rt-trip-updates",
|
||||
"TI": "https://thello.axelor.com/public/gtfs/GTFS-RT.bin",
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--debug', '-d', action='store_true', help="Activate debug mode")
|
||||
|
||||
def handle(self, debug=False, *args, **options):
|
||||
for feed_type, feed_url in self.GTFS_RT_FEEDS.items():
|
||||
self.stdout.write(f"Updating {feed_type} feed...")
|
||||
def handle(self, debug: bool = False, verbosity: int = 1, *args, **options):
|
||||
for gtfs_feed in GTFSFeed.objects.all():
|
||||
if not gtfs_feed.rt_feed_url:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write(self.style.WARNING(f"No GTFS-RT feed found for {gtfs_feed}."))
|
||||
continue
|
||||
|
||||
self.stdout.write(f"Updating GTFS-RT feed for {gtfs_feed}…")
|
||||
|
||||
gtfs_code = gtfs_feed.code
|
||||
feed_message = FeedMessage()
|
||||
feed_message.ParseFromString(requests.get(feed_url).content)
|
||||
feed_message.ParseFromString(requests.get(gtfs_feed.rt_feed_url, allow_redirects=True).content)
|
||||
|
||||
stop_times_updates = []
|
||||
|
||||
if debug:
|
||||
with open(f'feed_message-{feed_type}.txt', 'w') as f:
|
||||
with open(f'feed_message-{gtfs_code}.txt', 'w') as f:
|
||||
f.write(str(feed_message))
|
||||
|
||||
for entity in feed_message.entity:
|
||||
if entity.HasField("trip_update"):
|
||||
trip_update = entity.trip_update
|
||||
trip_id = trip_update.trip.trip_id
|
||||
if feed_type in ["TGV", "IC", "TER"]:
|
||||
trip_id = trip_id.split(":", 1)[0]
|
||||
trip_id = f"{gtfs_code}-{trip_id}"
|
||||
|
||||
start_date = date(year=int(trip_update.trip.start_date[:4]),
|
||||
month=int(trip_update.trip.start_date[4:6]),
|
||||
@ -50,7 +49,7 @@ class Command(BaseCommand):
|
||||
|
||||
if trip_update.trip.schedule_relationship == TripScheduleRelationship.ADDED:
|
||||
# C'est un trajet nouveau. On crée le trajet associé.
|
||||
self.create_trip(trip_update, trip_id, start_dt, feed_type)
|
||||
self.create_trip(trip_update, trip_id, start_dt, gtfs_feed)
|
||||
|
||||
if not Trip.objects.filter(id=trip_id).exists():
|
||||
self.stdout.write(f"Trip {trip_id} does not exist in the GTFS feed.")
|
||||
@ -68,22 +67,19 @@ class Command(BaseCommand):
|
||||
|
||||
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
|
||||
stop_id = stop_time_update.stop_id
|
||||
if stop_id.startswith('StopArea:'):
|
||||
# On est dans le cadre d'une gare. On cherche le quai associé.
|
||||
if StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists():
|
||||
# U
|
||||
stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop
|
||||
stop_id = f"{gtfs_code}-{stop_id}"
|
||||
if StopTime.objects.filter(trip_id=trip_id, stop=stop_id).exists():
|
||||
st = StopTime.objects.filter(trip_id=trip_id, stop=stop_id)
|
||||
if st.count() > 1:
|
||||
st = st.get(stop_sequence=stop_sequence)
|
||||
else:
|
||||
stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all()
|
||||
for s2 in StopTime.objects.filter(trip_id=trip_id).all()
|
||||
if s.stop_type in s2.stop.stop_type
|
||||
or s2.stop.stop_type in s.stop_type]
|
||||
stop = stops[0] if stops else Stop.objects.get(id=stop_id)
|
||||
|
||||
st, _created = StopTime.objects.update_or_create(
|
||||
id=f"{trip_id}-{stop.id}",
|
||||
st = st.first()
|
||||
else:
|
||||
# Stop is added
|
||||
st = StopTime.objects.create(
|
||||
id=f"{trip_id}-{stop_time_update.stop_id}",
|
||||
trip_id=trip_id,
|
||||
stop_id=stop.id,
|
||||
stop_id=stop_id,
|
||||
defaults={
|
||||
"stop_sequence": stop_sequence,
|
||||
"arrival_time": datetime.fromtimestamp(stop_time_update.arrival.time,
|
||||
@ -96,23 +92,16 @@ class Command(BaseCommand):
|
||||
else PickupType.NONE),
|
||||
}
|
||||
)
|
||||
elif stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
|
||||
st = StopTime.objects.get(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id),
|
||||
trip_id=trip_id)
|
||||
|
||||
if stop_time_update.schedule_relationship == StopScheduleRelationship.SKIPPED:
|
||||
if st.pickup_type != PickupType.NONE or st.drop_off_type != PickupType.NONE:
|
||||
st.pickup_type = PickupType.NONE
|
||||
st.drop_off_type = PickupType.NONE
|
||||
st.save()
|
||||
else:
|
||||
qs = StopTime.objects.filter(Q(stop=stop_id) | Q(stop__parent_station_id=stop_id),
|
||||
trip_id=trip_id)
|
||||
if qs.count() == 1:
|
||||
st = qs.first()
|
||||
else:
|
||||
st = qs.get(stop_sequence=stop_sequence)
|
||||
if st.stop_sequence != stop_sequence:
|
||||
st.stop_sequence = stop_sequence
|
||||
st.save()
|
||||
|
||||
if st.stop_sequence != stop_sequence:
|
||||
st.stop_sequence = stop_sequence
|
||||
st.save()
|
||||
|
||||
st_update = StopTimeUpdate(
|
||||
trip_update=tu,
|
||||
@ -136,73 +125,22 @@ class Command(BaseCommand):
|
||||
'departure_delay', 'departure_time'],
|
||||
unique_fields=['trip_update', 'stop_time'])
|
||||
|
||||
def create_trip(self, trip_update, trip_id, start_dt, feed_type):
|
||||
def create_trip(self, trip_update: GTFSTripUpdate, trip_id: str, start_dt: datetime, gtfs_feed: GTFSFeed) -> None:
|
||||
headsign = trip_id[5:-1]
|
||||
trip_qs = Trip.objects.all()
|
||||
trip_ids = trip_qs.values_list('id', flat=True)
|
||||
gtfs_code = gtfs_feed.code
|
||||
|
||||
first_stop_queryset = StopTime.objects.filter(
|
||||
stop__parent_station_id=trip_update.stop_time_update[0].stop_id,
|
||||
).values('trip_id')
|
||||
last_stop_queryset = StopTime.objects.filter(
|
||||
stop__parent_station_id=trip_update.stop_time_update[-1].stop_id,
|
||||
).values('trip_id')
|
||||
|
||||
trip_ids = trip_ids.intersection(first_stop_queryset).intersection(last_stop_queryset)
|
||||
# print(trip_id, trip_ids)
|
||||
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
|
||||
stop_id = stop_time_update.stop_id
|
||||
st_queryset = StopTime.objects.filter(stop__parent_station_id=stop_id)
|
||||
if stop_sequence == 0:
|
||||
st_queryset = st_queryset.filter(stop_sequence=0)
|
||||
# print(stop_sequence, Stop.objects.get(id=stop_id).name, stop_time_update)
|
||||
# print(trip_ids)
|
||||
# print(st_queryset.values('trip_id').all())
|
||||
trip_ids_restrict = trip_ids.intersection(st_queryset.values('trip_id'))
|
||||
if trip_ids_restrict:
|
||||
trip_ids = trip_ids_restrict
|
||||
else:
|
||||
stop = Stop.objects.get(id=stop_id)
|
||||
self.stdout.write(self.style.WARNING(f"Warning: No trip is found passing by stop "
|
||||
f"{stop.name} ({stop_id})"))
|
||||
trip_ids = set(trip_ids)
|
||||
route_ids = set(Trip.objects.filter(id__in=trip_ids).values_list('route_id', flat=True))
|
||||
self.stdout.write(f"{len(route_ids)} routes found on trip for new train {headsign}")
|
||||
if not route_ids:
|
||||
origin_id = trip_update.stop_time_update[0].stop_id
|
||||
origin = Stop.objects.get(id=origin_id)
|
||||
destination_id = trip_update.stop_time_update[-1].stop_id
|
||||
destination = Stop.objects.get(id=destination_id)
|
||||
trip_name = f"{origin.name} - {destination.name}"
|
||||
trip_reverse_name = f"{destination.name} - {origin.name}"
|
||||
route_qs = Route.objects.filter(long_name=trip_name, transport_type=feed_type)
|
||||
route_reverse_qs = Route.objects.filter(long_name=trip_reverse_name,
|
||||
transport_type=feed_type)
|
||||
if route_qs.exists():
|
||||
route_ids = set(route_qs.values_list('id', flat=True))
|
||||
elif route_reverse_qs.exists():
|
||||
route_ids = set(route_reverse_qs.values_list('id', flat=True))
|
||||
else:
|
||||
self.stdout.write(f"Route not found for trip {trip_id} ({trip_name}). Creating new one")
|
||||
route = Route.objects.create(
|
||||
id=f"CREATED-{trip_name}",
|
||||
agency=Agency.objects.filter(routes__transport_type=feed_type).first(),
|
||||
transport_type=feed_type,
|
||||
type=RouteType.RAIL,
|
||||
short_name=trip_name,
|
||||
long_name=trip_name,
|
||||
)
|
||||
route_ids = {route.id}
|
||||
self.stdout.write(f"Route {route.id} created for trip {trip_id} ({trip_name})")
|
||||
elif len(route_ids) > 1:
|
||||
self.stdout.write(f"Multiple routes found for trip {trip_id}.")
|
||||
self.stdout.write(", ".join(route_ids))
|
||||
route_id = route_ids.pop()
|
||||
route, _created = Route.objects.get_or_create(
|
||||
id=f"{gtfs_code}-ADDED-{headsign}",
|
||||
gtfs_feed=gtfs_feed,
|
||||
type=RouteType.RAIL,
|
||||
short_name="ADDED",
|
||||
long_name="ADDED ROUTE",
|
||||
)
|
||||
|
||||
Calendar.objects.update_or_create(
|
||||
id=f"{feed_type}-new-{headsign}",
|
||||
id=f"{gtfs_code}-ADDED-{headsign}",
|
||||
defaults={
|
||||
"transport_type": feed_type,
|
||||
"gtfs_feed": gtfs_feed,
|
||||
"monday": False,
|
||||
"tuesday": False,
|
||||
"wednesday": False,
|
||||
@ -215,9 +153,9 @@ class Command(BaseCommand):
|
||||
}
|
||||
)
|
||||
CalendarDate.objects.update_or_create(
|
||||
id=f"{feed_type}-{headsign}-{trip_update.trip.start_date}",
|
||||
id=f"{gtfs_code}-ADDED-{headsign}-{trip_update.trip.start_date}",
|
||||
defaults={
|
||||
"service_id": f"{feed_type}-new-{headsign}",
|
||||
"service_id": f"{gtfs_code}-ADDED-{headsign}",
|
||||
"date": trip_update.trip.start_date,
|
||||
"exception_type": ExceptionType.ADDED,
|
||||
}
|
||||
@ -225,32 +163,17 @@ class Command(BaseCommand):
|
||||
Trip.objects.update_or_create(
|
||||
id=trip_id,
|
||||
defaults={
|
||||
"route_id": route_id,
|
||||
"service_id": f"{feed_type}-new-{headsign}",
|
||||
"route_id": route.id,
|
||||
"service_id": f"{gtfs_code}-ADDED-{headsign}",
|
||||
"headsign": headsign,
|
||||
"direction_id": trip_update.trip.direction_id,
|
||||
"gtfs_feed": gtfs_feed,
|
||||
}
|
||||
)
|
||||
|
||||
sample_trip = Trip.objects.filter(id__in=trip_ids, route_id=route_id)
|
||||
sample_trip = sample_trip.first() if sample_trip.exists() else None
|
||||
for stop_sequence, stop_time_update in enumerate(trip_update.stop_time_update):
|
||||
stop_id = stop_time_update.stop_id
|
||||
stop = Stop.objects.get(id=stop_id)
|
||||
if stop.location_type == LocationType.STATION:
|
||||
if not StopTime.objects.filter(trip_id=trip_id).exists():
|
||||
if sample_trip:
|
||||
stop = StopTime.objects.get(trip_id=sample_trip.id,
|
||||
stop__parent_station_id=stop_id).stop
|
||||
elif StopTime.objects.filter(trip_id=trip_id, stop__parent_station_id=stop_id).exists():
|
||||
stop = StopTime.objects.get(trip_id=trip_id, stop__parent_station_id=stop_id).stop
|
||||
else:
|
||||
stops = [s for s in Stop.objects.filter(parent_station_id=stop_id).all()
|
||||
for s2 in StopTime.objects.filter(trip_id=trip_id).all()
|
||||
if s.stop_type in s2.stop.stop_type
|
||||
or s2.stop.stop_type in s.stop_type]
|
||||
stop = stops[0] if stops else stop
|
||||
stop_id = stop.id
|
||||
stop_id = f"{gtfs_code}-{stop_id}"
|
||||
|
||||
arr_time = datetime.fromtimestamp(stop_time_update.arrival.time,
|
||||
tz=ZoneInfo("Europe/Paris")) - start_dt
|
||||
@ -263,7 +186,7 @@ class Command(BaseCommand):
|
||||
and stop_sequence < len(trip_update.stop_time_update) - 1 else PickupType.NONE
|
||||
|
||||
StopTime.objects.update_or_create(
|
||||
id=f"{trip_id}-{stop_id}",
|
||||
id=f"{trip_id}-{stop_time_update.stop_id}",
|
||||
trip_id=trip_id,
|
||||
defaults={
|
||||
"stop_id": stop_id,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,15 +2,58 @@ 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")
|
||||
EUROSTAR = "ES", _("Eurostar")
|
||||
TRENITALIA = "TI", _("Trenitalia")
|
||||
RENFE = "RENFE", _("Renfe")
|
||||
OBB = "OBB", _("ÖBB")
|
||||
class Country(models.TextChoices):
|
||||
"""
|
||||
Country list by ISO 3166-1 alpha-2 code.
|
||||
Only countries that are member of the Council of Europe
|
||||
are listed for now.
|
||||
"""
|
||||
ALBANIA = "AL", _("Albania")
|
||||
ANDORRA = "AD", _("Andorra")
|
||||
ARMENIA = "AM", _("Armenia")
|
||||
AUSTRIA = "AT", _("Austria")
|
||||
AZERBAIJAN = "AZ", _("Azerbaijan")
|
||||
BELGIUM = "BE", _("Belgium")
|
||||
BOSNIA_AND_HERZEGOVINA = "BA", _("Bosnia and Herzegovina")
|
||||
BULGARIA = "BG", _("Bulgaria")
|
||||
CROATIA = "HR", _("Croatia")
|
||||
CYPRUS = "CY", _("Cyprus")
|
||||
CZECH_REPUBLIC = "CZ", _("Czech Republic")
|
||||
DENMARK = "DK", _("Denmark")
|
||||
ESTONIA = "EE", _("Estonia")
|
||||
FINLAND = "FI", _("Finland")
|
||||
FRANCE = "FR", _("France")
|
||||
GEORGIA = "GE", _("Georgia")
|
||||
GERMANY = "DE", _("Germany")
|
||||
GREECE = "GR", _("Greece")
|
||||
HUNGARY = "HU", _("Hungary")
|
||||
ICELAND = "IS", _("Iceland")
|
||||
IRELAND = "IE", _("Ireland")
|
||||
ITALY = "IT", _("Italy")
|
||||
LATVIA = "LV", _("Latvia")
|
||||
LIECHTENSTEIN = "LI", _("Liechtenstein")
|
||||
LITHUANIA = "LT", _("Lithuania")
|
||||
LUXEMBOURG = "LU", _("Luxembourg")
|
||||
MALTA = "MT", _("Malta")
|
||||
MOLDOVA = "MD", _("Moldova")
|
||||
MONACO = "MC", _("Monaco")
|
||||
MONTENEGRO = "ME", _("Montenegro")
|
||||
NETHERLANDS = "NL", _("Netherlands")
|
||||
NORTH_MACEDONIA = "MK", _("North Macedonia")
|
||||
NORWAY = "NO", _("Norway")
|
||||
POLAND = "PL", _("Poland")
|
||||
PORTUGAL = "PT", _("Portugal")
|
||||
ROMANIA = "RO", _("Romania")
|
||||
SAN_MARINO = "SM", _("San Marino")
|
||||
SERBIA = "RS", _("Serbia")
|
||||
SLOVAKIA = "SK", _("Slovakia")
|
||||
SLOVENIA = "SI", _("Slovenia")
|
||||
SPAIN = "ES", _("Spain")
|
||||
SWEDEN = "SE", _("Sweden")
|
||||
SWITZERLAND = "CH", _("Switzerland")
|
||||
TURKEY = "TR", _("Turkey")
|
||||
UNITED_KINGDOM = "GB", _("United Kingdom")
|
||||
UKRAINE = "UA", _("Ukraine")
|
||||
|
||||
|
||||
class LocationType(models.IntegerChoices):
|
||||
@ -79,6 +122,66 @@ class StopScheduleRelationship(models.IntegerChoices):
|
||||
UNSCHEDULED = 3, _("Unscheduled")
|
||||
|
||||
|
||||
class GTFSFeed(models.Model):
|
||||
code = models.CharField(
|
||||
primary_key=True,
|
||||
max_length=64,
|
||||
verbose_name=_("code"),
|
||||
help_text=_("Unique code of the feed.")
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
unique=True,
|
||||
help_text=_("Full name that describes the feed."),
|
||||
)
|
||||
|
||||
country = models.CharField(
|
||||
max_length=2,
|
||||
verbose_name=_("country"),
|
||||
choices=Country,
|
||||
)
|
||||
|
||||
feed_url = models.URLField(
|
||||
verbose_name=_("feed URL"),
|
||||
help_text=_("URL to download the GTFS feed. Must point to a ZIP archive. "
|
||||
"See https://gtfs.org/schedule/ for more information."),
|
||||
)
|
||||
|
||||
rt_feed_url = models.URLField(
|
||||
verbose_name=_("realtime feed URL"),
|
||||
blank=True,
|
||||
default="",
|
||||
help_text=_("URL to download the GTFS-Realtime feed, in the GTFS-RT format. "
|
||||
"See https://gtfs.org/realtime/ for more information."),
|
||||
)
|
||||
|
||||
last_modified = models.DateTimeField(
|
||||
verbose_name=_("last modified date"),
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
etag = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("ETag"),
|
||||
blank=True,
|
||||
default="",
|
||||
help_text=_("If applicable, corresponds to the tag of the last downloaded file. "
|
||||
"If it is not modified, the file is the same."),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("GTFS feed")
|
||||
verbose_name_plural = _("GTFS feeds")
|
||||
ordering = ('country', 'name',)
|
||||
indexes = (models.Index(fields=['name']),)
|
||||
|
||||
|
||||
class Agency(models.Model):
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
@ -117,6 +220,12 @@ class Agency(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -124,6 +233,7 @@ class Agency(models.Model):
|
||||
verbose_name = _("Agency")
|
||||
verbose_name_plural = _("Agencies")
|
||||
ordering = ("name",)
|
||||
indexes = (models.Index(fields=['name']), models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class Stop(models.Model):
|
||||
@ -161,6 +271,7 @@ class Stop(models.Model):
|
||||
zone_id = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Zone ID"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
url = models.URLField(
|
||||
@ -209,10 +320,10 @@ class Stop(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
transport_type = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Transport type"),
|
||||
choices=TransportType,
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
@property
|
||||
@ -227,6 +338,9 @@ class Stop(models.Model):
|
||||
verbose_name = _("Stop")
|
||||
verbose_name_plural = _("Stops")
|
||||
ordering = ("id",)
|
||||
indexes = (models.Index(fields=['name']),
|
||||
models.Index(fields=['code']),
|
||||
models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class Route(models.Model):
|
||||
@ -241,6 +355,9 @@ class Route(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Agency"),
|
||||
related_name="routes",
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
short_name = models.CharField(
|
||||
@ -251,6 +368,7 @@ class Route(models.Model):
|
||||
long_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Route long name"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
desc = models.CharField(
|
||||
@ -281,19 +399,20 @@ class Route(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
transport_type = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Transport type"),
|
||||
choices=TransportType,
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.long_name}"
|
||||
return self.long_name or self.short_name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Route")
|
||||
verbose_name_plural = _("Routes")
|
||||
ordering = ("id",)
|
||||
indexes = (models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class Trip(models.Model):
|
||||
@ -361,21 +480,24 @@ class Trip(models.Model):
|
||||
null=True,
|
||||
)
|
||||
|
||||
last_update = models.DateTimeField(
|
||||
verbose_name=_("Last update"),
|
||||
null=True,
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self.stop_times.order_by('stop_sequence').first().stop
|
||||
def origin(self) -> Stop | None:
|
||||
return self.stop_times.order_by('stop_sequence').first().stop if self.stop_times.exists() else None
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
return self.stop_times.order_by('-stop_sequence').first().stop
|
||||
def destination(self) -> Stop | None:
|
||||
return self.stop_times.order_by('-stop_sequence').first().stop if self.stop_times.exists() else None
|
||||
|
||||
@property
|
||||
def departure_time(self):
|
||||
if not self.stop_times.exists():
|
||||
return _("Unknown")
|
||||
dep_time = self.stop_times.order_by('stop_sequence').first().departure_time
|
||||
hours = int(dep_time.total_seconds() // 3600)
|
||||
minutes = int((dep_time.total_seconds() % 3600) // 60)
|
||||
@ -383,6 +505,8 @@ class Trip(models.Model):
|
||||
|
||||
@property
|
||||
def arrival_time(self):
|
||||
if not self.stop_times.exists():
|
||||
return _("Unknown")
|
||||
arr_time = self.stop_times.order_by('-stop_sequence').first().arrival_time
|
||||
hours = int(arr_time.total_seconds() // 3600)
|
||||
minutes = int((arr_time.total_seconds() % 3600) // 60)
|
||||
@ -390,14 +514,14 @@ class Trip(models.Model):
|
||||
|
||||
@property
|
||||
def train_type(self):
|
||||
if self.route.transport_type == TransportType.TRANSILIEN:
|
||||
if self.gtfs_feed.code == "FR-IDF-TN":
|
||||
return self.route.short_name
|
||||
else:
|
||||
return self.origin.stop_type
|
||||
|
||||
@property
|
||||
def train_number(self):
|
||||
if self.route.transport_type == TransportType.TRANSILIEN:
|
||||
if self.gtfs_feed.code == "FR-IDF-TN":
|
||||
return self.short_name
|
||||
else:
|
||||
return self.headsign
|
||||
@ -422,13 +546,23 @@ class Trip(models.Model):
|
||||
return "404042"
|
||||
return "000000"
|
||||
|
||||
@property
|
||||
def origin_destination(self):
|
||||
origin = self.origin
|
||||
origin = origin.name if origin else _("Unknown")
|
||||
destination = self.destination
|
||||
destination = destination.name if destination else _("Unknown")
|
||||
return f"{origin} {self.departure_time} → {destination} {self.arrival_time}"
|
||||
|
||||
origin_destination.fget.short_description = _("Origin → Destination")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.origin.name} {self.departure_time} → {self.destination.name} {self.arrival_time}" \
|
||||
f" - {self.service_id}"
|
||||
return self.origin_destination
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Trip")
|
||||
verbose_name_plural = _("Trips")
|
||||
indexes = (models.Index(fields=['route']), models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class StopTime(models.Model):
|
||||
@ -510,6 +644,7 @@ class StopTime(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Stop time")
|
||||
verbose_name_plural = _("Stop times")
|
||||
indexes = (models.Index(fields=['stop']), models.Index(fields=['trip']),)
|
||||
|
||||
|
||||
class Calendar(models.Model):
|
||||
@ -555,10 +690,10 @@ class Calendar(models.Model):
|
||||
verbose_name=_("End date"),
|
||||
)
|
||||
|
||||
transport_type = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Transport type"),
|
||||
choices=TransportType,
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@ -568,6 +703,7 @@ class Calendar(models.Model):
|
||||
verbose_name = _("Calendar")
|
||||
verbose_name_plural = _("Calendars")
|
||||
ordering = ("id",)
|
||||
indexes = (models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class CalendarDate(models.Model):
|
||||
@ -600,6 +736,7 @@ class CalendarDate(models.Model):
|
||||
verbose_name = _("Calendar date")
|
||||
verbose_name_plural = _("Calendar dates")
|
||||
ordering = ("id",)
|
||||
indexes = (models.Index(fields=['service']), models.Index(fields=['date']),)
|
||||
|
||||
|
||||
class Transfer(models.Model):
|
||||
@ -668,10 +805,17 @@ class FeedInfo(models.Model):
|
||||
verbose_name=_("Feed version"),
|
||||
)
|
||||
|
||||
gtfs_feed = models.ForeignKey(
|
||||
GTFSFeed,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("GTFS feed"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Feed info")
|
||||
verbose_name_plural = _("Feed infos")
|
||||
ordering = ("publisher_name",)
|
||||
indexes = (models.Index(fields=['gtfs_feed']),)
|
||||
|
||||
|
||||
class TripUpdate(models.Model):
|
||||
@ -705,6 +849,7 @@ class TripUpdate(models.Model):
|
||||
verbose_name_plural = _("Trip updates")
|
||||
ordering = ("start_date", "trip",)
|
||||
unique_together = ("trip", "start_date", "start_time",)
|
||||
indexes = (models.Index(fields=['trip']),)
|
||||
|
||||
|
||||
class StopTimeUpdate(models.Model):
|
||||
@ -753,3 +898,4 @@ class StopTimeUpdate(models.Model):
|
||||
verbose_name_plural = _("Stop time updates")
|
||||
ordering = ("trip_update", "stop_time",)
|
||||
unique_together = ("trip_update", "stop_time",)
|
||||
indexes = (models.Index(fields=['trip_update']), models.Index(fields=['stop_time']),)
|
||||
|
Loading…
Reference in New Issue
Block a user