From 605ade4078e352c75ccd62ae5ad4b8f1f8feb5d7 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 26 Jan 2021 17:02:51 +0100 Subject: [PATCH] Add `Pixel.northeast/southwest` properties - the properties are needed for the drawing functionalitites --- src/urban_meal_delivery/db/pixels.py | 47 +++++++++++++++++++ src/urban_meal_delivery/db/utils/locations.py | 7 ++- tests/db/test_grids.py | 9 +++- tests/db/test_pixels.py | 28 +++++++++++ tests/db/utils/test_locations.py | 7 +++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/urban_meal_delivery/db/pixels.py b/src/urban_meal_delivery/db/pixels.py index f75e9e3..c182206 100644 --- a/src/urban_meal_delivery/db/pixels.py +++ b/src/urban_meal_delivery/db/pixels.py @@ -1,9 +1,11 @@ """Provide the ORM's `Pixel` model.""" import sqlalchemy as sa +import utm from sqlalchemy import orm from urban_meal_delivery.db import meta +from urban_meal_delivery.db import utils class Pixel(meta.Base): @@ -58,3 +60,48 @@ class Pixel(meta.Base): def area(self) -> float: """The area of a pixel in square kilometers.""" return self.grid.pixel_area + + @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 diff --git a/src/urban_meal_delivery/db/utils/locations.py b/src/urban_meal_delivery/db/utils/locations.py index 741edfe..b6ef41e 100644 --- a/src/urban_meal_delivery/db/utils/locations.py +++ b/src/urban_meal_delivery/db/utils/locations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Tuple import utm @@ -82,6 +82,11 @@ class Location: """The UTM zone of the location.""" return f'{self._zone}{self._band}' + @property + def zone_details(self) -> Tuple[int, str]: + """The UTM zone of the location as the zone number and the band.""" + return (self._zone, self._band) + def __eq__(self, other: object) -> bool: """Check if two `Location` objects are the same location.""" if not isinstance(other, Location): diff --git a/tests/db/test_grids.py b/tests/db/test_grids.py index e28baf2..2babf25 100644 --- a/tests/db/test_grids.py +++ b/tests/db/test_grids.py @@ -205,7 +205,7 @@ class TestGridification: @pytest.mark.db @pytest.mark.no_cover @pytest.mark.parametrize('side_length', [250, 500, 1_000, 2_000, 4_000, 8_000]) - def test_make_random_grids( # noqa:WPS211 + def test_make_random_grids( # noqa:WPS211,WPS218 self, db_session, city, make_address, make_restaurant, make_order, side_length, ): """With 100 random `Address` objects, a grid must have ... @@ -228,5 +228,12 @@ class TestGridification: assert isinstance(result, db.Grid) assert 1 <= len(result.pixels) <= n_pixels_x * n_pixels_y + # Sanity checks for `Pixel.southwest` and `Pixel.northeast`. + for pixel in result.pixels: + assert abs(pixel.southwest.x - pixel.n_x * side_length) < 2 + assert abs(pixel.southwest.y - pixel.n_y * side_length) < 2 + assert abs(pixel.northeast.x - (pixel.n_x + 1) * side_length) < 2 + assert abs(pixel.northeast.y - (pixel.n_y + 1) * side_length) < 2 + db_session.add(result) db_session.commit() diff --git a/tests/db/test_pixels.py b/tests/db/test_pixels.py index 3ebfb26..d5acc4a 100644 --- a/tests/db/test_pixels.py +++ b/tests/db/test_pixels.py @@ -87,3 +87,31 @@ class TestProperties: result = pixel.area assert result == 1.0 + + def test_northeast(self, pixel, city): + """Test `Pixel.northeast` property.""" + result = pixel.northeast + + assert abs(result.x - pixel.side_length) < 2 + assert abs(result.y - pixel.side_length) < 2 + + def test_northeast_is_cached(self, pixel): + """Test `Pixel.northeast` property.""" + result1 = pixel.northeast + result2 = pixel.northeast + + assert result1 is result2 + + def test_southwest(self, pixel, city): + """Test `Pixel.southwest` property.""" + result = pixel.southwest + + assert abs(result.x) < 2 + assert abs(result.y) < 2 + + def test_southwest_is_cached(self, pixel): + """Test `Pixel.southwest` property.""" + result1 = pixel.southwest + result2 = pixel.southwest + + assert result1 is result2 diff --git a/tests/db/utils/test_locations.py b/tests/db/utils/test_locations.py index 51750e2..8eb0263 100644 --- a/tests/db/utils/test_locations.py +++ b/tests/db/utils/test_locations.py @@ -140,6 +140,13 @@ class TestProperties: assert result == ZONE + def test_zone_details(self, location): + """Test `Location.zone_details` property.""" + result = location.zone_details + + zone, band = result + assert ZONE == f'{zone}{band}' + class TestRelateTo: """Test the `Location.relate_to()` method and the `.x` and `.y` properties."""