218 lines
8.9 KiB
Python
218 lines
8.9 KiB
Python
"""Tests that the geometry base classes in :mod:`pyrate.plan.geometry.geospatial` work correctly."""
|
|
|
|
# Python standard
|
|
from copy import copy
|
|
from copy import deepcopy
|
|
from json import loads
|
|
|
|
# Typing
|
|
from typing import Any
|
|
from typing import Sequence
|
|
from typing import Tuple
|
|
|
|
# Generic testing
|
|
from unittest import TestCase
|
|
|
|
# Hypothesis testing
|
|
from hypothesis import given
|
|
from hypothesis import HealthCheck
|
|
from hypothesis import Phase
|
|
from hypothesis import settings
|
|
import hypothesis.strategies as st
|
|
|
|
# Package under test
|
|
from pyrate.plan.geometry import CartesianLocation
|
|
from pyrate.plan.geometry import CartesianPolygon
|
|
from pyrate.plan.geometry import CartesianRoute
|
|
from pyrate.plan.geometry import Direction
|
|
from pyrate.plan.geometry import Geospatial
|
|
from pyrate.plan.geometry import PolarLocation
|
|
from pyrate.plan.geometry import PolarPolygon
|
|
from pyrate.plan.geometry import PolarRoute
|
|
|
|
# Hypothesis testing
|
|
from pyrate.common.testing.strategies.geometry import geospatial_objects
|
|
|
|
|
|
_CARTESIAN_LOCATION_1 = CartesianLocation(5003.0, 139.231)
|
|
_CARTESIAN_LOCATION_2 = CartesianLocation(600.1, 139.231)
|
|
_POLAR_LOCATION_1 = PolarLocation(65.01, -180.0)
|
|
_POLAR_LOCATION_2 = PolarLocation(-80.3, -180.0)
|
|
|
|
|
|
class TestStringRepresentations(TestCase):
|
|
"""Makes sure that the string conversion with ``__str__`` and ``__repr__`` works."""
|
|
|
|
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
|
|
(
|
|
_CARTESIAN_LOCATION_1,
|
|
"CartesianLocation(east=5003.0, north=139.231)",
|
|
),
|
|
(
|
|
PolarLocation(65.01, -180.0),
|
|
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)",
|
|
),
|
|
(
|
|
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
|
|
"CartesianPolygon(locations=[(5003.0, 139.231), (5003.0, 139.231), (5003.0, 139.231), "
|
|
"(5003.0, 139.231)])",
|
|
),
|
|
(
|
|
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
|
"PolarPolygon(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
|
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
|
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
|
|
),
|
|
(
|
|
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
|
|
"CartesianRoute(locations=[(5003.0, 139.231), (600.1, 139.231), (5003.0, 139.231)])",
|
|
),
|
|
(
|
|
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
|
"PolarRoute(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
|
"PolarLocation(latitude=-80.3, longitude=-180.0), "
|
|
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
|
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
|
|
),
|
|
]
|
|
|
|
def test_conversions(self) -> None:
|
|
"""Makes sure that all given geospatial objects can be converted."""
|
|
|
|
for geospatial, desired_str in TestStringRepresentations._GROUND_TRUTH:
|
|
|
|
with self.subTest(f"{type(geospatial)}.__str__"):
|
|
self.assertEqual(str(geospatial), desired_str)
|
|
|
|
with self.subTest(f"{type(geospatial)}.__repr__"):
|
|
self.assertEqual(repr(geospatial), desired_str)
|
|
|
|
|
|
class TestGeoJsonRepresentations(TestCase):
|
|
"""Makes sure that the conversion to GeoJSON via the common property ``__geo_interface__`` works."""
|
|
|
|
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
|
|
(
|
|
_CARTESIAN_LOCATION_1,
|
|
'{"type": "Feature", "geometry": {"type": "Point", '
|
|
'"coordinates": [5003.0, 139.231]}, "properties": {}}',
|
|
),
|
|
(
|
|
PolarLocation(65.01, -180.0),
|
|
'{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-180.0, 65.01]}, '
|
|
'"properties": {}}',
|
|
),
|
|
(
|
|
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
|
|
'{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": '
|
|
"[[[5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231]]]}, "
|
|
'"properties": {}}',
|
|
),
|
|
(
|
|
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
|
'{"type": "Feature", "geometry": {"type": "Polygon", '
|
|
'"coordinates": [[[-180.0, 65.01], [-180.0, 65.01], [-180.0, 65.01]]]}, "properties": {}}',
|
|
),
|
|
(
|
|
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
|
|
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
|
|
'[[5003.0, 139.231], [600.1, 139.231], [5003.0, 139.231]]}, "properties": {}}',
|
|
),
|
|
(
|
|
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1]),
|
|
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
|
|
'[[-180.0, 65.01], [-180.0, -80.3], [-180.0, 65.01]]}, "properties": {}}',
|
|
),
|
|
]
|
|
|
|
def test_conversions(self) -> None:
|
|
"""Makes sure that all given geospatial objects can be converted."""
|
|
|
|
for geospatial, desired_geojson in TestGeoJsonRepresentations._GROUND_TRUTH:
|
|
for indent in (None, 1, 8):
|
|
with self.subTest(f"{type(geospatial)} with indent={indent}"):
|
|
geojson = geospatial.to_geo_json(indent=indent)
|
|
# load as JSON get get better error messages and become whitespace independent
|
|
self.assertDictEqual(loads(geojson), loads(desired_geojson))
|
|
|
|
|
|
class TestIdentifiers(TestCase):
|
|
"""Makes sure that identifiers are validated correctly.
|
|
|
|
The test is only performed on polar locations for simplicity and because validation is handled in the
|
|
abstract common parent class :class:`pyrate.plan.geometry.Geospatial` anyway.
|
|
"""
|
|
|
|
@given(st.integers(min_value=0, max_value=(2**63) - 1))
|
|
def test_on_locations_success(self, integer: int) -> None: # pylint: disable=no-self-use
|
|
"""Tests that valid identifiers are accepted."""
|
|
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
|
|
|
|
@given(
|
|
st.one_of(
|
|
st.integers(max_value=-1), # negative numbers
|
|
st.integers(min_value=2**63), # very large numbers
|
|
)
|
|
)
|
|
def test_on_locations_rejected(self, integer: int) -> None:
|
|
"""Tests that invalid identifiers are rejected."""
|
|
with self.assertRaises(AssertionError):
|
|
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
|
|
|
|
|
|
class TestEqualityMethods(TestCase):
|
|
"""Test the various equality methods."""
|
|
|
|
@given(geospatial_objects(stable=True))
|
|
@settings(
|
|
max_examples=200,
|
|
suppress_health_check=(HealthCheck.data_too_large,),
|
|
phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), # Do not shrink as it takes long
|
|
)
|
|
def test_equality_after_translation(self, geospatial: Any) -> None:
|
|
"""Tests that translated objects are only equal under sufficient tolerance."""
|
|
|
|
# We discard the second output since it differs between cartesian and polar objects
|
|
translated, _ = geospatial.translate(direction=Direction.North, distance=0.5)
|
|
|
|
# Try since generated primitives might cause an exception to be thrown
|
|
# e.g. if projected routes become length 0
|
|
try:
|
|
# They should not be equal
|
|
self.assertNotEqual(geospatial, translated)
|
|
self.assertFalse(geospatial.equals(translated))
|
|
self.assertFalse(geospatial.equals_exact(translated, tolerance=0.0))
|
|
if hasattr(geospatial, "equals_almost_congruent"):
|
|
self.assertFalse(
|
|
geospatial.equals_almost_congruent(translated, abs_tolerance=0.0, rel_tolerance=0.0)
|
|
)
|
|
|
|
# They should be equal within some tolerance (the tolerance needs to be large for the polar
|
|
# variants)
|
|
# TODO(Someone): re-enable; see #114
|
|
# self.assertTrue(geospatial.equals_exact(translated, tolerance=5))
|
|
# self.assertTrue(geospatial.almost_equals(translated, decimal=-1))
|
|
|
|
# We do not use `equals_almost_congruent` as it is not a per-coordinate difference and might cause
|
|
# a very large symmetric difference on very large objects
|
|
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
class TestCopyAndDeepcopy(TestCase):
|
|
"""Tests that all geometric objects can be deep-copied."""
|
|
|
|
@given(geospatial_objects())
|
|
@settings(max_examples=500)
|
|
def test_is_copyable(self, geospatial: Any) -> None:
|
|
"""Tests that copies can be made and are equal to the original."""
|
|
|
|
# Check copy
|
|
copied = copy(geospatial)
|
|
self.assertEqual(geospatial, copied)
|
|
|
|
# Check deepcopy
|
|
deep_copied = deepcopy(geospatial)
|
|
self.assertEqual(geospatial, deep_copied)
|