Restructurate GTFS feeds into dedicated models

This commit is contained in:
2024-05-09 19:28:19 +02:00
parent 820fc0cc19
commit 11949228ee
11 changed files with 1594 additions and 950 deletions

View File

@ -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']),)