2021-01-03 19:33:36 +01:00
|
|
|
"""Provide the ORM's `Pixel` model."""
|
|
|
|
|
|
|
|
|
|
import sqlalchemy as sa
|
2021-01-26 17:02:51 +01:00
|
|
|
import utm
|
2021-01-03 19:33:36 +01:00
|
|
|
from sqlalchemy import orm
|
|
|
|
|
|
|
|
|
|
from urban_meal_delivery.db import meta
|
2021-01-26 17:02:51 +01:00
|
|
|
from urban_meal_delivery.db import utils
|
2021-01-03 19:33:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Pixel(meta.Base):
|
|
|
|
|
"""A pixel in a `Grid`.
|
|
|
|
|
|
|
|
|
|
Square pixels aggregate `Address` objects within a `City`.
|
|
|
|
|
Every `Address` belongs to exactly one `Pixel` in a `Grid`.
|
|
|
|
|
|
2021-01-24 18:57:44 +01:00
|
|
|
Every `Pixel` has a unique `n_x`-`n_y` coordinate within the `Grid`.
|
2021-01-03 19:33:36 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
__tablename__ = 'pixels'
|
|
|
|
|
|
|
|
|
|
# Columns
|
|
|
|
|
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # noqa:WPS125
|
2021-01-05 22:37:12 +01:00
|
|
|
grid_id = sa.Column(sa.SmallInteger, nullable=False, index=True)
|
2021-01-03 19:33:36 +01:00
|
|
|
n_x = sa.Column(sa.SmallInteger, nullable=False, index=True)
|
|
|
|
|
n_y = sa.Column(sa.SmallInteger, nullable=False, index=True)
|
|
|
|
|
|
|
|
|
|
# Constraints
|
|
|
|
|
__table_args__ = (
|
|
|
|
|
sa.ForeignKeyConstraint(
|
|
|
|
|
['grid_id'], ['grids.id'], onupdate='RESTRICT', ondelete='RESTRICT',
|
|
|
|
|
),
|
|
|
|
|
sa.CheckConstraint('0 <= n_x', name='n_x_is_positive'),
|
|
|
|
|
sa.CheckConstraint('0 <= n_y', name='n_y_is_positive'),
|
|
|
|
|
# Needed by a `ForeignKeyConstraint` in `AddressPixelAssociation`.
|
|
|
|
|
sa.UniqueConstraint('id', 'grid_id'),
|
|
|
|
|
# Each coordinate within the same `grid` is used at most once.
|
|
|
|
|
sa.UniqueConstraint('grid_id', 'n_x', 'n_y'),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Relationships
|
|
|
|
|
grid = orm.relationship('Grid', back_populates='pixels')
|
|
|
|
|
addresses = orm.relationship('AddressPixelAssociation', back_populates='pixel')
|
2021-01-07 12:45:32 +01:00
|
|
|
forecasts = orm.relationship('Forecast', back_populates='pixel')
|
2021-01-03 19:33:36 +01:00
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
"""Non-literal text representation."""
|
|
|
|
|
return '<{cls}: ({x}, {y})>'.format(
|
|
|
|
|
cls=self.__class__.__name__, x=self.n_x, y=self.n_y,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Convenience properties
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def side_length(self) -> int:
|
|
|
|
|
"""The length of one side of a pixel in meters."""
|
|
|
|
|
return self.grid.side_length
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def area(self) -> float:
|
|
|
|
|
"""The area of a pixel in square kilometers."""
|
|
|
|
|
return self.grid.pixel_area
|
2021-01-26 17:02:51 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def northeast(self) -> utils.Location:
|
|
|
|
|
"""The pixel's northeast corner, relative to `.grid.city.southwest`.
|
|
|
|
|
|
|
|
|
|
Implementation detail: This property is cached as none of the
|
|
|
|
|
underlying attributes to calculate the value are to be changed.
|
|
|
|
|
"""
|
|
|
|
|
if not hasattr(self, '_northeast'): # noqa:WPS421 note:d334120e
|
|
|
|
|
# The origin is the southwest corner of the `.grid.city`'s viewport.
|
|
|
|
|
easting_origin = self.grid.city.southwest.easting
|
|
|
|
|
northing_origin = self.grid.city.southwest.northing
|
|
|
|
|
|
|
|
|
|
# `+1` as otherwise we get the pixel's `.southwest` corner.
|
|
|
|
|
easting = easting_origin + ((self.n_x + 1) * self.side_length)
|
|
|
|
|
northing = northing_origin + ((self.n_y + 1) * self.side_length)
|
|
|
|
|
zone, band = self.grid.city.southwest.zone_details
|
|
|
|
|
latitude, longitude = utm.to_latlon(easting, northing, zone, band)
|
|
|
|
|
|
|
|
|
|
self._northeast = utils.Location(latitude, longitude)
|
|
|
|
|
self._northeast.relate_to(self.grid.city.southwest)
|
|
|
|
|
|
|
|
|
|
return self._northeast
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def southwest(self) -> utils.Location:
|
|
|
|
|
"""The pixel's northeast corner, relative to `.grid.city.southwest`.
|
|
|
|
|
|
|
|
|
|
Implementation detail: This property is cached as none of the
|
|
|
|
|
underlying attributes to calculate the value are to be changed.
|
|
|
|
|
"""
|
|
|
|
|
if not hasattr(self, '_southwest'): # noqa:WPS421 note:d334120e
|
|
|
|
|
# The origin is the southwest corner of the `.grid.city`'s viewport.
|
|
|
|
|
easting_origin = self.grid.city.southwest.easting
|
|
|
|
|
northing_origin = self.grid.city.southwest.northing
|
|
|
|
|
|
|
|
|
|
easting = easting_origin + (self.n_x * self.side_length)
|
|
|
|
|
northing = northing_origin + (self.n_y * self.side_length)
|
|
|
|
|
zone, band = self.grid.city.southwest.zone_details
|
|
|
|
|
latitude, longitude = utm.to_latlon(easting, northing, zone, band)
|
|
|
|
|
|
|
|
|
|
self._southwest = utils.Location(latitude, longitude)
|
|
|
|
|
self._southwest.relate_to(self.grid.city.southwest)
|
|
|
|
|
|
|
|
|
|
return self._southwest
|