Add DistanceMatrix class
- the class stores the data of a distance matrix between all addresses + air distances + bicycle distances - in addition, the "path" returned by the Google Directions API are also stored as a JSON serialized sequence of latitude-longitude pairs - we assume a symmetric graph
This commit is contained in:
parent
28368cc30a
commit
cc75307e5a
7 changed files with 442 additions and 2 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"""Provide the ORM models and a connection to the database."""
|
||||
|
||||
from urban_meal_delivery.db.addresses import Address
|
||||
from urban_meal_delivery.db.addresses_addresses import DistanceMatrix
|
||||
from urban_meal_delivery.db.addresses_pixels import AddressPixelAssociation
|
||||
from urban_meal_delivery.db.cities import City
|
||||
from urban_meal_delivery.db.connection import connection
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@ class Address(meta.Base):
|
|||
|
||||
# Relationships
|
||||
city = orm.relationship('City', back_populates='addresses')
|
||||
_distances1 = orm.relationship(
|
||||
'DistanceMatrix',
|
||||
back_populates='first_address',
|
||||
foreign_keys='[DistanceMatrix.first_address_id, DistanceMatrix.city_id]',
|
||||
)
|
||||
_distances2 = orm.relationship(
|
||||
'DistanceMatrix',
|
||||
back_populates='second_address',
|
||||
foreign_keys='[DistanceMatrix.second_address_id, DistanceMatrix.city_id]',
|
||||
)
|
||||
restaurants = orm.relationship('Restaurant', back_populates='address')
|
||||
orders_picked_up = orm.relationship(
|
||||
'Order',
|
||||
|
|
|
|||
114
src/urban_meal_delivery/db/addresses_addresses.py
Normal file
114
src/urban_meal_delivery/db/addresses_addresses.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""Model for the relationship between two `Address` objects (= distance matrix)."""
|
||||
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from urban_meal_delivery.db import meta
|
||||
from urban_meal_delivery.db import utils
|
||||
|
||||
|
||||
class DistanceMatrix(meta.Base):
|
||||
"""Distance matrix between `Address` objects.
|
||||
|
||||
Models the pairwise distances between two `Address` objects,
|
||||
including directions for a `Courier` to get from one `Address` to another.
|
||||
|
||||
As the couriers are on bicycles, we model the distance matrix
|
||||
as a symmetric graph (i.e., same distance in both directions).
|
||||
|
||||
Implements an association pattern between `Address` and `Address`.
|
||||
|
||||
Further info:
|
||||
https://docs.sqlalchemy.org/en/stable/orm/basic_relationships.html#association-object # noqa:E501
|
||||
"""
|
||||
|
||||
__tablename__ = 'addresses_addresses'
|
||||
|
||||
# Columns
|
||||
first_address_id = sa.Column(sa.Integer, primary_key=True)
|
||||
second_address_id = sa.Column(sa.Integer, primary_key=True)
|
||||
city_id = sa.Column(sa.SmallInteger, nullable=False)
|
||||
# Distances are measured in meters.
|
||||
air_distance = sa.Column(sa.Integer, nullable=False)
|
||||
bicycle_distance = sa.Column(sa.Integer, nullable=True)
|
||||
# The duration is measured in seconds.
|
||||
bicycle_duration = sa.Column(sa.Integer, nullable=True)
|
||||
# An array of latitude-longitude pairs approximating a courier's way.
|
||||
directions = sa.Column(postgresql.JSON, nullable=True)
|
||||
|
||||
# Constraints
|
||||
__table_args__ = (
|
||||
# The two `Address` objects must be in the same `.city`.
|
||||
sa.ForeignKeyConstraint(
|
||||
['first_address_id', 'city_id'],
|
||||
['addresses.id', 'addresses.city_id'],
|
||||
onupdate='RESTRICT',
|
||||
ondelete='RESTRICT',
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['second_address_id', 'city_id'],
|
||||
['addresses.id', 'addresses.city_id'],
|
||||
onupdate='RESTRICT',
|
||||
ondelete='RESTRICT',
|
||||
),
|
||||
# Each `Address`-`Address` pair only has one distance.
|
||||
sa.UniqueConstraint('first_address_id', 'second_address_id'),
|
||||
sa.CheckConstraint(
|
||||
'first_address_id < second_address_id',
|
||||
name='distances_are_symmetric_for_bicycles',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'0 <= air_distance AND air_distance < 20000', name='realistic_air_distance',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'bicycle_distance < 25000', # `.bicycle_distance` may not be negatative
|
||||
name='realistic_bicycle_distance', # due to the constraint below.
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'air_distance <= bicycle_distance', name='air_distance_is_shortest',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'0 <= bicycle_duration AND bicycle_duration <= 3600',
|
||||
name='realistic_bicycle_travel_time',
|
||||
),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
first_address = orm.relationship(
|
||||
'Address',
|
||||
back_populates='_distances1',
|
||||
foreign_keys='[DistanceMatrix.first_address_id, DistanceMatrix.city_id]',
|
||||
)
|
||||
second_address = orm.relationship(
|
||||
'Address',
|
||||
back_populates='_distances2',
|
||||
foreign_keys='[DistanceMatrix.second_address_id, DistanceMatrix.city_id]',
|
||||
)
|
||||
|
||||
# We do not implement a `.__init__()` method and leave that to SQLAlchemy.
|
||||
# Instead, we use `hasattr()` to check for uninitialized attributes. grep:86ffc14e
|
||||
|
||||
@property
|
||||
def path(self) -> List[utils.Location]:
|
||||
"""The couriers' path from `.first_address` to `.second_address`.
|
||||
|
||||
The returned `Location`s all relates to `.first_address.city.southwest`.
|
||||
|
||||
Implementation detail: This property is cached as none of the
|
||||
underlying attributes (i.e., `.directions`) are to be changed.
|
||||
"""
|
||||
if not hasattr(self, '_path'): # noqa:WPS421 note:86ffc14e
|
||||
inner_points = [
|
||||
utils.Location(point[0], point[1])
|
||||
for point in json.loads(self.directions)
|
||||
]
|
||||
for point in inner_points:
|
||||
point.relate_to(self.first_address.city.southwest)
|
||||
|
||||
self._path = inner_points
|
||||
|
||||
return self._path
|
||||
|
|
@ -10,7 +10,7 @@ class AddressPixelAssociation(meta.Base):
|
|||
"""Association pattern between `Address` and `Pixel`.
|
||||
|
||||
This approach is needed here mainly because it implicitly
|
||||
updates the `_city_id` and `_grid_id` columns.
|
||||
updates the `city_id` and `grid_id` columns.
|
||||
|
||||
Further info:
|
||||
https://docs.sqlalchemy.org/en/stable/orm/basic_relationships.html#association-object # noqa:E501
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue