From 6cb4be80f67d26ed423275bfc223145f2142b88e Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sat, 2 Jan 2021 16:29:50 +0100 Subject: [PATCH] Add Address.x and Address.y coordinates - the Address.x and Address.y properties use the UTMCoordinate class behind the scenes - x and y are simple coordinates in an x-y plane - the (0, 0) origin is the southwest corner of Address.city.viewport --- setup.cfg | 3 +++ src/urban_meal_delivery/db/addresses.py | 22 ++++++++++++++++++++++ src/urban_meal_delivery/db/cities.py | 24 +++++++++++++++++++++++- tests/db/test_addresses.py | 12 ++++++++++++ tests/db/test_cities.py | 9 +++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 91f2727..7443dc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -134,8 +134,11 @@ per-file-ignores = WPS115, # Numbers are normal in config files. WPS432, + # No real string constant over-use. src/urban_meal_delivery/db/addresses.py: WPS226, + src/urban_meal_delivery/db/cities.py: + WPS226, src/urban_meal_delivery/db/orders.py: WPS226, tests/*.py: diff --git a/src/urban_meal_delivery/db/addresses.py b/src/urban_meal_delivery/db/addresses.py index 8a9337b..bfb848c 100644 --- a/src/urban_meal_delivery/db/addresses.py +++ b/src/urban_meal_delivery/db/addresses.py @@ -1,11 +1,14 @@ """Provide the ORM's `Address` model.""" +from typing import Any + import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.dialects import postgresql from sqlalchemy.ext import hybrid from urban_meal_delivery.db import meta +from urban_meal_delivery.db import utils class Address(meta.Base): @@ -64,6 +67,15 @@ class Address(meta.Base): foreign_keys='[Order._delivery_address_id]', ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create a new address.""" + # Call SQLAlchemy's default `.__init__()` method. + super().__init__(*args, **kwargs) + + self._utm_coordinates = utils.UTMCoordinate( + self.latitude, self.longitude, relative_to=self.city.as_origin, + ) + def __repr__(self) -> str: """Non-literal text representation.""" return '<{cls}({street} in {city})>'.format( @@ -80,3 +92,13 @@ class Address(meta.Base): `.is_primary` indicates the first in a group of `Address` objects. """ return self.id == self._primary_id + + @property + def x(self) -> int: # noqa=WPS111 + """The `.easting` of the address in meters, relative to the `.city`.""" + return self._utm_coordinates.x + + @property + def y(self) -> int: # noqa=WPS111 + """The `.northing` of the address in meters, relative to the `.city`.""" + return self._utm_coordinates.y diff --git a/src/urban_meal_delivery/db/cities.py b/src/urban_meal_delivery/db/cities.py index 2a36ced..a8e1360 100644 --- a/src/urban_meal_delivery/db/cities.py +++ b/src/urban_meal_delivery/db/cities.py @@ -1,12 +1,13 @@ """Provide the ORM's `City` model.""" -from typing import Dict +from typing import Any, Dict 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 City(meta.Base): @@ -45,6 +46,18 @@ class City(meta.Base): # Relationships addresses = orm.relationship('Address', back_populates='city') + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create a new city.""" + # Call SQLAlchemy's default `.__init__()` method. + super().__init__(*args, **kwargs) + + # Take the "lower left" of the viewport as the origin + # of a Cartesian coordinate system. + lower_left = self.viewport['southwest'] + self._origin = utils.UTMCoordinate( + lower_left['latitude'], lower_left['longitude'], + ) + def __repr__(self) -> str: """Non-literal text representation.""" return '<{cls}({name})>'.format(cls=self.__class__.__name__, name=self.name) @@ -81,3 +94,12 @@ class City(meta.Base): 'longitude': self._southwest_longitude, }, } + + @property + def as_origin(self) -> utils.UTMCoordinate: + """The lower left corner of the `.viewport` in UTM coordinates. + + This property serves as the `relative_to` argument to the + `UTMConstructor` when representing an `Address` in the x-y plane. + """ + return self._origin diff --git a/tests/db/test_addresses.py b/tests/db/test_addresses.py index 4086f9c..2f51bb8 100644 --- a/tests/db/test_addresses.py +++ b/tests/db/test_addresses.py @@ -122,3 +122,15 @@ class TestProperties: result = address.is_primary assert result is False + + def test_x_is_positive(self, address): + """Test `Address.x` property.""" + result = address.x + + assert result > 0 + + def test_y_is_positive(self, address): + """Test `Address.y` property.""" + result = address.y + + assert result > 0 diff --git a/tests/db/test_cities.py b/tests/db/test_cities.py index 51aefc7..94d69fe 100644 --- a/tests/db/test_cities.py +++ b/tests/db/test_cities.py @@ -3,6 +3,7 @@ import pytest +from tests.db.utils import test_coordinates as consts from urban_meal_delivery import db @@ -63,3 +64,11 @@ class TestProperties: assert len(result) == 2 assert result['latitude'] == pytest.approx(city_data[f'_{corner}_latitude']) assert result['longitude'] == pytest.approx(city_data[f'_{corner}_longitude']) + + def test_city_in_utm_coordinates(self, city): + """Test `City.as_origin` property.""" + result = city.as_origin + + assert result.zone == consts.ZONE + assert consts.MIN_EASTING < result.easting < consts.MAX_EASTING + assert consts.MIN_NORTHING < result.northing < consts.MAX_NORTHING