From 407ec8426a186d270c5ee5c7d40507a66a24412c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 31 Oct 2026 15:59:45 +0100 Subject: [PATCH] Give the circular buffer, network point, pose and rigid geometry temporal types their classes PyMEOS binds the four temporal types of the MEOS 1.4 families as it binds the temporal points: TCbuffer, TNpoint, TPose and TRgeometry, each with its instant, sequence and sequence set classes, and the base values Cbuffer, Npoint, Nsegment and Pose with the sets CbufferSet, NpointSet and PoseSet. The value of a temporal rigid geometry is a shapely geometry, its reference geometry placed by the pose. The factory and the package exports name every class. Each temporal class implements the methods the Temporal base declares through the typed MEOS functions of its family: from_base_time and from_base_temporal, from_mfjson, start_value, end_value, value_set and value_at_timestamp. TRgeometry also carries the segments, the deletion of a time value, the restriction before or after a timestamp, to_instant and the sequence accessors of its own trgeometry functions. The pose accessors are yaw and quaternion, the MEOS 1.4 names. TCbuffer and TRgeometry carry the ever and always spatial relationships MEOS defines for them, with the names TCbuffer and TPoint use: is_ever_contains, is_ever_covers, is_ever_disjoint, is_ever_within_distance, ever_intersects and ever_touches with their always twins, each against the geometries, base values and temporal values of the same type MEOS defines it for. For a geometry or a base value containing or covering self they carry is_ever_contained_in and is_ever_coveredby with their always twins; coveredby is the covers relationship with the container first, named and computed as PostGIS's coveredby and geography_coveredby. TCbuffer carries the temporal relationships as well. MEOS defines no spatial relationship for the network point or the pose. A base value is built from its text or by keyword, as Npoint(route=1, position=0.5), Cbuffer(point=..., radius=...) and Pose(point=..., theta=...). Its WKB output is the bytes PyMEOS-CFFI returns, its WKB input and a set's elements are a bytes object and a Python list whose length is their own, and a network point set takes each element as the Npoint struct npointset_make reads. The text of a value is its asText, as in MobilityDB: str of a pose, a circular buffer, a network point and their sets is pose_as_text, cbuffer_as_text, npoint_as_text and spatialset_as_text, Pose(POINT(1 1),0.5), the sets gaining as_text, as_wkt and as_ewkt as GeoSet has them, and a temporal rigid geometry reads its text through trgeometry_in and writes it through trgeometry_as_text and trgeometry_as_ewkt, the reference geometry before the poses; repr of a temporal value is the class name around its text. The tests cover every class: construction from text, from the base values and from MF-JSON, the outputs, the accessors, the comparisons and the transformations of each family. --- pymeos/__init__.py | 23 + pymeos/collections/__init__.py | 10 + pymeos/collections/cbuffer/__init__.py | 4 + pymeos/collections/cbuffer/cbuffer.py | 631 +++++++++++ pymeos/collections/cbuffer/cbufferset.py | 272 +++++ pymeos/collections/npoint/__init__.py | 5 + pymeos/collections/npoint/npoint.py | 399 +++++++ pymeos/collections/npoint/npointset.py | 283 +++++ pymeos/collections/npoint/nsegment.py | 150 +++ pymeos/collections/pose/__init__.py | 4 + pymeos/collections/pose/pose.py | 516 +++++++++ pymeos/collections/pose/poseset.py | 271 +++++ pymeos/factory.py | 66 ++ pymeos/main/__init__.py | 25 + pymeos/main/tcbuffer.py | 1204 ++++++++++++++++++++ pymeos/main/tnpoint.py | 816 ++++++++++++++ pymeos/main/tpose.py | 801 +++++++++++++ pymeos/main/trgeometry.py | 1299 ++++++++++++++++++++++ tests/main/tcbuffer_test.py | 244 ++++ tests/main/tnpoint_test.py | 324 ++++++ tests/main/tpose_test.py | 241 ++++ tests/main/trgeometry_test.py | 254 +++++ 22 files changed, 7842 insertions(+) create mode 100644 pymeos/collections/cbuffer/__init__.py create mode 100644 pymeos/collections/cbuffer/cbuffer.py create mode 100644 pymeos/collections/cbuffer/cbufferset.py create mode 100644 pymeos/collections/npoint/__init__.py create mode 100644 pymeos/collections/npoint/npoint.py create mode 100644 pymeos/collections/npoint/npointset.py create mode 100644 pymeos/collections/npoint/nsegment.py create mode 100644 pymeos/collections/pose/__init__.py create mode 100644 pymeos/collections/pose/pose.py create mode 100644 pymeos/collections/pose/poseset.py create mode 100644 pymeos/main/tcbuffer.py create mode 100644 pymeos/main/tnpoint.py create mode 100644 pymeos/main/tpose.py create mode 100644 pymeos/main/trgeometry.py create mode 100644 tests/main/tcbuffer_test.py create mode 100644 tests/main/tnpoint_test.py create mode 100644 tests/main/tpose_test.py create mode 100644 tests/main/trgeometry_test.py diff --git a/pymeos/__init__.py b/pymeos/__init__.py index 004ad8ab..ae01c556 100644 --- a/pymeos/__init__.py +++ b/pymeos/__init__.py @@ -67,6 +67,22 @@ "TGeogPointInst", "TGeogPointSeq", "TGeogPointSeqSet", + "TNpoint", + "TNpointInst", + "TNpointSeq", + "TNpointSeqSet", + "TCbuffer", + "TCbufferInst", + "TCbufferSeq", + "TCbufferSeqSet", + "TPose", + "TPoseInst", + "TPoseSeq", + "TPoseSeqSet", + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", # temporal "Temporal", "TInstant", @@ -87,6 +103,13 @@ "GeoSet", "GeometrySet", "GeographySet", + "Npoint", + "NpointSet", + "Nsegment", + "Cbuffer", + "CbufferSet", + "Pose", + "PoseSet", # extras "TInterpolation", # aggregators diff --git a/pymeos/collections/__init__.py b/pymeos/collections/__init__.py index a741bad8..d1b067d3 100644 --- a/pymeos/collections/__init__.py +++ b/pymeos/collections/__init__.py @@ -1,6 +1,9 @@ from .base import * +from .cbuffer import * from .geo import * +from .npoint import * from .number import * +from .pose import * from .text import * from .time import * @@ -29,4 +32,11 @@ "FloatSet", "FloatSpan", "FloatSpanSet", + "Npoint", + "NpointSet", + "Nsegment", + "Cbuffer", + "CbufferSet", + "Pose", + "PoseSet", ] diff --git a/pymeos/collections/cbuffer/__init__.py b/pymeos/collections/cbuffer/__init__.py new file mode 100644 index 00000000..43b0d6d8 --- /dev/null +++ b/pymeos/collections/cbuffer/__init__.py @@ -0,0 +1,4 @@ +from .cbuffer import Cbuffer +from .cbufferset import CbufferSet + +__all__ = ["Cbuffer", "CbufferSet"] diff --git a/pymeos/collections/cbuffer/cbuffer.py b/pymeos/collections/cbuffer/cbuffer.py new file mode 100644 index 00000000..234e23ee --- /dev/null +++ b/pymeos/collections/cbuffer/cbuffer.py @@ -0,0 +1,631 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import shapely.geometry.base as shp +from pymeos_cffi import * + +if TYPE_CHECKING: + from ...boxes import STBox + + +class Cbuffer: + """ + Class for representing a circular buffer, that is, a point together with a + radius. + + ``Cbuffer`` objects can be created with a single argument of type string as + in MobilityDB. + + >>> Cbuffer('Cbuffer(Point(1 1), 2)') + + Another possibility is to provide the ``point`` and the ``radius`` + arguments. + + >>> Cbuffer(point=Point(1, 1), radius=2.0) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "cbuffer" + + # ------------------------- Constructors ---------------------------------- + def __init__( + self, + string: str | None = None, + *, + point: str | shp.BaseGeometry | None = None, + radius: float | None = None, + _inner=None, + ): + assert (_inner is not None) or (string is not None) != (point is not None and radius is not None), ( + "Either string must be not None or both point and radius must be not None" + ) + if _inner is not None: + self._inner = _inner + elif string is not None: + self._inner = cbuffer_in(string) + else: + if isinstance(point, str): + gs = geom_in(point, -1) + else: + gs = geometry_to_gserialized(point) + self._inner = cbuffer_make(gs, float(radius)) + + def __copy__(self) -> Cbuffer: + """ + Returns a copy of ``self``. + + Returns: + A :class:`Cbuffer` instance. + + MEOS Functions: + cbuffer_copy + """ + inner_copy = cbuffer_copy(self._inner) + return Cbuffer(_inner=inner_copy) + + @staticmethod + def from_wkb(wkb: bytes) -> Cbuffer: + """ + Returns a `Cbuffer` from its WKB representation. + + Args: + wkb: WKB representation + + Returns: + A new :class:`Cbuffer` instance + + MEOS Functions: + cbuffer_from_wkb + """ + result = cbuffer_from_wkb(wkb) + return Cbuffer(_inner=result) + + @staticmethod + def from_hexwkb(hexwkb: str) -> Cbuffer: + """ + Returns a `Cbuffer` from its WKB representation in hex-encoded ASCII. + + Args: + hexwkb: WKB representation in hex-encoded ASCII + + Returns: + A new :class:`Cbuffer` instance + + MEOS Functions: + cbuffer_from_hexwkb + """ + result = cbuffer_from_hexwkb(hexwkb) + return Cbuffer(_inner=result) + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15): + """ + Returns the string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + cbuffer_as_text + """ + return self.as_text(max_decimals) + + def __repr__(self): + """ + Returns the string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + cbuffer_out + """ + return f"{self.__class__.__name__}({self})" + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use. + + Returns: + A :class:`str` instance. + + MEOS Functions: + cbuffer_as_text + """ + return cbuffer_as_text(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use. + + Returns: + A :class:`str` instance. + + MEOS Functions: + cbuffer_as_text + """ + return cbuffer_as_text(self._inner, max_decimals) + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use. + + Returns: + A :class:`str` instance. + + MEOS Functions: + cbuffer_as_ewkt + """ + return cbuffer_as_ewkt(self._inner, max_decimals) + + def as_wkb(self) -> bytes: + """ + Returns the WKB representation of ``self``. + + Returns: + A :class:`bytes` object with the WKB representation of ``self``. + + MEOS Functions: + cbuffer_as_wkb + """ + return cbuffer_as_wkb(self._inner, 4) + + def as_hexwkb(self) -> str: + """ + Returns the WKB representation of ``self`` in hex-encoded ASCII. + + Returns: + A :class:`str` object with the WKB representation of ``self`` in + hex-encoded ASCII. + + MEOS Functions: + cbuffer_as_hexwkb + """ + return cbuffer_as_hexwkb(self._inner, -1)[0] + + # ------------------------- Conversions ----------------------------------- + def to_geometry(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the geometry representation of ``self``. + + Args: + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + cbuffer_to_geom + """ + return gserialized_to_shapely_geometry(cbuffer_to_geom(self._inner), precision) + + def to_stbox(self) -> STBox: + """ + Returns the bounding box of ``self``. + + Returns: + A new :class:`~pymeos.boxes.STBox` instance. + + MEOS Functions: + cbuffer_to_stbox + """ + from ...boxes import STBox + + return STBox(_inner=cbuffer_to_stbox(self._inner)) + + @staticmethod + def from_geometry(geom: shp.BaseGeometry) -> Cbuffer: + """ + Returns a `Cbuffer` from a geometry. + + Args: + geom: A :class:`~shapely.geometry.base.BaseGeometry` instance. + + Returns: + A new :class:`Cbuffer` instance. + + MEOS Functions: + geom_to_cbuffer + """ + result = geom_to_cbuffer(geometry_to_gserialized(geom)) + return Cbuffer(_inner=result) + + # ------------------------- Accessors ------------------------------------- + def point(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the point of ``self``. + + Args: + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + cbuffer_point + """ + return gserialized_to_shapely_geometry(cbuffer_point(self._inner), precision) + + def radius(self) -> float: + """ + Returns the radius of ``self``. + + Returns: + A :class:`float` instance. + + MEOS Functions: + cbuffer_radius + """ + return cbuffer_radius(self._inner) + + def srid(self) -> int: + """ + Returns the SRID of ``self``. + + Returns: + An :class:`int` instance. + + MEOS Functions: + cbuffer_srid + """ + return cbuffer_srid(self._inner) + + def hash(self) -> int: + """ + Returns the hash of ``self``. + + Returns: + An :class:`int` instance. + + MEOS Functions: + cbuffer_hash + """ + return cbuffer_hash(self._inner) + + def __hash__(self) -> int: + """ + Returns the hash of ``self``. + + Returns: + An :class:`int` instance. + + MEOS Functions: + cbuffer_hash + """ + return cbuffer_hash(self._inner) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> Cbuffer: + """ + Returns `self` rounded to the given number of decimal digits. + + Args: + max_decimals: Maximum number of decimal digits. + + Returns: + A new :class:`Cbuffer` instance. + + MEOS Functions: + cbuffer_round + """ + return Cbuffer(_inner=cbuffer_round(self._inner, max_decimals)) + + def set_srid(self, srid: int) -> Cbuffer: + """ + Returns a new :class:`Cbuffer` with the SRID set to ``srid``. + + Args: + srid: The SRID to set. + + Returns: + A new :class:`Cbuffer` instance. + + MEOS Functions: + cbuffer_set_srid + """ + new_inner = cbuffer_copy(self._inner) + cbuffer_set_srid(new_inner, srid) + return Cbuffer(_inner=new_inner) + + def transform(self, srid: int) -> Cbuffer: + """ + Returns a new :class:`Cbuffer` transformed to another SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`Cbuffer` instance. + + MEOS Functions: + cbuffer_transform + """ + return Cbuffer(_inner=cbuffer_transform(self._inner, srid)) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: Cbuffer | shp.BaseGeometry | STBox) -> float: + """ + Returns the distance between ``self`` and ``other``. + + Args: + other: A :class:`Cbuffer`, geometry or + :class:`~pymeos.boxes.STBox` instance. + + Returns: + A :class:`float` instance. + + MEOS Functions: + distance_cbuffer_cbuffer, distance_cbuffer_geo, + distance_cbuffer_stbox + """ + from ...boxes import STBox + + if isinstance(other, Cbuffer): + return distance_cbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + return distance_cbuffer_geo(self._inner, geometry_to_gserialized(other)) + elif isinstance(other, STBox): + return distance_cbuffer_stbox(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + def nearest_approach_distance(self, other: STBox) -> float: + """ + Returns the nearest approach distance between ``self`` and ``other``. + + Args: + other: A :class:`~pymeos.boxes.STBox` instance. + + Returns: + A :class:`float` instance. + + MEOS Functions: + nad_cbuffer_stbox + """ + from ...boxes import STBox + + if isinstance(other, STBox): + return nad_cbuffer_stbox(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + # ------------------------- Spatial Relationships ------------------------- + def contains(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` contains ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` contains ``other``, ``False`` otherwise. + + MEOS Functions: + contains_cbuffer_cbuffer + """ + return contains_cbuffer_cbuffer(self._inner, other._inner) == 1 + + def covers(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` covers ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` covers ``other``, ``False`` otherwise. + + MEOS Functions: + covers_cbuffer_cbuffer + """ + return covers_cbuffer_cbuffer(self._inner, other._inner) == 1 + + def is_disjoint(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` is disjoint from ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` is disjoint from ``other``, ``False`` + otherwise. + + MEOS Functions: + disjoint_cbuffer_cbuffer + """ + return disjoint_cbuffer_cbuffer(self._inner, other._inner) == 1 + + def intersects(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` intersects ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` intersects ``other``, ``False`` otherwise. + + MEOS Functions: + intersects_cbuffer_cbuffer + """ + return intersects_cbuffer_cbuffer(self._inner, other._inner) == 1 + + def touches(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` touches ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` touches ``other``, ``False`` otherwise. + + MEOS Functions: + touches_cbuffer_cbuffer + """ + return touches_cbuffer_cbuffer(self._inner, other._inner) == 1 + + def is_within_distance(self, other: Cbuffer, distance: float) -> bool: + """ + Returns whether ``self`` is within ``distance`` of ``other``. + + Args: + other: A :class:`Cbuffer` instance. + distance: The distance to check. + + Returns: + ``True`` if ``self`` is within ``distance`` of ``other``, ``False`` + otherwise. + + MEOS Functions: + dwithin_cbuffer_cbuffer + """ + return dwithin_cbuffer_cbuffer(self._inner, other._inner, distance) == 1 + + # ------------------------- Comparisons ----------------------------------- + def __eq__(self, other): + """ + Returns whether ``self`` is equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is equal to ``other``, ``False`` otherwise. + + MEOS Functions: + cbuffer_eq + """ + if isinstance(other, self.__class__): + return cbuffer_eq(self._inner, other._inner) + return False + + def __ne__(self, other): + """ + Returns whether ``self`` is not equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is not equal to ``other``, ``False`` + otherwise. + + MEOS Functions: + cbuffer_ne + """ + if isinstance(other, self.__class__): + return cbuffer_ne(self._inner, other._inner) + return True + + def __lt__(self, other): + """ + Returns whether ``self`` is less than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is less than ``other``, ``False`` otherwise. + + MEOS Functions: + cbuffer_lt + """ + if isinstance(other, self.__class__): + return cbuffer_lt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __le__(self, other): + """ + Returns whether ``self`` is less than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is less than or equal to ``other``, ``False`` + otherwise. + + MEOS Functions: + cbuffer_le + """ + if isinstance(other, self.__class__): + return cbuffer_le(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __gt__(self, other): + """ + Returns whether ``self`` is greater than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is greater than ``other``, ``False`` + otherwise. + + MEOS Functions: + cbuffer_gt + """ + if isinstance(other, self.__class__): + return cbuffer_gt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __ge__(self, other): + """ + Returns whether ``self`` is greater than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is greater than or equal to ``other``, + ``False`` otherwise. + + MEOS Functions: + cbuffer_ge + """ + if isinstance(other, self.__class__): + return cbuffer_ge(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def cmp(self, other: Cbuffer) -> int: + """ + Returns -1, 0, or 1 depending on whether ``self`` is less than, equal + to, or greater than ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + An :class:`int` instance. + + MEOS Functions: + cbuffer_cmp + """ + return cbuffer_cmp(self._inner, other._inner) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`Cbuffer` from a database cursor. Used when + automatically loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + return Cbuffer(string=value) diff --git a/pymeos/collections/cbuffer/cbufferset.py b/pymeos/collections/cbuffer/cbufferset.py new file mode 100644 index 00000000..7be001f1 --- /dev/null +++ b/pymeos/collections/cbuffer/cbufferset.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from typing import overload + +from pymeos_cffi import * + +from ..base import Set +from .cbuffer import Cbuffer + + +class CbufferSet(Set[Cbuffer]): + """ + Class for representing a set of :class:`Cbuffer` values. + + ``CbufferSet`` objects can be created with a single argument of type string + as in MobilityDB. + + >>> CbufferSet(string='{"Cbuffer(Point(1 1), 1)", "Cbuffer(Point(2 2), 2)"}') + + Another possibility is to create a ``CbufferSet`` object from a list of + :class:`Cbuffer` instances. + + >>> CbufferSet(elements=[Cbuffer(point=Point(1, 1), radius=1)]) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "cbufferset" + + _parse_function = cbufferset_in + _parse_value_function = lambda x: cbuffer_in(x) if isinstance(x, str) else x._inner + _make_function = cbufferset_make + + # ------------------------- Constructors ---------------------------------- + + # ------------------------- Output ---------------------------------------- + + def __str__(self, max_decimals: int = 15): + """ + Return the string representation of the content of ``self``. + + Returns: + A new :class:`str` instance + + MEOS Functions: + spatialset_as_text + """ + return self.as_text(max_decimals) + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_ewkt + """ + return spatialset_as_ewkt(self._inner, max_decimals) + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + # ------------------------- Conversions ----------------------------------- + + def to_spanset(self): + raise NotImplementedError() + + def to_span(self): + raise NotImplementedError() + + # ------------------------- Accessors ------------------------------------- + + def start_element(self) -> Cbuffer: + """ + Returns the first element in ``self``. + + Returns: + A :class:`Cbuffer` instance + + MEOS Functions: + cbufferset_start_value + """ + return Cbuffer(_inner=cbufferset_start_value(self._inner)) + + def end_element(self) -> Cbuffer: + """ + Returns the last element in ``self``. + + Returns: + A :class:`Cbuffer` instance + + MEOS Functions: + cbufferset_end_value + """ + return Cbuffer(_inner=cbufferset_end_value(self._inner)) + + def element_n(self, n: int) -> Cbuffer: + """ + Returns the ``n``-th element in ``self``. + + Args: + n: The 0-based index of the element to return. + + Returns: + A :class:`Cbuffer` instance + + MEOS Functions: + cbufferset_value_n + """ + super().element_n(n) + return Cbuffer(_inner=cbufferset_value_n(self._inner, n + 1)[0]) + + def elements(self) -> list[Cbuffer]: + """ + Returns a list of all elements in ``self``. + + Returns: + A list of :class:`Cbuffer` instances + + MEOS Functions: + cbufferset_values + """ + elems = cbufferset_values(self._inner) + return [Cbuffer(_inner=elems[i]) for i in range(self.num_elements())] + + # ------------------------- Topological Operations ------------------------ + + def contains(self, content: CbufferSet | Cbuffer) -> bool: + """ + Returns whether ``self`` contains ``content``. + + Args: + content: object to compare with + + Returns: + True if contains, False otherwise + + MEOS Functions: + contains_set_cbuffer, contains_set_set + """ + if isinstance(content, Cbuffer): + return contains_set_cbuffer(self._inner, content._inner) + else: + return super().contains(content) + + # ------------------------- Set Operations -------------------------------- + + @overload + def intersection(self, other: Cbuffer) -> CbufferSet | None: ... + + @overload + def intersection(self, other: CbufferSet) -> CbufferSet | None: ... + + def intersection(self, other): + """ + Returns the intersection of ``self`` and ``other``. + + Args: + other: A :class:`CbufferSet` or :class:`Cbuffer` instance + + Returns: + An object of the same type as ``other`` or ``None`` if the + intersection is empty. + + MEOS Functions: + intersection_set_cbuffer, intersection_set_set + """ + if isinstance(other, Cbuffer): + result = intersection_set_cbuffer(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + elif isinstance(other, CbufferSet): + result = intersection_set_set(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + else: + return super().intersection(other) + + def minus(self, other: CbufferSet | Cbuffer) -> CbufferSet | None: + """ + Returns the difference of ``self`` and ``other``. + + Args: + other: A :class:`CbufferSet` or :class:`Cbuffer` instance + + Returns: + A :class:`CbufferSet` instance or ``None`` if the difference is + empty. + + MEOS Functions: + minus_set_cbuffer, minus_set_set + """ + if isinstance(other, Cbuffer): + result = minus_set_cbuffer(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + elif isinstance(other, CbufferSet): + result = minus_set_set(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + else: + return super().minus(other) + + def subtract_from(self, other: Cbuffer) -> CbufferSet | None: + """ + Returns the difference of ``other`` and ``self``. + + Args: + other: A :class:`Cbuffer` instance + + Returns: + A :class:`CbufferSet` instance. + + MEOS Functions: + minus_cbuffer_set + + See Also: + :meth:`minus` + """ + result = minus_cbuffer_set(other._inner, self._inner) + return CbufferSet(_inner=result) if result is not None else None + + def union(self, other: CbufferSet | Cbuffer) -> CbufferSet: + """ + Returns the union of ``self`` and ``other``. + + Args: + other: A :class:`CbufferSet` or :class:`Cbuffer` instance + + Returns: + A :class:`CbufferSet` instance. + + MEOS Functions: + union_set_cbuffer, union_set_set + """ + if isinstance(other, Cbuffer): + result = union_set_cbuffer(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + elif isinstance(other, CbufferSet): + result = union_set_set(self._inner, other._inner) + return CbufferSet(_inner=result) if result is not None else None + else: + return super().union(other) diff --git a/pymeos/collections/npoint/__init__.py b/pymeos/collections/npoint/__init__.py new file mode 100644 index 00000000..332aaa11 --- /dev/null +++ b/pymeos/collections/npoint/__init__.py @@ -0,0 +1,5 @@ +from .npoint import Npoint +from .npointset import NpointSet +from .nsegment import Nsegment + +__all__ = ["Npoint", "NpointSet", "Nsegment"] diff --git a/pymeos/collections/npoint/npoint.py b/pymeos/collections/npoint/npoint.py new file mode 100644 index 00000000..138eeaca --- /dev/null +++ b/pymeos/collections/npoint/npoint.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import shapely.geometry.base as shp +from pymeos_cffi import * + +if TYPE_CHECKING: + from ...boxes import STBox + + +class Npoint: + """ + Class for representing a network point, i.e. a point defined by a route + identifier and a relative position along that route. + + ``Npoint`` objects can be created with a single argument of type string as + in MobilityDB. + + >>> Npoint('NPoint(1, 0.5)') + + Another possibility is to provide the route identifier and the relative + position with the corresponding parameters. + + >>> Npoint(route=1, position=0.5) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "npoint" + + # ------------------------- Constructors ---------------------------------- + def __init__( + self, + string: str | None = None, + *, + route: int | None = None, + position: float | None = None, + _inner=None, + ): + assert (_inner is not None) or (string is not None) != (route is not None and position is not None), ( + "Either string must be not None or route and position must be not None" + ) + if _inner is not None: + self._inner = _inner + elif string is not None: + self._inner = npoint_in(string) + else: + self._inner = npoint_make(route, position) + + @staticmethod + def from_wkb(wkb: bytes) -> Npoint: + """ + Returns a `Npoint` from its WKB representation. + + Args: + wkb: WKB representation + + Returns: + A new :class:`Npoint` instance + + MEOS Functions: + npoint_from_wkb + """ + result = npoint_from_wkb(wkb) + return Npoint(_inner=result) + + @staticmethod + def from_hexwkb(hexwkb: str) -> Npoint: + """ + Returns a `Npoint` from its WKB representation in hex-encoded ASCII. + + Args: + hexwkb: WKB representation in hex-encoded ASCII + + Returns: + A new :class:`Npoint` instance + + MEOS Functions: + npoint_from_hexwkb + """ + result = npoint_from_hexwkb(hexwkb) + return Npoint(_inner=result) + + @staticmethod + def from_geometry(geom: shp.BaseGeometry) -> Npoint: + """ + Returns a `Npoint` from a `shp.BaseGeometry`. + + Args: + geom: A `shp.BaseGeometry` instance. + + Returns: + A new :class:`Npoint` instance. + + MEOS Functions: + geompoint_to_npoint + """ + gs = geometry_to_gserialized(geom) + return Npoint(_inner=geompoint_to_npoint(gs)) + + def __copy__(self) -> Npoint: + """ + Returns a copy of ``self``. + + Returns: + A :class:`Npoint` instance. + + MEOS Functions: + npoint_round + """ + inner_copy = npoint_round(self._inner, 100) + return Npoint(_inner=inner_copy) + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15): + """ + Returns the string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + npoint_as_text + """ + return self.as_text(max_decimals) + + def __repr__(self): + """ + Returns the string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + npoint_out + """ + return f"{self.__class__.__name__}({self})" + + def as_wkb(self) -> bytes: + """ + Returns the WKB representation of ``self``. + + Returns: + A :class:`bytes` object with the WKB representation of ``self``. + + MEOS Functions: + npoint_as_wkb + """ + return npoint_as_wkb(self._inner, 4) + + def as_hexwkb(self) -> str: + """ + Returns the WKB representation of ``self`` in hex-encoded ASCII. + + Returns: + A :class:`str` object with the WKB representation of ``self`` in + hex-encoded ASCII. + + MEOS Functions: + npoint_as_hexwkb + """ + return npoint_as_hexwkb(self._inner, -1)[0] + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the + coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + npoint_as_ewkt + """ + return npoint_as_ewkt(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the + coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + npoint_as_text + """ + return npoint_as_text(self._inner, max_decimals) + + # ------------------------- Conversions ----------------------------------- + def to_geometry(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the geometry represented by ``self``. + + Args: + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + npoint_to_geompoint + """ + return gserialized_to_shapely_geometry(npoint_to_geompoint(self._inner), precision) + + def to_stbox(self) -> STBox: + """ + Returns the bounding box of ``self`` as an :class:`STBox`. + + Returns: + A new :class:`STBox` instance. + + MEOS Functions: + npoint_to_stbox + """ + from ...boxes import STBox + + return STBox(_inner=npoint_to_stbox(self._inner)) + + # ------------------------- Accessors ------------------------------------- + def route(self) -> int: + """ + Returns the route identifier of ``self``. + + Returns: + An :class:`int` with the route identifier of ``self``. + + MEOS Functions: + npoint_route + """ + return npoint_route(self._inner) + + def position(self) -> float: + """ + Returns the relative position of ``self`` along its route. + + Returns: + A :class:`float` with the relative position of ``self``. + + MEOS Functions: + npoint_position + """ + return npoint_position(self._inner) + + def srid(self) -> int: + """ + Returns the SRID of ``self``. + + Returns: + An :class:`int` with the SRID of ``self``. + + MEOS Functions: + npoint_srid + """ + return npoint_srid(self._inner) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> Npoint: + """ + Returns a new :class:`Npoint` with the relative position of ``self`` + rounded to ``max_decimals`` decimal places. + + Args: + max_decimals: The number of decimal places. + + Returns: + A new :class:`Npoint` instance. + + MEOS Functions: + npoint_round + """ + return Npoint(_inner=npoint_round(self._inner, max_decimals)) + + # ------------------------- Comparisons ----------------------------------- + def __eq__(self, other): + """ + Returns whether ``self`` is equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is equal to ``other``, ``False`` otherwise. + + MEOS Functions: + npoint_eq + """ + if isinstance(other, self.__class__): + return npoint_eq(self._inner, other._inner) + return False + + def __ne__(self, other): + """ + Returns whether ``self`` is not equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is not equal to ``other``, ``False`` + otherwise. + + MEOS Functions: + npoint_ne + """ + if isinstance(other, self.__class__): + return npoint_ne(self._inner, other._inner) + return True + + def __lt__(self, other): + """ + Returns whether ``self`` is less than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is less than ``other``, ``False`` otherwise. + + MEOS Functions: + npoint_lt + """ + if isinstance(other, self.__class__): + return npoint_lt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __le__(self, other): + """ + Returns whether ``self`` is less than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is less than or equal to ``other``, ``False`` + otherwise. + + MEOS Functions: + npoint_le + """ + if isinstance(other, self.__class__): + return npoint_le(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __gt__(self, other): + """ + Returns whether ``self`` is greater than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is greater than ``other``, ``False`` + otherwise. + + MEOS Functions: + npoint_gt + """ + if isinstance(other, self.__class__): + return npoint_gt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __ge__(self, other): + """ + Returns whether ``self`` is greater than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if ``self`` is greater than or equal to ``other``, + ``False`` otherwise. + + MEOS Functions: + npoint_ge + """ + if isinstance(other, self.__class__): + return npoint_ge(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __hash__(self) -> int: + """ + Returns the hash of ``self``. + + Returns: + An :class:`int` with the hash of ``self``. + + MEOS Functions: + npoint_hash + """ + return npoint_hash(self._inner) diff --git a/pymeos/collections/npoint/npointset.py b/pymeos/collections/npoint/npointset.py new file mode 100644 index 00000000..a2c1b918 --- /dev/null +++ b/pymeos/collections/npoint/npointset.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +from typing import overload + +from pymeos_cffi import * + +from ..base import Set +from .npoint import Npoint + + +class NpointSet(Set[Npoint]): + """ + Class for representing a set of network points. + + ``NpointSet`` objects can be created with a single argument of type string + as in MobilityDB. + + >>> NpointSet(string='{"NPoint(1, 0.5)", "NPoint(2, 0.3)"}') + + Another possibility is to create a ``NpointSet`` object from a list of + network points. + + >>> NpointSet(elements=[Npoint(route=1, position=0.5), Npoint(route=2, position=0.3)]) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "npointset" + + _parse_function = npointset_in + _parse_value_function = lambda x: npoint_in(x)[0] if isinstance(x, str) else x._inner[0] + _make_function = npointset_make + + # ------------------------- Constructors ---------------------------------- + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15): + """ + Return the string representation of the content of ``self``. + + Returns: + A new :class:`str` instance + + MEOS Functions: + spatialset_as_text + """ + return self.as_text(max_decimals) + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_ewkt + """ + return spatialset_as_ewkt(self._inner, max_decimals) + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + # ------------------------- Conversions ----------------------------------- + def to_spanset(self): + raise NotImplementedError() + + def to_span(self): + raise NotImplementedError() + + # ------------------------- Accessors ------------------------------------- + def start_element(self) -> Npoint: + """ + Returns the first element in ``self``. + + Returns: + A :class:`Npoint` instance + + MEOS Functions: + npointset_start_value + """ + return Npoint(_inner=npointset_start_value(self._inner)) + + def end_element(self) -> Npoint: + """ + Returns the last element in ``self``. + + Returns: + A :class:`Npoint` instance + + MEOS Functions: + npointset_end_value + """ + return Npoint(_inner=npointset_end_value(self._inner)) + + def element_n(self, n: int) -> Npoint: + """ + Returns the ``n``-th element in ``self``. + + Args: + n: The 0-based index of the element to return. + + Returns: + A :class:`Npoint` instance + + MEOS Functions: + npointset_value_n + """ + super().element_n(n) + return Npoint(_inner=npointset_value_n(self._inner, n + 1)[0]) + + def elements(self) -> list[Npoint]: + """ + Returns the elements in ``self``. + + Returns: + A list of :class:`Npoint` instances + + MEOS Functions: + npointset_values + """ + elems = npointset_values(self._inner) + return [Npoint(_inner=elems[i]) for i in range(self.num_elements())] + + def routes(self) -> Set[int]: + """ + Returns the set of route identifiers of the elements in ``self``. + + Returns: + A new :class:`Set` instance with the route identifiers. + + MEOS Functions: + npointset_routes + """ + from ...factory import _CollectionFactory + + return _CollectionFactory.create_collection(npointset_routes(self._inner)) + + # ------------------------- Topological Operations ------------------------ + + def contains(self, content: NpointSet | Npoint) -> bool: + """ + Returns whether ``self`` contains ``content``. + + Args: + content: object to compare with + + Returns: + True if contains, False otherwise + + MEOS Functions: + contains_set_npoint, contains_set_set + """ + if isinstance(content, Npoint): + return contains_set_npoint(self._inner, content._inner) + else: + return super().contains(content) + + # ------------------------- Set Operations -------------------------------- + + @overload + def intersection(self, other: Npoint) -> NpointSet | None: ... + + @overload + def intersection(self, other: NpointSet) -> NpointSet | None: ... + + def intersection(self, other): + """ + Returns the intersection of ``self`` and ``other``. + + Args: + other: A :class:`NpointSet` or :class:`Npoint` instance + + Returns: + An object of the same type as ``other`` or ``None`` if the + intersection is empty. + + MEOS Functions: + intersection_set_npoint, intersection_set_set + """ + if isinstance(other, Npoint): + result = intersection_set_npoint(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + elif isinstance(other, NpointSet): + result = intersection_set_set(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + else: + return super().intersection(other) + + def minus(self, other: NpointSet | Npoint) -> NpointSet | None: + """ + Returns the difference of ``self`` and ``other``. + + Args: + other: A :class:`NpointSet` or :class:`Npoint` instance + + Returns: + A :class:`NpointSet` instance or ``None`` if the difference is + empty. + + MEOS Functions: + minus_set_npoint, minus_set_set + """ + if isinstance(other, Npoint): + result = minus_set_npoint(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + elif isinstance(other, NpointSet): + result = minus_set_set(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + else: + return super().minus(other) + + def subtract_from(self, other: Npoint) -> NpointSet | None: + """ + Returns the difference of ``other`` and ``self``. + + Args: + other: A :class:`Npoint` instance + + Returns: + A :class:`NpointSet` instance. + + MEOS Functions: + minus_npoint_set + + See Also: + :meth:`minus` + """ + result = minus_npoint_set(other._inner, self._inner) + return NpointSet(_inner=result) if result is not None else None + + def union(self, other: NpointSet | Npoint) -> NpointSet: + """ + Returns the union of ``self`` and ``other``. + + Args: + other: A :class:`NpointSet` or :class:`Npoint` instance + + Returns: + A :class:`NpointSet` instance. + + MEOS Functions: + union_set_npoint, union_set_set + """ + if isinstance(other, Npoint): + result = union_set_npoint(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + elif isinstance(other, NpointSet): + result = union_set_set(self._inner, other._inner) + return NpointSet(_inner=result) if result is not None else None + else: + return super().union(other) diff --git a/pymeos/collections/npoint/nsegment.py b/pymeos/collections/npoint/nsegment.py new file mode 100644 index 00000000..e0ec2989 --- /dev/null +++ b/pymeos/collections/npoint/nsegment.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import shapely.geometry.base as shp +from pymeos_cffi import * + +if TYPE_CHECKING: + from ...boxes import STBox + + +class Nsegment: + """ + Class for representing a network segment, that is, a route identifier + together with a start and end position along that route. + + ``Nsegment`` objects can be created with a single string argument as in + MobilityDB. + + >>> Nsegment('NSegment(1, 0.2, 0.6)') + + Another possibility is to provide the ``rid``, ``pos1`` and ``pos2`` + arguments. + + >>> Nsegment(rid=1, pos1=0.2, pos2=0.6) + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "nsegment" + + # ------------------------- Constructors ---------------------------------- + def __init__( + self, + string: str | None = None, + *, + rid: int | None = None, + pos1: float | None = None, + pos2: float | None = None, + _inner=None, + ): + assert (_inner is not None) or (string is not None) != ( + rid is not None and pos1 is not None and pos2 is not None + ), "Either string must be not None or rid, pos1 and pos2 must be not None" + if _inner is not None: + self._inner = _inner + elif string is not None: + self._inner = nsegment_in(string) + else: + self._inner = nsegment_make(rid, float(pos1), float(pos2)) + + def __copy__(self) -> Nsegment: + """ + Returns a copy of ``self``. + + MEOS Functions: + nsegment_round + """ + return Nsegment(_inner=nsegment_round(self._inner, 100)) + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15) -> str: + """ + Returns the string representation of ``self``. + + MEOS Functions: + nsegment_out + """ + return nsegment_out(self._inner, max_decimals) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self})" + + # ------------------------- Accessors ------------------------------------- + def route(self) -> int: + """ + Returns the route identifier of ``self``. + + MEOS Functions: + nsegment_route + """ + return nsegment_route(self._inner) + + def start_position(self) -> float: + """ + Returns the start position of ``self``. + + MEOS Functions: + nsegment_start_position + """ + return nsegment_start_position(self._inner) + + def end_position(self) -> float: + """ + Returns the end position of ``self``. + + MEOS Functions: + nsegment_end_position + """ + return nsegment_end_position(self._inner) + + def srid(self) -> int: + """ + Returns the SRID of ``self``. + + MEOS Functions: + nsegment_srid + """ + return nsegment_srid(self._inner) + + # ------------------------- Conversions ----------------------------------- + def to_geometry(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns ``self`` as a `shapely` + :class:`~shapely.geometry.base.BaseGeometry`. + + MEOS Functions: + nsegment_to_geom + """ + return gserialized_to_shapely_geometry(nsegment_to_geom(self._inner), precision) + + def to_stbox(self) -> STBox: + """ + Returns the bounding box of ``self``. + + MEOS Functions: + nsegment_to_stbox + """ + from ...boxes import STBox + + return STBox(_inner=nsegment_to_stbox(self._inner)) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> Nsegment: + """ + Returns ``self`` with the positions rounded to a number of decimals. + + MEOS Functions: + nsegment_round + """ + return Nsegment(_inner=nsegment_round(self._inner, max_decimals)) + + # ------------------------- Comparisons ----------------------------------- + def __eq__(self, other) -> bool: + if isinstance(other, Nsegment): + return str(self) == str(other) + return NotImplemented + + def __hash__(self) -> int: + return hash(str(self)) diff --git a/pymeos/collections/pose/__init__.py b/pymeos/collections/pose/__init__.py new file mode 100644 index 00000000..a2f69bc4 --- /dev/null +++ b/pymeos/collections/pose/__init__.py @@ -0,0 +1,4 @@ +from .pose import Pose +from .poseset import PoseSet + +__all__ = ["Pose", "PoseSet"] diff --git a/pymeos/collections/pose/pose.py b/pymeos/collections/pose/pose.py new file mode 100644 index 00000000..0dd806a4 --- /dev/null +++ b/pymeos/collections/pose/pose.py @@ -0,0 +1,516 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import shapely.geometry.base as shp +from pymeos_cffi import * + +if TYPE_CHECKING: + from ...boxes import STBox + + +class Pose: + """ + Class for representing a pose, i.e. a point with an orientation. + + ``Pose`` objects can be created with a single argument of type string as + in MobilityDB. + + >>> Pose('Pose(Point(1 1), 0.5)') + + Another possibility is to provide the spatial point and the orientation + with the corresponding parameters. For 2D poses the orientation is given + by a single angle ``theta``. + + >>> Pose(point='Point(1 1)', theta=0.5) + + For 3D poses the orientation is given by a unit quaternion + ``(W, X, Y, Z)``. + + >>> Pose(point='Point(1 1 1)', W=1.0, X=0.0, Y=0.0, Z=0.0) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "pose" + + # ------------------------- Constructors ---------------------------------- + def __init__( + self, + string: str | None = None, + *, + point: str | shp.BaseGeometry | None = None, + theta: float | None = None, + W: float | None = None, + X: float | None = None, + Y: float | None = None, + Z: float | None = None, + srid: int | None = 0, + geodetic: bool = False, + _inner=None, + ): + assert (_inner is not None) or (string is not None) != (point is not None), ( + "Either string must be not None or point must be not None" + ) + if _inner is not None: + self._inner = _inner + elif string is not None: + self._inner = pose_in(string) + else: + gs = geo_to_gserialized(point, geodetic) + if W is not None and X is not None and Y is not None and Z is not None: + self._inner = pose_make_point3d(gs, W, X, Y, Z) + else: + self._inner = pose_make_point2d(gs, float(theta or 0)) + if srid is not None: + pose_set_srid(self._inner, srid) + + def __copy__(self) -> Pose: + """ + Returns a copy of ``self``. + + Returns: + A :class:`Pose` instance. + + MEOS Functions: + pose_copy + """ + inner_copy = pose_copy(self._inner) + return Pose(_inner=inner_copy) + + @staticmethod + def from_wkb(wkb: bytes) -> Pose: + """ + Returns a `Pose` from its WKB representation. + + Args: + wkb: WKB representation + + Returns: + A new :class:`Pose` instance + + MEOS Functions: + pose_from_wkb + """ + result = pose_from_wkb(wkb) + return Pose(_inner=result) + + @staticmethod + def from_hexwkb(hexwkb: str) -> Pose: + """ + Returns a `Pose` from its WKB representation in hex-encoded ASCII. + + Args: + hexwkb: WKB representation in hex-encoded ASCII + + Returns: + A new :class:`Pose` instance + + MEOS Functions: + pose_from_hexwkb + """ + result = pose_from_hexwkb(hexwkb) + return Pose(_inner=result) + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15): + """ + Returns the string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + pose_as_text + """ + return self.as_text(max_decimals) + + def __repr__(self): + """ + Returns a string representation of ``self``. + + Returns: + A :class:`str` instance. + + MEOS Functions: + pose_out + """ + return f"{self.__class__.__name__}({self})" + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the + coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + pose_as_text + """ + return pose_as_text(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the + coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + pose_as_text + """ + return pose_as_text(self._inner, max_decimals) + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the + coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + pose_as_ewkt + """ + return pose_as_ewkt(self._inner, max_decimals) + + def as_wkb(self) -> bytes: + """ + Returns the WKB representation of ``self``. + + Returns: + A :class:`bytes` object with the WKB representation of ``self``. + + MEOS Functions: + pose_as_wkb + """ + return pose_as_wkb(self._inner, 4) + + def as_hexwkb(self) -> str: + """ + Returns the WKB representation of ``self`` in hex-encoded ASCII. + + Returns: + A :class:`str` object with the WKB representation of ``self`` in + hex-encoded ASCII. + + MEOS Functions: + pose_as_hexwkb + """ + return pose_as_hexwkb(self._inner, -1)[0] + + # ------------------------- Conversions ----------------------------------- + def to_geometry(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the point of ``self`` as a `shapely` + :class:`~shapely.BaseGeometry` instance. + + Args: + precision: The precision of the geometry coordinates. + + Returns: + A new :class:`~shapely.BaseGeometry` instance. + + MEOS Functions: + pose_to_point + """ + return gserialized_to_shapely_geometry(pose_to_point(self._inner), precision) + + def to_stbox(self) -> STBox: + """ + Returns the bounding box of ``self`` as an :class:`STBox` instance. + + Returns: + A new :class:`STBox` instance. + + MEOS Functions: + pose_to_stbox + """ + from ...boxes import STBox + + return STBox(_inner=pose_to_stbox(self._inner)) + + # ------------------------- Accessors ------------------------------------- + def quaternion(self) -> shp.BaseGeometry: + """ + Returns the orientation of ``self`` as a unit quaternion. + + Returns: + The orientation of ``self``. + + MEOS Functions: + pose_quaternion + """ + return pose_quaternion(self._inner) + + def yaw(self) -> float: + """ + Returns the rotation angle of ``self``. + + Returns: + A :class:`float` instance. + + MEOS Functions: + pose_yaw + """ + return pose_yaw(self._inner) + + def __hash__(self) -> int: + """ + Returns the hash of ``self``. + + Returns: + A new :class:`int` instance. + + MEOS Functions: + pose_hash + """ + return pose_hash(self._inner) + + # ------------------------- Spatial Reference System ---------------------- + def srid(self) -> int: + """ + Returns the SRID of ``self``. + + Returns: + An :class:`int` instance. + + MEOS Functions: + pose_srid + """ + return pose_srid(self._inner) + + def set_srid(self, srid: int) -> Pose: + """ + Returns a new :class:`Pose` with the SRID set to ``srid``. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`Pose` instance. + + MEOS Functions: + pose_set_srid + """ + result = pose_copy(self._inner) + pose_set_srid(result, srid) + return Pose(_inner=result) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> Pose: + """ + Returns a new :class:`Pose` with the coordinate values of ``self`` + rounded to ``max_decimals`` decimal places. + + Args: + max_decimals: The number of decimal places. + + Returns: + A new :class:`Pose` instance. + + MEOS Functions: + pose_round + """ + return Pose(_inner=pose_round(self._inner, max_decimals)) + + def transform(self, srid: int) -> Pose: + """ + Returns a new :class:`Pose` transformed to another SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`Pose` instance. + + MEOS Functions: + pose_transform + """ + return Pose(_inner=pose_transform(self._inner, srid)) + + def transform_pipeline(self, pipeline: str, srid: int, is_forward: bool = True) -> Pose: + """ + Returns a new :class:`Pose` transformed using a transformation + pipeline. + + Args: + pipeline: The transformation pipeline. + srid: The desired SRID. + is_forward: Whether to apply the pipeline in the forward direction. + + Returns: + A new :class:`Pose` instance. + + MEOS Functions: + pose_transform_pipeline + """ + return Pose(_inner=pose_transform_pipeline(self._inner, pipeline, srid, is_forward)) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: shp.BaseGeometry | Pose | STBox) -> float: + """ + Returns the distance between ``self`` and ``other``. + + Args: + other: An object to check the distance to. + + Returns: + A :class:`float` instance. + + MEOS Functions: + distance_pose_geo, distance_pose_pose, distance_pose_stbox + """ + from ...boxes import STBox + + if isinstance(other, shp.BaseGeometry): + gs = geo_to_gserialized(other, False) + return distance_pose_geo(self._inner, gs) + elif isinstance(other, Pose): + return distance_pose_pose(self._inner, other._inner) + elif isinstance(other, STBox): + return distance_pose_stbox(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + # ------------------------- Comparisons ----------------------------------- + def __eq__(self, other): + """ + Returns whether ``self`` is equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if equal, ``False`` otherwise. + + MEOS Functions: + pose_eq + """ + if isinstance(other, self.__class__): + return pose_eq(self._inner, other._inner) + return False + + def __ne__(self, other): + """ + Returns whether ``self`` is not equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if not equal, ``False`` otherwise. + + MEOS Functions: + pose_ne + """ + if isinstance(other, self.__class__): + return pose_ne(self._inner, other._inner) + return True + + def __lt__(self, other): + """ + Returns whether ``self`` is less than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if less than, ``False`` otherwise. + + MEOS Functions: + pose_lt + """ + if isinstance(other, self.__class__): + return pose_lt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __le__(self, other): + """ + Returns whether ``self`` is less than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if less than or equal, ``False`` otherwise. + + MEOS Functions: + pose_le + """ + if isinstance(other, self.__class__): + return pose_le(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __gt__(self, other): + """ + Returns whether ``self`` is greater than ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if greater than, ``False`` otherwise. + + MEOS Functions: + pose_gt + """ + if isinstance(other, self.__class__): + return pose_gt(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def __ge__(self, other): + """ + Returns whether ``self`` is greater than or equal to ``other``. + + Args: + other: The object to compare with ``self``. + + Returns: + ``True`` if greater than or equal, ``False`` otherwise. + + MEOS Functions: + pose_ge + """ + if isinstance(other, self.__class__): + return pose_ge(self._inner, other._inner) + raise TypeError(f"Operation not supported with type {other.__class__}") + + def cmp(self, other: Pose) -> int: + """ + Returns the comparison value between ``self`` and ``other``. + + Args: + other: The :class:`Pose` to compare with ``self``. + + Returns: + An :class:`int` instance: -1, 0 or 1. + + MEOS Functions: + pose_cmp + """ + return pose_cmp(self._inner, other._inner) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`Pose` from a database cursor. Used when automatically + loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + return Pose(string=value) diff --git a/pymeos/collections/pose/poseset.py b/pymeos/collections/pose/poseset.py new file mode 100644 index 00000000..5162a67a --- /dev/null +++ b/pymeos/collections/pose/poseset.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +from typing import overload + +from pymeos_cffi import * + +from ..base import Set +from .pose import Pose + + +class PoseSet(Set[Pose]): + """ + Class for representing a set of pose values. + + ``PoseSet`` objects can be created with a single argument of type string + as in MobilityDB. + + >>> PoseSet(string='{"Pose(Point(1 1), 0.5)", "Pose(Point(2 2), 0.3)"}') + + Another possibility is to create a ``PoseSet`` object from a list of + :class:`Pose` instances. + + >>> PoseSet(elements=[Pose('Pose(Point(1 1), 0.5)')]) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "poseset" + + _parse_function = poseset_in + _parse_value_function = lambda pose: pose._inner if isinstance(pose, Pose) else pose_in(pose) + _make_function = poseset_make + + # ------------------------- Constructors ---------------------------------- + + # ------------------------- Output ---------------------------------------- + + def __str__(self, max_decimals: int = 15): + """ + Return the string representation of the content of ``self``. + + Returns: + A new :class:`str` instance + + MEOS Functions: + spatialset_as_text + """ + return self.as_text(max_decimals) + + def as_ewkt(self, max_decimals: int = 15) -> str: + """ + Returns the EWKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_ewkt + """ + return spatialset_as_ewkt(self._inner, max_decimals) + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + def as_text(self, max_decimals: int = 15) -> str: + """ + Returns the WKT representation of ``self``. + + Args: + max_decimals: The number of decimal places to use for the coordinates. + + Returns: + A :class:`str` instance. + + MEOS Functions: + spatialset_as_text + """ + return spatialset_as_text(self._inner, max_decimals) + + # ------------------------- Conversions ----------------------------------- + + def to_spanset(self): + raise NotImplementedError() + + def to_span(self): + raise NotImplementedError() + + # ------------------------- Accessors ------------------------------------- + + def start_element(self) -> Pose: + """ + Returns the first element in ``self``. + + Returns: + A :class:`Pose` instance + + MEOS Functions: + poseset_start_value + """ + return Pose(_inner=poseset_start_value(self._inner)) + + def end_element(self) -> Pose: + """ + Returns the last element in ``self``. + + Returns: + A :class:`Pose` instance + + MEOS Functions: + poseset_end_value + """ + return Pose(_inner=poseset_end_value(self._inner)) + + def element_n(self, n: int) -> Pose: + """ + Returns the ``n``-th element in ``self``. + + Args: + n: The 0-based index of the element to return. + + Returns: + A :class:`Pose` instance + + MEOS Functions: + poseset_value_n + """ + super().element_n(n) + return Pose(_inner=poseset_value_n(self._inner, n + 1)[0]) + + def elements(self) -> list[Pose]: + """ + Returns the elements in ``self``. + + Returns: + A list of :class:`Pose` instances + + MEOS Functions: + poseset_values + """ + elems = poseset_values(self._inner) + return [Pose(_inner=elems[i]) for i in range(self.num_elements())] + + # ------------------------- Topological Operations ------------------------ + + def contains(self, content: PoseSet | Pose) -> bool: + """ + Returns whether ``self`` contains ``content``. + + Args: + content: object to compare with + + Returns: + True if contains, False otherwise + + MEOS Functions: + contains_set_set, contains_set_pose + """ + if isinstance(content, Pose): + return contains_set_pose(self._inner, content._inner) + else: + return super().contains(content) + + # ------------------------- Set Operations -------------------------------- + + @overload + def intersection(self, other: Pose) -> Pose | None: ... + + @overload + def intersection(self, other: PoseSet) -> PoseSet | None: ... + + def intersection(self, other): + """ + Returns the intersection of ``self`` and ``other``. + + Args: + other: A :class:`PoseSet` or :class:`Pose` instance + + Returns: + An object of the same type as ``other`` or ``None`` if the + intersection is empty. + + MEOS Functions: + intersection_set_pose, intersection_set_set + """ + if isinstance(other, Pose): + result = intersection_set_pose(self._inner, other._inner) + return Pose(_inner=result) if result is not None else None + elif isinstance(other, PoseSet): + result = intersection_set_set(self._inner, other._inner) + return PoseSet(_inner=result) if result is not None else None + else: + return super().intersection(other) + + def minus(self, other: PoseSet | Pose) -> PoseSet | None: + """ + Returns the difference of ``self`` and ``other``. + + Args: + other: A :class:`PoseSet` or :class:`Pose` instance + + Returns: + A :class:`PoseSet` instance or ``None`` if the difference is empty. + + MEOS Functions: + minus_set_pose, minus_set_set + """ + if isinstance(other, Pose): + result = minus_set_pose(self._inner, other._inner) + return PoseSet(_inner=result) if result is not None else None + elif isinstance(other, PoseSet): + result = minus_set_set(self._inner, other._inner) + return PoseSet(_inner=result) if result is not None else None + else: + return super().minus(other) + + def subtract_from(self, other: Pose) -> Pose | None: + """ + Returns the difference of ``other`` and ``self``. + + Args: + other: A :class:`Pose` instance + + Returns: + A :class:`Pose` instance. + + MEOS Functions: + minus_pose_set + + See Also: + :meth:`minus` + """ + result = minus_pose_set(other._inner, self._inner) + return Pose(_inner=result) if result is not None else None + + def union(self, other: PoseSet | Pose) -> PoseSet: + """ + Returns the union of ``self`` and ``other``. + + Args: + other: A :class:`PoseSet` or :class:`Pose` instance + + Returns: + A :class:`PoseSet` instance. + + MEOS Functions: + union_set_pose, union_set_set + """ + if isinstance(other, Pose): + result = union_set_pose(self._inner, other._inner) + return PoseSet(_inner=result) if result is not None else None + elif isinstance(other, PoseSet): + result = union_set_set(self._inner, other._inner) + return PoseSet(_inner=result) if result is not None else None + else: + return super().union(other) diff --git a/pymeos/factory.py b/pymeos/factory.py index 0f967af3..59c15323 100644 --- a/pymeos/factory.py +++ b/pymeos/factory.py @@ -1,6 +1,7 @@ from pymeos_cffi import MeosTemporalSubtype, MeosType from .collections import ( + CbufferSet, DateSet, DateSpan, DateSpanSet, @@ -12,6 +13,8 @@ IntSet, IntSpan, IntSpanSet, + NpointSet, + PoseSet, TextSet, TsTzSet, TsTzSpan, @@ -21,6 +24,9 @@ TBoolInst, TBoolSeq, TBoolSeqSet, + TCbufferInst, + TCbufferSeq, + TCbufferSeqSet, TFloatInst, TFloatSeq, TFloatSeqSet, @@ -33,11 +39,44 @@ TIntInst, TIntSeq, TIntSeqSet, + TNpointInst, + TNpointSeq, + TNpointSeqSet, + TPoseInst, + TPoseSeq, + TPoseSeqSet, + TRgeometryInst, + TRgeometrySeq, + TRgeometrySeqSet, TTextInst, TTextSeq, TTextSeqSet, ) +try: # the compiled binding (same source pymeos_cffi's own enums use) + from _meos_cffi import lib as _meos_lib +except Exception: # pragma: no cover - the binding is present at runtime + _meos_lib = None + + +def _meos_type(name: str): + """Return the MEOS type tag for ``name``. + + Robust to ``pymeos_cffi`` not yet wrapping a valid, compiled MEOS type + tag in its ``MeosType`` enum: prefer the exposed enum member, else read + the constant from the compiled binding (``_meos_cffi.lib`` — the very + source ``pymeos_cffi`` builds its enums from), else ``None`` so the + mapping is skipped and importing never fails. ``MeosType`` is an + ``IntEnum``, so an ``int`` key matches ``inner.temptype`` lookups + identically to an enum-keyed one. + """ + member = getattr(MeosType, name, None) + if member is not None: + return member + if _meos_lib is not None and hasattr(_meos_lib, name): + return int(getattr(_meos_lib, name)) + return None + class _TemporalFactory: """ @@ -130,3 +169,30 @@ def create_collection(inner): attributes = ["spansettype", "spantype", "settype"] collection_type = next(getattr(inner, attribute) for attribute in attributes if hasattr(inner, attribute)) return _CollectionFactory._mapper[collection_type](_inner=inner) + + +# Extended temporal type families (cbuffer, npoint, pose, rgeo) are full +# user-facing temporal types and are wired exactly like every other type. +# Registered post-hoc through `_meos_type` so a build whose `pymeos_cffi` +# does not yet wrap these (valid, compiled) tags in its `MeosType` enum +# still imports cleanly and activates them automatically. +for _tag, _inst, _seq, _seqset in ( + ("T_TNPOINT", TNpointInst, TNpointSeq, TNpointSeqSet), + ("T_TCBUFFER", TCbufferInst, TCbufferSeq, TCbufferSeqSet), + ("T_TPOSE", TPoseInst, TPoseSeq, TPoseSeqSet), + ("T_TRGEOMETRY", TRgeometryInst, TRgeometrySeq, TRgeometrySeqSet), +): + _k = _meos_type(_tag) + if _k is not None: + _TemporalFactory._mapper[(_k, MeosTemporalSubtype.INSTANT)] = _inst + _TemporalFactory._mapper[(_k, MeosTemporalSubtype.SEQUENCE)] = _seq + _TemporalFactory._mapper[(_k, MeosTemporalSubtype.SEQUENCE_SET)] = _seqset + +for _tag, _cls in ( + ("T_NPOINTSET", NpointSet), + ("T_CBUFFERSET", CbufferSet), + ("T_POSESET", PoseSet), +): + _k = _meos_type(_tag) + if _k is not None: + _CollectionFactory._mapper[_k] = _cls diff --git a/pymeos/main/__init__.py b/pymeos/main/__init__.py index 153e4cab..653e495b 100644 --- a/pymeos/main/__init__.py +++ b/pymeos/main/__init__.py @@ -1,6 +1,8 @@ from .tbool import TBool, TBoolInst, TBoolSeq, TBoolSeqSet +from .tcbuffer import TCbuffer, TCbufferInst, TCbufferSeq, TCbufferSeqSet from .tfloat import TFloat, TFloatInst, TFloatSeq, TFloatSeqSet from .tint import TInt, TIntInst, TIntSeq, TIntSeqSet +from .tnpoint import TNpoint, TNpointInst, TNpointSeq, TNpointSeqSet from .tnumber import TNumber from .tpoint import ( TGeogPoint, @@ -16,6 +18,13 @@ TPointSeq, TPointSeqSet, ) +from .tpose import TPose, TPoseInst, TPoseSeq, TPoseSeqSet +from .trgeometry import ( + TRgeometry, + TRgeometryInst, + TRgeometrySeq, + TRgeometrySeqSet, +) from .ttext import TText, TTextInst, TTextSeq, TTextSeqSet __all__ = [ @@ -48,4 +57,20 @@ "TGeogPointInst", "TGeogPointSeq", "TGeogPointSeqSet", + "TNpoint", + "TNpointInst", + "TNpointSeq", + "TNpointSeqSet", + "TCbuffer", + "TCbufferInst", + "TCbufferSeq", + "TCbufferSeqSet", + "TPose", + "TPoseInst", + "TPoseSeq", + "TPoseSeqSet", + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", ] diff --git a/pymeos/main/tcbuffer.py b/pymeos/main/tcbuffer.py new file mode 100644 index 00000000..8b5b0fd7 --- /dev/null +++ b/pymeos/main/tcbuffer.py @@ -0,0 +1,1204 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, TypeVar, overload + +import shapely.geometry.base as shpb +from pymeos_cffi import * + +from ..collections import * +from ..collections.cbuffer import Cbuffer, CbufferSet +from ..mixins import TTemporallyComparable +from ..temporal import Temporal, TInstant, TInterpolation, TSequence, TSequenceSet +from .tfloat import TFloat +from .tpoint import TPoint + +if TYPE_CHECKING: + from ..boxes import STBox + from .tbool import TBool + +Self = TypeVar("Self", bound="TCbuffer") + + +class TCbuffer( + Temporal[Cbuffer, "TCbuffer", "TCbufferInst", "TCbufferSeq", "TCbufferSeqSet"], + TTemporallyComparable, + ABC, +): + """ + Abstract class for temporal circular buffers. + """ + + _mobilitydb_name = "tcbuffer" + + BaseClass = Cbuffer + + _parse_function = tcbuffer_in + + def __init__(self, _inner) -> None: + super().__init__() + + # ------------------------- Constructors ---------------------------------- + @staticmethod + def from_point_radius(point: TPoint, radius: TFloat) -> TCbuffer: + """ + Create a temporal circular buffer from a temporal point and a temporal + float representing the radius. + + Args: + point: A :class:`TPoint` with the center. + radius: A :class:`TFloat` with the radius. + + Returns: + A new :class:`TCbuffer` object. + + MEOS Functions: + tcbuffer_make + """ + result = tcbuffer_make(point._inner, radius._inner) + return Temporal._factory(result) + + # ------------------------- Output ---------------------------------------- + def __str__(self): + """ + Returns the string representation of `self`. + + Returns: + A :class:`str` with the string representation of `self`. + + MEOS Functions: + tspatial_as_text + """ + return tspatial_as_text(self._inner, 15) + + def as_wkt(self, precision: int = 15) -> str: + """ + Returns the temporal circular buffer as a WKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal circular buffer. + + MEOS Functions: + tspatial_as_text + """ + return tspatial_as_text(self._inner, precision) + + def as_ewkt(self, precision: int = 15) -> str: + """ + Returns the temporal circular buffer as an EWKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal circular buffer. + + MEOS Functions: + tspatial_as_ewkt + """ + return tspatial_as_ewkt(self._inner, precision) + + # ------------------------- Conversions ----------------------------------- + def to_tfloat(self) -> TFloat: + """ + Returns the temporal float of the radii of `self`. + + Returns: + A new :class:`TFloat` object. + + MEOS Functions: + tcbuffer_to_tfloat + """ + result = tcbuffer_to_tfloat(self._inner) + return Temporal._factory(result) + + def to_tgeompoint(self) -> TPoint: + """ + Returns the temporal geometry point of the centers of `self`. + + Returns: + A new :class:`TGeomPoint` object. + + MEOS Functions: + tcbuffer_to_tgeompoint + """ + result = tcbuffer_to_tgeompoint(self._inner) + return Temporal._factory(result) + + # ------------------------- Accessors ------------------------------------- + def bounding_box(self) -> STBox: + """ + Returns the bounding box of `self`. + + Returns: + An :class:`~pymeos.boxes.STBox` representing the bounding box. + + MEOS Functions: + tspatial_to_stbox + """ + from ..boxes import STBox + + return STBox(_inner=tspatial_to_stbox(self._inner)) + + def radius(self) -> TFloat: + """ + Returns the radius of `self` as a temporal float. + + Returns: + A new :class:`TFloat` with the radius. + + MEOS Functions: + tcbuffer_radius + """ + result = tcbuffer_radius(self._inner) + return Temporal._factory(result) + + def points(self) -> CbufferSet: + """ + Returns the set of points of `self`. + + Returns: + A :class:`CbufferSet` with the points. + + MEOS Functions: + tcbuffer_points + """ + return CbufferSet(_inner=tcbuffer_points(self._inner)) + + def traversed_area(self) -> shpb.BaseGeometry: + """ + Returns the traversed area of `self` as a `shapely` + :class:`~shapely.geometry.base.BaseGeometry`. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` representing the + traversed area. + + MEOS Functions: + tcbuffer_traversed_area + """ + return gserialized_to_shapely_geometry(tcbuffer_traversed_area(self._inner), 15) + + # ------------------------- Spatial Reference System ---------------------- + def srid(self) -> int: + """ + Returns the SRID of `self`. + + Returns: + An :class:`int` representing the SRID. + + MEOS Functions: + tspatial_srid + """ + return tspatial_srid(self._inner) + + def set_srid(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TCbuffer` with the given SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TCbuffer` instance. + + MEOS Functions: + tspatial_set_srid + """ + return self.__class__(_inner=tspatial_set_srid(self._inner, srid)) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> TCbuffer: + """ + Round the coordinate values to a number of decimal places. + + Returns: + A new :class:`TCbuffer` object. + + MEOS Functions: + temporal_round + """ + result = temporal_round(self._inner, max_decimals) + return Temporal._factory(result) + + def transform(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TCbuffer` transformed to another SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TCbuffer` instance. + + MEOS Functions: + tspatial_transform + """ + result = tspatial_transform(self._inner, srid) + return Temporal._factory(result) + + # ------------------------- Value Accessors ------------------------------- + def start_value(self) -> Cbuffer: + """ + Returns the start value of `self`. + + Returns: + A :class:`Cbuffer` with the start value. + + MEOS Functions: + tcbuffer_start_value + """ + return Cbuffer(_inner=tcbuffer_start_value(self._inner)) + + def end_value(self) -> Cbuffer: + """ + Returns the end value of `self`. + + Returns: + A :class:`Cbuffer` with the end value. + + MEOS Functions: + tcbuffer_end_value + """ + return Cbuffer(_inner=tcbuffer_end_value(self._inner)) + + def value_set(self) -> set[Cbuffer]: + """ + Returns the set of values of `self`. + + Returns: + A :class:`set` of :class:`Cbuffer` with the values. + + MEOS Functions: + tcbuffer_values + """ + values, count = tcbuffer_values(self._inner) + return {Cbuffer(_inner=values[i]) for i in range(count)} + + def value_at_timestamp(self, timestamp: datetime) -> Cbuffer: + """ + Returns the value of `self` at the given timestamp. + + Args: + timestamp: A :class:`datetime` representing the timestamp. + + Returns: + A :class:`Cbuffer` with the value. + + MEOS Functions: + tcbuffer_value_at_timestamptz + """ + _, value = tcbuffer_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True) + return Cbuffer(_inner=value) + + # ------------------------- Constructors ---------------------------------- + @staticmethod + def from_base_temporal(value: Cbuffer, base: Temporal) -> TCbuffer: + """ + Create a temporal circular buffer from a circular buffer and the time frame of another + temporal object. + + Args: + value: Cbuffer value of the temporal. + base: Temporal object to use as time frame. + + Returns: + A new :class:`TCbuffer` object. + + MEOS Functions: + tcbuffer_from_base_temp + """ + result = tcbuffer_from_base_temp(value._inner, base._inner) + return Temporal._factory(result) + + @staticmethod + @overload + def from_base_time(value: Cbuffer, base: datetime, interpolation: None = None) -> TCbufferInst: ... + + @staticmethod + @overload + def from_base_time(value: Cbuffer, base: TsTzSet, interpolation: None = None) -> TCbufferSeq: ... + + @staticmethod + @overload + def from_base_time(value: Cbuffer, base: TsTzSpan, interpolation: TInterpolation = None) -> TCbufferSeq: ... + + @staticmethod + @overload + def from_base_time(value: Cbuffer, base: TsTzSpanSet, interpolation: TInterpolation = None) -> TCbufferSeqSet: ... + + @staticmethod + def from_base_time(value: Cbuffer, base: Time, interpolation: TInterpolation = None) -> TCbuffer: + """ + Create a temporal circular buffer from a circular buffer and a time object. + + Args: + value: Cbuffer value. + base: Time object to use as temporal dimension. + interpolation: Interpolation of the temporal circular buffer. + + Returns: + A new temporal circular buffer. + + MEOS Functions: + tcbufferinst_make, tcbufferseq_from_base_tstzset, + tcbufferseq_from_base_tstzspan, tcbufferseqset_from_base_tstzspanset + """ + if isinstance(base, datetime): + return TCbufferInst(_inner=tcbufferinst_make(value._inner, datetime_to_timestamptz(base))) + elif isinstance(base, TsTzSet): + return TCbufferSeq(_inner=tcbufferseq_from_base_tstzset(value._inner, base._inner)) + elif isinstance(base, TsTzSpan): + return TCbufferSeq(_inner=tcbufferseq_from_base_tstzspan(value._inner, base._inner, interpolation)) + elif isinstance(base, TsTzSpanSet): + return TCbufferSeqSet(_inner=tcbufferseqset_from_base_tstzspanset(value._inner, base._inner, interpolation)) + raise TypeError(f"Operation not supported with type {base.__class__}") + + @classmethod + def from_mfjson(cls: type[Self], mfjson: str) -> Self: + """ + Returns a temporal circular buffer from a MF-JSON string. + + Args: + mfjson: The MF-JSON string. + + Returns: + A temporal circular buffer. + + MEOS Functions: + tcbuffer_from_mfjson + """ + result = tcbuffer_from_mfjson(mfjson) + return Temporal._factory(result) + + # ------------------------- Ever and Always Comparisons ------------------- + def always_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are always equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are always equal to `value`, + `False` otherwise. + + MEOS Functions: + always_eq_tcbuffer_cbuffer, always_eq_tcbuffer_tcbuffer + """ + if isinstance(value, Cbuffer): + return always_eq_tcbuffer_cbuffer(self._inner, value._inner) > 0 + elif isinstance(value, TCbuffer): + return always_eq_tcbuffer_tcbuffer(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def always_not_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are always not equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are always not equal to `value`, + `False` otherwise. + + MEOS Functions: + always_ne_tcbuffer_cbuffer, always_ne_tcbuffer_tcbuffer + """ + if isinstance(value, Cbuffer): + return always_ne_tcbuffer_cbuffer(self._inner, value._inner) > 0 + elif isinstance(value, TCbuffer): + return always_ne_tcbuffer_tcbuffer(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are ever equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are ever equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tcbuffer_cbuffer, ever_eq_tcbuffer_tcbuffer + """ + if isinstance(value, Cbuffer): + return ever_eq_tcbuffer_cbuffer(self._inner, value._inner) > 0 + elif isinstance(value, TCbuffer): + return ever_eq_tcbuffer_tcbuffer(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_not_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are ever not equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are ever not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tcbuffer_cbuffer, ever_ne_tcbuffer_tcbuffer + """ + if isinstance(value, Cbuffer): + return ever_ne_tcbuffer_cbuffer(self._inner, value._inner) > 0 + elif isinstance(value, TCbuffer): + return ever_ne_tcbuffer_tcbuffer(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def never_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are never equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are never equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tcbuffer_cbuffer, ever_eq_tcbuffer_tcbuffer + """ + return not self.ever_equal(value) + + def never_not_equal(self, value: Cbuffer | TCbuffer) -> bool: + """ + Returns whether the values of `self` are never not equal to `value`. + + Args: + value: :class:`Cbuffer` or :class:`TCbuffer` to compare. + + Returns: + `True` if the values of `self` are never not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tcbuffer_cbuffer, ever_ne_tcbuffer_tcbuffer + """ + return not self.ever_not_equal(value) + + # ------------------------- Temporal Comparisons -------------------------- + def temporal_equal(self, other: Cbuffer | TCbuffer) -> TBool: + """ + Returns the temporal equality relation between `self` and `other`. + + Args: + other: A :class:`Cbuffer` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal equality relation. + + MEOS Functions: + teq_tcbuffer_cbuffer, teq_temporal_temporal + """ + if isinstance(other, Cbuffer): + result = teq_tcbuffer_cbuffer(self._inner, other._inner) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other: Cbuffer | TCbuffer) -> TBool: + """ + Returns the temporal not equal relation between `self` and `other`. + + Args: + other: A :class:`Cbuffer` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal not equal + relation. + + MEOS Functions: + tne_tcbuffer_cbuffer, tne_temporal_temporal + """ + if isinstance(other, Cbuffer): + result = tne_tcbuffer_cbuffer(self._inner, other._inner) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) + + # ------------------------- Restrictions ---------------------------------- + def at(self, other: Cbuffer | shpb.BaseGeometry | STBox | Time) -> TCbuffer: + """ + Returns a new temporal circular buffer with the values of `self` + restricted to `other`. + + Args: + other: An object to restrict the values of `self` to. + + Returns: + A new :class:`TCbuffer` with the values of `self` restricted to + `other`. + + MEOS Functions: + tcbuffer_at_cbuffer, tcbuffer_at_geom, tcbuffer_at_stbox, + temporal_at_timestamp, temporal_at_tstzset, temporal_at_tstzspan, + temporal_at_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Cbuffer): + result = tcbuffer_at_cbuffer(self._inner, other._inner) + elif isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tcbuffer_at_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tcbuffer_at_stbox(self._inner, other._inner, True) + else: + return super().at(other) + return Temporal._factory(result) + + def minus(self, other: Cbuffer | shpb.BaseGeometry | STBox | Time) -> TCbuffer: + """ + Returns a new temporal circular buffer with the values of `self` + restricted to the complement of `other`. + + Args: + other: An object to restrict the values of `self` to the + complement of. + + Returns: + A new :class:`TCbuffer` with the values of `self` restricted to + the complement of `other`. + + MEOS Functions: + tcbuffer_minus_cbuffer, tcbuffer_minus_geom, tcbuffer_minus_stbox, + temporal_minus_timestamp, temporal_minus_tstzset, + temporal_minus_tstzspan, temporal_minus_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Cbuffer): + result = tcbuffer_minus_cbuffer(self._inner, other._inner) + elif isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tcbuffer_minus_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tcbuffer_minus_stbox(self._inner, other._inner, True) + else: + return super().minus(other) + return Temporal._factory(result) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TFloat: + """ + Returns the temporal distance between `self` and `other`. + + Args: + other: An object to check the distance to. + + Returns: + A new :class:`TFloat` with the temporal distance. + + MEOS Functions: + tdistance_tcbuffer_geo, tdistance_tcbuffer_cbuffer, + tdistance_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tdistance_tcbuffer_geo(self._inner, gs) + elif isinstance(other, Cbuffer): + result = tdistance_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = tdistance_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def nearest_approach_distance(self, other: shpb.BaseGeometry | Cbuffer | STBox | TCbuffer) -> float: + """ + Returns the nearest approach distance between `self` and `other`. + + Args: + other: An object to check the nearest approach distance to. + + Returns: + A :class:`float` with the nearest approach distance. + + MEOS Functions: + nad_tcbuffer_geo, nad_tcbuffer_cbuffer, nad_tcbuffer_stbox, + nad_tcbuffer_tcbuffer + """ + from ..boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + return nad_tcbuffer_geo(self._inner, gs) + elif isinstance(other, Cbuffer): + return nad_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, STBox): + return nad_tcbuffer_stbox(self._inner, other._inner) + elif isinstance(other, TCbuffer): + return nad_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + def nearest_approach_instant(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TCbufferInst: + """ + Returns the nearest approach instant between `self` and `other`. + + Args: + other: An object to check the nearest approach instant to. + + Returns: + A new :class:`TCbufferInst` with the nearest approach instant. + + MEOS Functions: + nai_tcbuffer_geo, nai_tcbuffer_cbuffer, nai_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = nai_tcbuffer_geo(self._inner, gs) + elif isinstance(other, Cbuffer): + result = nai_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = nai_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def shortest_line(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> shpb.BaseGeometry: + """ + Returns the shortest line between `self` and `other`. + + Args: + other: An object to check the shortest line to. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` with the + shortest line. + + MEOS Functions: + shortestline_tcbuffer_geo, shortestline_tcbuffer_cbuffer, + shortestline_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = shortestline_tcbuffer_geo(self._inner, gs) + elif isinstance(other, Cbuffer): + result = shortestline_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = shortestline_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return gserialized_to_shapely_geometry(result, 10) + + # ------------------------- Ever Spatial Relationships -------------------- + def is_ever_contains(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` ever contains `other`. + + MEOS Functions: + econtains_tcbuffer_geo, econtains_tcbuffer_cbuffer, + econtains_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = econtains_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = econtains_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = econtains_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_ever_contained_in(self, container: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` is ever contained by `container`, as + #TPoint.is_ever_contained_in. + + MEOS Functions: + econtains_geo_tcbuffer, econtains_cbuffer_tcbuffer + """ + if isinstance(container, shpb.BaseGeometry): + result = econtains_geo_tcbuffer(geo_to_gserialized(container, False), self._inner) + elif isinstance(container, Cbuffer): + result = econtains_cbuffer_tcbuffer(container._inner, self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_ever_covers(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` ever covers `other`. + + MEOS Functions: + ecovers_tcbuffer_geo, ecovers_tcbuffer_cbuffer, + ecovers_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = ecovers_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = ecovers_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = ecovers_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_ever_coveredby(self, container: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` is ever covered by `container`: the covers + relationship with `container` first, as #TCbuffer.is_ever_contained_in + is for contains and as PostGIS's coveredby is for covers. + + MEOS Functions: + ecovers_geo_tcbuffer, ecovers_cbuffer_tcbuffer + """ + if isinstance(container, shpb.BaseGeometry): + result = ecovers_geo_tcbuffer(geo_to_gserialized(container, False), self._inner) + elif isinstance(container, Cbuffer): + result = ecovers_cbuffer_tcbuffer(container._inner, self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_ever_disjoint(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` is ever disjoint from `other`. + + MEOS Functions: + edisjoint_tcbuffer_geo, edisjoint_tcbuffer_cbuffer, + edisjoint_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = edisjoint_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = edisjoint_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = edisjoint_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_ever_within_distance( + self, + other: shpb.BaseGeometry | Cbuffer | TCbuffer, + distance: float, + ) -> bool: + """ + Returns whether `self` is ever within `distance` of `other`. + + MEOS Functions: + edwithin_tcbuffer_geo, edwithin_tcbuffer_cbuffer, + edwithin_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = edwithin_tcbuffer_geo(self._inner, geo_to_gserialized(other, False), distance) + elif isinstance(other, Cbuffer): + result = edwithin_tcbuffer_cbuffer(self._inner, other._inner, distance) + elif isinstance(other, TCbuffer): + result = edwithin_tcbuffer_tcbuffer(self._inner, other._inner, distance) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def ever_intersects(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` ever intersects `other`. + + MEOS Functions: + eintersects_tcbuffer_geo, eintersects_tcbuffer_cbuffer, + eintersects_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = eintersects_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = eintersects_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = eintersects_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def ever_touches(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` ever touches `other`. + + MEOS Functions: + etouches_tcbuffer_geo, etouches_tcbuffer_cbuffer, + etouches_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = etouches_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = etouches_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = etouches_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + # ------------------------- Always Spatial Relationships ------------------ + def is_always_contains(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` always contains `other`. + + MEOS Functions: + acontains_tcbuffer_geo, acontains_tcbuffer_cbuffer, + acontains_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = acontains_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = acontains_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = acontains_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_contained_in(self, container: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` is always contained by `container`, as + #TPoint.is_ever_contained_in for the ever relationship. + + MEOS Functions: + acontains_geo_tcbuffer, acontains_cbuffer_tcbuffer + """ + if isinstance(container, shpb.BaseGeometry): + result = acontains_geo_tcbuffer(geo_to_gserialized(container, False), self._inner) + elif isinstance(container, Cbuffer): + result = acontains_cbuffer_tcbuffer(container._inner, self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_always_covers(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` always covers `other`. + + MEOS Functions: + acovers_tcbuffer_geo, acovers_tcbuffer_cbuffer, + acovers_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = acovers_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = acovers_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = acovers_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_coveredby(self, container: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` is always covered by `container`: the covers + relationship with `container` first, as + #TCbuffer.is_always_contained_in is for contains. + + MEOS Functions: + acovers_geo_tcbuffer, acovers_cbuffer_tcbuffer + """ + if isinstance(container, shpb.BaseGeometry): + result = acovers_geo_tcbuffer(geo_to_gserialized(container, False), self._inner) + elif isinstance(container, Cbuffer): + result = acovers_cbuffer_tcbuffer(container._inner, self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_always_disjoint(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` is always disjoint from `other`. + + MEOS Functions: + adisjoint_tcbuffer_geo, adisjoint_tcbuffer_cbuffer, + adisjoint_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = adisjoint_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = adisjoint_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = adisjoint_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_within_distance( + self, + other: shpb.BaseGeometry | Cbuffer | TCbuffer, + distance: float, + ) -> bool: + """ + Returns whether `self` is always within `distance` of `other`. + + MEOS Functions: + adwithin_tcbuffer_geo, adwithin_tcbuffer_cbuffer, + adwithin_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = adwithin_tcbuffer_geo(self._inner, geo_to_gserialized(other, False), distance) + elif isinstance(other, Cbuffer): + result = adwithin_tcbuffer_cbuffer(self._inner, other._inner, distance) + elif isinstance(other, TCbuffer): + result = adwithin_tcbuffer_tcbuffer(self._inner, other._inner, distance) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def always_intersects(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` always intersects `other`. + + MEOS Functions: + aintersects_tcbuffer_geo, aintersects_tcbuffer_cbuffer, + aintersects_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = aintersects_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = aintersects_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = aintersects_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def always_touches(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> bool: + """ + Returns whether `self` always touches `other`. + + MEOS Functions: + atouches_tcbuffer_geo, atouches_tcbuffer_cbuffer, + atouches_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = atouches_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = atouches_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = atouches_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + # ------------------------- Temporal Spatial Relationships ---------------- + def contains(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TBool: + """ + Returns a temporal boolean of whether `self` contains `other`. + + MEOS Functions: + tcontains_tcbuffer_geo, tcontains_tcbuffer_cbuffer, + tcontains_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = tcontains_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = tcontains_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = tcontains_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def covers(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TBool: + """ + Returns a temporal boolean of whether `self` covers `other`. + + MEOS Functions: + tcovers_tcbuffer_geo, tcovers_tcbuffer_cbuffer, + tcovers_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = tcovers_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = tcovers_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = tcovers_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def disjoint(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TBool: + """ + Returns a temporal boolean of whether `self` is disjoint from `other`. + + MEOS Functions: + tdisjoint_tcbuffer_geo, tdisjoint_tcbuffer_cbuffer, + tdisjoint_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = tdisjoint_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = tdisjoint_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = tdisjoint_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def within_distance( + self, + other: shpb.BaseGeometry | Cbuffer | TCbuffer, + distance: float, + ) -> TBool: + """ + Returns a temporal boolean of whether `self` is within `distance` of + `other`. + + MEOS Functions: + tdwithin_tcbuffer_geo, tdwithin_tcbuffer_cbuffer, + tdwithin_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = tdwithin_tcbuffer_geo(self._inner, geo_to_gserialized(other, False), distance) + elif isinstance(other, Cbuffer): + result = tdwithin_tcbuffer_cbuffer(self._inner, other._inner, distance) + elif isinstance(other, TCbuffer): + result = tdwithin_tcbuffer_tcbuffer(self._inner, other._inner, distance) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def intersects(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TBool: + """ + Returns a temporal boolean of whether `self` intersects `other`. + + MEOS Functions: + tintersects_tcbuffer_geo, tintersects_tcbuffer_cbuffer, + tintersects_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = tintersects_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = tintersects_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = tintersects_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def touches(self, other: shpb.BaseGeometry | Cbuffer | TCbuffer) -> TBool: + """ + Returns a temporal boolean of whether `self` touches `other`. + + MEOS Functions: + ttouches_tcbuffer_geo, ttouches_tcbuffer_cbuffer, + ttouches_tcbuffer_tcbuffer + """ + if isinstance(other, shpb.BaseGeometry): + result = ttouches_tcbuffer_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, Cbuffer): + result = ttouches_tcbuffer_cbuffer(self._inner, other._inner) + elif isinstance(other, TCbuffer): + result = ttouches_tcbuffer_tcbuffer(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`TCbuffer` from a database cursor. Used when + automatically loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + if value[0] != "{" and value[0] != "[" and value[0] != "(": + return TCbufferInst(string=value) + elif value[0] == "[" or value[0] == "(": + return TCbufferSeq(string=value) + elif value[0] == "{": + if value[1] == "[" or value[1] == "(": + return TCbufferSeqSet(string=value) + else: + return TCbufferSeq(string=value) + raise Exception("ERROR: Could not parse temporal circular buffer value") + + +class TCbufferInst( + TInstant[Cbuffer, "TCbuffer", "TCbufferInst", "TCbufferSeq", "TCbufferSeqSet"], + TCbuffer, +): + """ + Class for representing temporal circular buffers at a single instant. + """ + + _make_function = lambda *args: None + _cast_function = lambda x: None + + def __init__( + self, + string: str | None = None, + *, + _inner=None, + ) -> None: + super().__init__(string=string, _inner=_inner) + + +class TCbufferSeq( + TSequence[Cbuffer, "TCbuffer", "TCbufferInst", "TCbufferSeq", "TCbufferSeqSet"], + TCbuffer, +): + """ + Class for representing temporal circular buffers over a tstzspan of time. + """ + + ComponentClass = TCbufferInst + + def __init__( + self, + string: str | None = None, + *, + instant_list: list[str | TCbufferInst] | None = None, + lower_inc: bool = True, + upper_inc: bool = False, + interpolation: TInterpolation = TInterpolation.LINEAR, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + instant_list=instant_list, + lower_inc=lower_inc, + upper_inc=upper_inc, + interpolation=interpolation, + normalize=normalize, + _inner=_inner, + ) + + +class TCbufferSeqSet( + TSequenceSet[Cbuffer, "TCbuffer", "TCbufferInst", "TCbufferSeq", "TCbufferSeqSet"], + TCbuffer, +): + """ + Class for representing temporal circular buffers over a tstzspan of time + with gaps. + """ + + ComponentClass = TCbufferSeq + + def __init__( + self, + string: str | None = None, + *, + sequence_list: list[str | TCbufferSeq] | None = None, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + sequence_list=sequence_list, + normalize=normalize, + _inner=_inner, + ) diff --git a/pymeos/main/tnpoint.py b/pymeos/main/tnpoint.py new file mode 100644 index 00000000..03974145 --- /dev/null +++ b/pymeos/main/tnpoint.py @@ -0,0 +1,816 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, TypeVar, overload + +import shapely.geometry.base as shp +from pymeos_cffi import * + +from ..collections import * +from ..collections.npoint import Npoint, NpointSet, Nsegment +from ..mixins import TTemporallyComparable +from ..temporal import Temporal, TInstant, TInterpolation, TSequence, TSequenceSet + +if TYPE_CHECKING: + from ..boxes import STBox + from .tbool import TBool + from .tfloat import TFloat + from .tpoint import TGeomPoint + + +Self = TypeVar("Self", bound="TNpoint") + + +class TNpoint( + Temporal[Npoint, "TNpoint", "TNpointInst", "TNpointSeq", "TNpointSeqSet"], + TTemporallyComparable, + ABC, +): + """ + Abstract class for temporal network points. + """ + + _mobilitydb_name = "tnpoint" + + BaseClass = Npoint + + _parse_function = tnpoint_in + + def __init__(self, _inner) -> None: + super().__init__() + + # ------------------------- Constructors ---------------------------------- + @staticmethod + def from_base_temporal(value: Npoint, base: Temporal) -> TNpoint: + """ + Create a temporal network point from a network point and the time frame of another + temporal object. + + Args: + value: Npoint value of the temporal. + base: Temporal object to use as time frame. + + Returns: + A new :class:`TNpoint` object. + + MEOS Functions: + tnpoint_from_base_temp + """ + result = tnpoint_from_base_temp(value._inner, base._inner) + return Temporal._factory(result) + + @staticmethod + @overload + def from_base_time(value: Npoint, base: datetime, interpolation: None = None) -> TNpointInst: ... + + @staticmethod + @overload + def from_base_time(value: Npoint, base: TsTzSet, interpolation: None = None) -> TNpointSeq: ... + + @staticmethod + @overload + def from_base_time(value: Npoint, base: TsTzSpan, interpolation: TInterpolation = None) -> TNpointSeq: ... + + @staticmethod + @overload + def from_base_time(value: Npoint, base: TsTzSpanSet, interpolation: TInterpolation = None) -> TNpointSeqSet: ... + + @staticmethod + def from_base_time(value: Npoint, base: Time, interpolation: TInterpolation = None) -> TNpoint: + """ + Create a temporal network point from a network point and a time object. + + Args: + value: Npoint value. + base: Time object to use as temporal dimension. + interpolation: Interpolation of the temporal network point. + + Returns: + A new temporal network point. + + MEOS Functions: + tnpointinst_make, tnpointseq_from_base_tstzset, + tnpointseq_from_base_tstzspan, tnpointseqset_from_base_tstzspanset + """ + if isinstance(base, datetime): + return TNpointInst(_inner=tnpointinst_make(value._inner, datetime_to_timestamptz(base))) + elif isinstance(base, TsTzSet): + return TNpointSeq(_inner=tnpointseq_from_base_tstzset(value._inner, base._inner)) + elif isinstance(base, TsTzSpan): + return TNpointSeq(_inner=tnpointseq_from_base_tstzspan(value._inner, base._inner, interpolation)) + elif isinstance(base, TsTzSpanSet): + return TNpointSeqSet(_inner=tnpointseqset_from_base_tstzspanset(value._inner, base._inner, interpolation)) + raise TypeError(f"Operation not supported with type {base.__class__}") + + @classmethod + def from_mfjson(cls: type[Self], mfjson: str) -> Self: + """ + Returns a temporal network point from a MF-JSON string. + + Args: + mfjson: The MF-JSON string. + + Returns: + A temporal network point. + + MEOS Functions: + tnpoint_from_mfjson + """ + result = tnpoint_from_mfjson(mfjson) + return Temporal._factory(result) + + # ------------------------- Output ---------------------------------------- + def __str__(self, max_decimals: int = 15) -> str: + """ + Returns the string representation of `self`. + + Returns: + A string with the string representation of `self`. + + MEOS Functions: + tnpoint_out + """ + return tnpoint_out(self._inner, max_decimals) + + def as_wkt(self, max_decimals: int = 15) -> str: + """ + Returns the Well-Known Text representation of `self`. + + Returns: + A string with the Well-Known Text representation of `self`. + + MEOS Functions: + tnpoint_out + """ + return tnpoint_out(self._inner, max_decimals) + + # ------------------------- Value Accessors ------------------------------- + def start_value(self) -> Npoint: + """ + Returns the start value of `self`. + + Returns: + A :class:`Npoint` with the start value. + + MEOS Functions: + tnpoint_start_value + """ + return Npoint(_inner=tnpoint_start_value(self._inner)) + + def end_value(self) -> Npoint: + """ + Returns the end value of `self`. + + Returns: + A :class:`Npoint` with the end value. + + MEOS Functions: + tnpoint_end_value + """ + return Npoint(_inner=tnpoint_end_value(self._inner)) + + def value_set(self) -> set[Npoint]: + """ + Returns the set of values of `self`. + + Returns: + A :class:`set` of :class:`Npoint` with the values. + + MEOS Functions: + tnpoint_values + """ + values, count = tnpoint_values(self._inner) + return {Npoint(_inner=values[i]) for i in range(count)} + + def value_at_timestamp(self, timestamp: datetime) -> Npoint: + """ + Returns the value of `self` at the given timestamp. + + Args: + timestamp: A :class:`datetime` representing the timestamp. + + Returns: + A :class:`Npoint` with the value. + + MEOS Functions: + tnpoint_value_at_timestamptz + """ + _, value = tnpoint_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True) + return Npoint(_inner=value) + + # ------------------------- Conversions ----------------------------------- + def to_tgeompoint(self) -> TGeomPoint: + """ + Returns a temporal geometric point equivalent to `self`. + + Returns: + A new :class:`TGeomPoint` instance. + + MEOS Functions: + tnpoint_to_tgeompoint + """ + result = tnpoint_to_tgeompoint(self._inner) + return Temporal._factory(result) + + @staticmethod + def from_tgeompoint(temporal: TGeomPoint) -> TNpoint: + """ + Returns a temporal network point equivalent to a temporal geometric + point. + + Args: + temporal: A :class:`TGeomPoint` instance. + + Returns: + A new :class:`TNpoint` instance. + + MEOS Functions: + tgeompoint_to_tnpoint + """ + result = tgeompoint_to_tnpoint(temporal._inner) + return Temporal._factory(result) + + # ------------------------- Accessors ------------------------------------- + def bounding_box(self) -> STBox: + """ + Returns the bounding box of `self`. + + Returns: + A new :class:`STBox` instance. + + MEOS Functions: + tspatial_to_stbox + """ + from ..boxes import STBox + + return STBox(_inner=tspatial_to_stbox(self._inner)) + + def route(self) -> int: + """ + Returns the route identifier of `self`. Only defined when `self` stays + on a single route. + + Returns: + An :class:`int` with the route identifier. + + MEOS Functions: + tnpoint_route + """ + return tnpoint_route(self._inner) + + def routes(self) -> Set[int]: + """ + Returns the set of route identifiers of `self`. + + Returns: + A :class:`Set` instance with the route identifiers. + + MEOS Functions: + tnpoint_routes + """ + from ..factory import _CollectionFactory + + return _CollectionFactory.create_collection(tnpoint_routes(self._inner)) + + def length(self) -> float: + """ + Returns the length of the trajectory of `self`. + + Returns: + A :class:`float` with the length. + + MEOS Functions: + tnpoint_length + """ + return tnpoint_length(self._inner) + + def cumulative_length(self) -> TFloat: + """ + Returns the cumulative length of the trajectory of `self`. + + Returns: + A new :class:`TFloat` instance. + + MEOS Functions: + tnpoint_cumulative_length + """ + result = tnpoint_cumulative_length(self._inner) + return Temporal._factory(result) + + def speed(self) -> TFloat: + """ + Returns the speed of `self`. + + Returns: + A new :class:`TFloat` instance. + + MEOS Functions: + tnpoint_speed + """ + result = tnpoint_speed(self._inner) + return Temporal._factory(result) + + def trajectory(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the trajectory of `self`. + + Args: + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + tnpoint_trajectory + """ + return gserialized_to_shapely_geometry(tnpoint_trajectory(self._inner), precision) + + def time_weighted_centroid(self, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the time weighted centroid of `self`. + + Args: + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + tnpoint_twcentroid + """ + return gserialized_to_shapely_geometry(tnpoint_twcentroid(self._inner), precision) + + def positions(self) -> list[Nsegment]: + """ + Returns the network segments covered by `self`. + + Returns: + A :class:`list` of :class:`Nsegment` with the positions. + + MEOS Functions: + tnpoint_positions + """ + nss, count = tnpoint_positions(self._inner) + return [Nsegment(_inner=nss[i]) for i in range(count)] + + # ------------------------- Spatial Reference System ---------------------- + def srid(self) -> int: + """ + Returns the SRID of `self`. + + Returns: + An :class:`int` with the SRID of `self`. + + MEOS Functions: + tspatial_srid + """ + return tspatial_srid(self._inner) + + def set_srid(self: Self, srid: int) -> Self: + """ + Returns a copy of `self` with the SRID set to `srid`. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TNpoint` instance. + + MEOS Functions: + tspatial_set_srid + """ + return self.__class__(_inner=tspatial_set_srid(self._inner, srid)) + + # ------------------------- Ever and Always Comparisons ------------------- + def always_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are always equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are always equal to `value`, + `False` otherwise. + + MEOS Functions: + always_eq_tnpoint_npoint, always_eq_tnpoint_tnpoint + """ + if isinstance(value, Npoint): + return always_eq_tnpoint_npoint(self._inner, value._inner) > 0 + elif isinstance(value, TNpoint): + return always_eq_tnpoint_tnpoint(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def always_not_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are always not equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are always not equal to `value`, + `False` otherwise. + + MEOS Functions: + always_ne_tnpoint_npoint, always_ne_tnpoint_tnpoint + """ + if isinstance(value, Npoint): + return always_ne_tnpoint_npoint(self._inner, value._inner) > 0 + elif isinstance(value, TNpoint): + return always_ne_tnpoint_tnpoint(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are ever equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are ever equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tnpoint_npoint, ever_eq_tnpoint_tnpoint + """ + if isinstance(value, Npoint): + return ever_eq_tnpoint_npoint(self._inner, value._inner) > 0 + elif isinstance(value, TNpoint): + return ever_eq_tnpoint_tnpoint(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_not_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are ever not equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are ever not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tnpoint_npoint, ever_ne_tnpoint_tnpoint + """ + if isinstance(value, Npoint): + return ever_ne_tnpoint_npoint(self._inner, value._inner) > 0 + elif isinstance(value, TNpoint): + return ever_ne_tnpoint_tnpoint(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def never_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are never equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are never equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tnpoint_npoint, ever_eq_tnpoint_tnpoint + """ + return not self.ever_equal(value) + + def never_not_equal(self, value: Npoint | TNpoint) -> bool: + """ + Returns whether the values of `self` are never not equal to `value`. + + Args: + value: :class:`Npoint` or :class:`TNpoint` to compare. + + Returns: + `True` if the values of `self` are never not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tnpoint_npoint, ever_ne_tnpoint_tnpoint + """ + return not self.ever_not_equal(value) + + # ------------------------- Temporal Comparisons -------------------------- + def temporal_equal(self, other: Npoint | TNpoint) -> TBool: + """ + Returns the temporal equality relation between `self` and `other`. + + Args: + other: An :class:`Npoint` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal equality relation. + + MEOS Functions: + teq_tnpoint_npoint, teq_temporal_temporal + """ + if isinstance(other, Npoint): + result = teq_tnpoint_npoint(self._inner, other._inner) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other: Npoint | TNpoint) -> TBool: + """ + Returns the temporal not equal relation between `self` and `other`. + + Args: + other: An :class:`Npoint` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal not equal + relation. + + MEOS Functions: + tne_tnpoint_npoint, tne_temporal_temporal + """ + if isinstance(other, Npoint): + result = tne_tnpoint_npoint(self._inner, other._inner) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: shp.BaseGeometry | Npoint | TNpoint) -> TFloat: + """ + Returns the temporal distance between `self` and `other`. + + Args: + other: An object to check the distance to. + + Returns: + A new :class:`TFloat` instance. + + MEOS Functions: + tdistance_tnpoint_geo, tdistance_tnpoint_npoint, + tdistance_tnpoint_tnpoint + """ + if isinstance(other, Npoint): + result = tdistance_tnpoint_npoint(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tdistance_tnpoint_geo(self._inner, gs) + elif isinstance(other, TNpoint): + result = tdistance_tnpoint_tnpoint(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def nearest_approach_distance(self, other: shp.BaseGeometry | Npoint | STBox | TNpoint) -> float: + """ + Returns the nearest approach distance between `self` and `other`. + + Args: + other: An object to check the nearest approach distance to. + + Returns: + A :class:`float` with the nearest approach distance. + + MEOS Functions: + nad_tnpoint_geo, nad_tnpoint_npoint, nad_tnpoint_stbox, + nad_tnpoint_tnpoint + """ + from ..boxes import STBox + + if isinstance(other, Npoint): + return nad_tnpoint_npoint(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + return nad_tnpoint_geo(self._inner, gs) + elif isinstance(other, STBox): + return nad_tnpoint_stbox(self._inner, other._inner) + elif isinstance(other, TNpoint): + return nad_tnpoint_tnpoint(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + def nearest_approach_instant(self, other: shp.BaseGeometry | Npoint | TNpoint) -> TNpointInst: + """ + Returns the nearest approach instant between `self` and `other`. + + Args: + other: An object to check the nearest approach instant to. + + Returns: + A new :class:`TNpointInst` instance. + + MEOS Functions: + nai_tnpoint_geo, nai_tnpoint_npoint, nai_tnpoint_tnpoint + """ + if isinstance(other, Npoint): + result = nai_tnpoint_npoint(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + result = nai_tnpoint_geo(self._inner, gs) + elif isinstance(other, TNpoint): + result = nai_tnpoint_tnpoint(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def shortest_line(self, other: shp.BaseGeometry | Npoint | TNpoint, precision: int = 15) -> shp.BaseGeometry: + """ + Returns the shortest line between `self` and `other`. + + Args: + other: An object to check the shortest line to. + precision: The number of decimal places to use for the coordinates. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` instance. + + MEOS Functions: + shortestline_tnpoint_geo, shortestline_tnpoint_npoint, + shortestline_tnpoint_tnpoint + """ + if isinstance(other, Npoint): + result = shortestline_tnpoint_npoint(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + result = shortestline_tnpoint_geo(self._inner, gs) + elif isinstance(other, TNpoint): + result = shortestline_tnpoint_tnpoint(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return gserialized_to_shapely_geometry(result, precision) + + # ------------------------- Restrictions ---------------------------------- + def at(self, other: shp.BaseGeometry | Npoint | NpointSet | STBox | Time) -> TNpoint: + """ + Returns a new temporal network point with the values of `self` + restricted to `other`. + + Args: + other: An object to restrict the values of `self` to. + + Returns: + A new :class:`TNpoint` instance. + + MEOS Functions: + tnpoint_at_geom, tnpoint_at_npoint, tnpoint_at_npointset, + tnpoint_at_stbox, temporal_at_timestamp, temporal_at_tstzset, + temporal_at_tstzspan, temporal_at_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Npoint): + result = tnpoint_at_npoint(self._inner, other._inner) + elif isinstance(other, NpointSet): + result = tnpoint_at_npointset(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tnpoint_at_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tnpoint_at_stbox(self._inner, other._inner, True) + else: + return super().at(other) + return Temporal._factory(result) + + def minus(self, other: shp.BaseGeometry | Npoint | NpointSet | STBox | Time) -> TNpoint: + """ + Returns a new temporal network point with the values of `self` + restricted to the complement of `other`. + + Args: + other: An object to restrict the values of `self` to the + complement of. + + Returns: + A new :class:`TNpoint` instance. + + MEOS Functions: + tnpoint_minus_geom, tnpoint_minus_npoint, tnpoint_minus_npointset, + tnpoint_minus_stbox, temporal_minus_timestamp, + temporal_minus_tstzset, temporal_minus_tstzspan, + temporal_minus_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Npoint): + result = tnpoint_minus_npoint(self._inner, other._inner) + elif isinstance(other, NpointSet): + result = tnpoint_minus_npointset(self._inner, other._inner) + elif isinstance(other, shp.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tnpoint_minus_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tnpoint_minus_stbox(self._inner, other._inner, True) + else: + return super().minus(other) + return Temporal._factory(result) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`TNpoint` from a database cursor. Used when + automatically loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + if value.startswith("Interp=Stepwise;"): + value1 = value.replace("Interp=Stepwise;", "") + if value1[0] == "{": + return TNpointSeqSet(string=value) + else: + return TNpointSeq(string=value) + elif value[0] != "{" and value[0] != "[" and value[0] != "(": + return TNpointInst(string=value) + elif value[0] == "[" or value[0] == "(": + return TNpointSeq(string=value) + elif value[0] == "{": + if value[1] == "[" or value[1] == "(": + return TNpointSeqSet(string=value) + else: + return TNpointSeq(string=value) + raise Exception("ERROR: Could not parse temporal network point value") + + +class TNpointInst( + TInstant[Npoint, "TNpoint", "TNpointInst", "TNpointSeq", "TNpointSeqSet"], + TNpoint, +): + """ + Class for representing temporal network points at a single instant. + """ + + _make_function = tnpointinst_make + _cast_function = lambda x: x._inner + + def __init__( + self, + string: str | None = None, + *, + value: str | Npoint | None = None, + timestamp: str | datetime | None = None, + _inner=None, + ) -> None: + super().__init__(string=string, value=value, timestamp=timestamp, _inner=_inner) + + +class TNpointSeq( + TSequence[Npoint, "TNpoint", "TNpointInst", "TNpointSeq", "TNpointSeqSet"], + TNpoint, +): + """ + Class for representing temporal network points over a tstzspan of time. + """ + + ComponentClass = TNpointInst + + def __init__( + self, + string: str | None = None, + *, + instant_list: list[str | TNpointInst] | None = None, + lower_inc: bool = True, + upper_inc: bool = False, + interpolation: TInterpolation = TInterpolation.LINEAR, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + instant_list=instant_list, + lower_inc=lower_inc, + upper_inc=upper_inc, + interpolation=interpolation, + normalize=normalize, + _inner=_inner, + ) + + +class TNpointSeqSet( + TSequenceSet[Npoint, "TNpoint", "TNpointInst", "TNpointSeq", "TNpointSeqSet"], + TNpoint, +): + """ + Class for representing temporal network points over a tstzspan of time + with gaps. + """ + + ComponentClass = TNpointSeq + + def __init__( + self, + string: str | None = None, + *, + sequence_list: list[str | TNpointSeq] | None = None, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + sequence_list=sequence_list, + normalize=normalize, + _inner=_inner, + ) diff --git a/pymeos/main/tpose.py b/pymeos/main/tpose.py new file mode 100644 index 00000000..21ad36e6 --- /dev/null +++ b/pymeos/main/tpose.py @@ -0,0 +1,801 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, TypeVar, overload + +import shapely.geometry.base as shpb +from pymeos_cffi import * + +from ..collections import * +from ..collections.pose import Pose, PoseSet +from ..mixins import TTemporallyComparable +from ..temporal import Temporal, TInstant, TInterpolation, TSequence, TSequenceSet +from .tbool import TBool +from .tfloat import TFloat +from .tpoint import TPoint + +if TYPE_CHECKING: + from ..boxes import STBox + +Self = TypeVar("Self", bound="TPose") + + +class TPose( + Temporal[Pose, "TPose", "TPoseInst", "TPoseSeq", "TPoseSeqSet"], + TTemporallyComparable, + ABC, +): + """ + Abstract class for temporal poses. + """ + + _mobilitydb_name = "tpose" + + BaseClass = Pose + + _parse_function = tpose_in + + def __init__(self, _inner) -> None: + super().__init__() + + # ------------------------- Constructors ---------------------------------- + @staticmethod + def from_base_temporal(value: Pose, base: Temporal) -> TPose: + """ + Create a temporal pose from a pose and the time frame of another + temporal object. + + Args: + value: Pose value of the temporal. + base: Temporal object to use as time frame. + + Returns: + A new :class:`TPose` object. + + MEOS Functions: + tpose_from_base_temp + """ + result = tpose_from_base_temp(value._inner, base._inner) + return Temporal._factory(result) + + @staticmethod + @overload + def from_base_time(value: Pose, base: datetime, interpolation: None = None) -> TPoseInst: ... + + @staticmethod + @overload + def from_base_time(value: Pose, base: TsTzSet, interpolation: None = None) -> TPoseSeq: ... + + @staticmethod + @overload + def from_base_time(value: Pose, base: TsTzSpan, interpolation: TInterpolation = None) -> TPoseSeq: ... + + @staticmethod + @overload + def from_base_time(value: Pose, base: TsTzSpanSet, interpolation: TInterpolation = None) -> TPoseSeqSet: ... + + @staticmethod + def from_base_time(value: Pose, base: Time, interpolation: TInterpolation = None) -> TPose: + """ + Create a temporal pose from a pose and a time object. + + Args: + value: Pose value. + base: Time object to use as temporal dimension. + interpolation: Interpolation of the temporal pose. + + Returns: + A new temporal pose. + + MEOS Functions: + tposeinst_make, tposeseq_from_base_tstzset, + tposeseq_from_base_tstzspan, tposeseqset_from_base_tstzspanset + """ + if isinstance(base, datetime): + return TPoseInst(_inner=tposeinst_make(value._inner, datetime_to_timestamptz(base))) + elif isinstance(base, TsTzSet): + return TPoseSeq(_inner=tposeseq_from_base_tstzset(value._inner, base._inner)) + elif isinstance(base, TsTzSpan): + return TPoseSeq(_inner=tposeseq_from_base_tstzspan(value._inner, base._inner, interpolation)) + elif isinstance(base, TsTzSpanSet): + return TPoseSeqSet(_inner=tposeseqset_from_base_tstzspanset(value._inner, base._inner, interpolation)) + raise TypeError(f"Operation not supported with type {base.__class__}") + + @classmethod + def from_mfjson(cls: type[Self], mfjson: str) -> Self: + """ + Returns a temporal pose from a MF-JSON string. + + Args: + mfjson: The MF-JSON string. + + Returns: + A temporal pose. + + MEOS Functions: + tpose_from_mfjson + """ + result = tpose_from_mfjson(mfjson) + return Temporal._factory(result) + + @staticmethod + def from_tpoint_tfloat(tpoint: TPoint, tradius: TFloat) -> TPose: + """ + Create a temporal pose from a temporal point and a temporal float + representing the orientation. + + Args: + tpoint: A :class:`TPoint` with the position. + tradius: A :class:`TFloat` with the orientation. + + Returns: + A new :class:`TPose` object. + + MEOS Functions: + tpose_make + """ + result = tpose_make(tpoint._inner, tradius._inner) + return Temporal._factory(result) + + # ------------------------- Output ---------------------------------------- + def __str__(self): + """ + Returns the string representation of `self`. + + Returns: + A :class:`str` with the string representation of `self`. + + MEOS Functions: + tspatial_as_text + """ + return tspatial_as_text(self._inner, 15) + + def as_wkt(self, precision: int = 15) -> str: + """ + Returns the temporal pose as a WKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal pose. + + MEOS Functions: + tspatial_as_text + """ + return tspatial_as_text(self._inner, precision) + + def as_ewkt(self, precision: int = 15) -> str: + """ + Returns the temporal pose as an EWKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal pose. + + MEOS Functions: + tspatial_as_ewkt + """ + return tspatial_as_ewkt(self._inner, precision) + + # ------------------------- Conversions ----------------------------------- + def to_tpoint(self) -> TPoint: + """ + Returns the temporal point of the positions of `self`. + + Returns: + A new :class:`TPoint` object. + + MEOS Functions: + tpose_to_tpoint + """ + result = tpose_to_tpoint(self._inner) + return Temporal._factory(result) + + # ------------------------- Accessors ------------------------------------- + def bounding_box(self) -> STBox: + """ + Returns the bounding box of `self`. + + Returns: + An :class:`~pymeos.boxes.STBox` representing the bounding box. + + MEOS Functions: + tspatial_to_stbox + """ + from ..boxes import STBox + + return STBox(_inner=tspatial_to_stbox(self._inner)) + + def values(self) -> list[Pose]: + """ + Returns the values of `self`. + + Returns: + A :class:`list` of :class:`Pose` with the values. + + MEOS Functions: + tpose_values + """ + values, count = tpose_values(self._inner) + return [Pose(_inner=values[i]) for i in range(count)] + + def start_value(self) -> Pose: + """ + Returns the start value of `self`. + + Returns: + A :class:`Pose` with the start value. + + MEOS Functions: + tpose_start_value + """ + return Pose(_inner=tpose_start_value(self._inner)) + + def end_value(self) -> Pose: + """ + Returns the end value of `self`. + + Returns: + A :class:`Pose` with the end value. + + MEOS Functions: + tpose_end_value + """ + return Pose(_inner=tpose_end_value(self._inner)) + + def value_set(self) -> set[Pose]: + """ + Returns the set of values of `self`. + + Returns: + A :class:`set` of :class:`Pose` with the values. + + MEOS Functions: + tpose_values + """ + values, count = tpose_values(self._inner) + return {Pose(_inner=values[i]) for i in range(count)} + + def value_at_timestamp(self, timestamp: datetime) -> Pose: + """ + Returns the value of `self` at the given timestamp. + + Args: + timestamp: A :class:`datetime` representing the timestamp. + + Returns: + A :class:`Pose` with the value. + + MEOS Functions: + tpose_value_at_timestamptz + """ + return Pose(_inner=tpose_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True)[0]) + + def value_n(self, n: int) -> Pose: + """ + Returns the ``n``-th value of `self`. + + Args: + n: The 0-based index of the value to return. + + Returns: + A :class:`Pose` with the value. + + MEOS Functions: + tpose_value_n + """ + return Pose(_inner=tpose_value_n(self._inner, n + 1)[0]) + + def points(self) -> PoseSet: + """ + Returns the set of points of `self`. + + Returns: + A :class:`PoseSet` with the points. + + MEOS Functions: + tpose_points + """ + return PoseSet(_inner=tpose_points(self._inner)) + + def trajectory(self, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the trajectory of `self` as a `shapely` + :class:`~shapely.geometry.base.BaseGeometry`. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` representing the + trajectory. + + MEOS Functions: + tpose_trajectory + """ + return gserialized_to_shapely_geometry(tpose_trajectory(self._inner), precision) + + def yaw(self) -> TFloat: + """ + Returns the rotation of `self` as a temporal float. + + Returns: + A new :class:`TFloat` with the rotation. + + MEOS Functions: + tpose_yaw + """ + result = tpose_yaw(self._inner) + return Temporal._factory(result) + + # ------------------------- Spatial Reference System ---------------------- + def srid(self) -> int: + """ + Returns the SRID of `self`. + + Returns: + An :class:`int` representing the SRID. + + MEOS Functions: + tspatial_srid + """ + return tspatial_srid(self._inner) + + def set_srid(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TPose` with the given SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TPose` instance. + + MEOS Functions: + tspatial_set_srid + """ + return self.__class__(_inner=tspatial_set_srid(self._inner, srid)) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> TPose: + """ + Round the coordinate values to a number of decimal places. + + Returns: + A new :class:`TPose` object. + + MEOS Functions: + temporal_round + """ + result = temporal_round(self._inner, max_decimals) + return Temporal._factory(result) + + def transform(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TPose` transformed to another SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TPose` instance. + + MEOS Functions: + tspatial_transform + """ + result = tspatial_transform(self._inner, srid) + return Temporal._factory(result) + + # ------------------------- Ever and Always Comparisons ------------------- + def always_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are always equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are always equal to `value`, + `False` otherwise. + + MEOS Functions: + always_eq_tpose_pose, always_eq_tpose_tpose + """ + if isinstance(value, Pose): + return always_eq_tpose_pose(self._inner, value._inner) > 0 + elif isinstance(value, TPose): + return always_eq_tpose_tpose(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def always_not_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are always not equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are always not equal to `value`, + `False` otherwise. + + MEOS Functions: + always_ne_tpose_pose, always_ne_tpose_tpose + """ + if isinstance(value, Pose): + return always_ne_tpose_pose(self._inner, value._inner) > 0 + elif isinstance(value, TPose): + return always_ne_tpose_tpose(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are ever equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are ever equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tpose_pose, ever_eq_tpose_tpose + """ + if isinstance(value, Pose): + return ever_eq_tpose_pose(self._inner, value._inner) > 0 + elif isinstance(value, TPose): + return ever_eq_tpose_tpose(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_not_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are ever not equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are ever not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tpose_pose, ever_ne_tpose_tpose + """ + if isinstance(value, Pose): + return ever_ne_tpose_pose(self._inner, value._inner) > 0 + elif isinstance(value, TPose): + return ever_ne_tpose_tpose(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def never_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are never equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are never equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_tpose_pose, ever_eq_tpose_tpose + """ + return not self.ever_equal(value) + + def never_not_equal(self, value: Pose | TPose) -> bool: + """ + Returns whether the values of `self` are never not equal to `value`. + + Args: + value: :class:`Pose` or :class:`TPose` to compare. + + Returns: + `True` if the values of `self` are never not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_tpose_pose, ever_ne_tpose_tpose + """ + return not self.ever_not_equal(value) + + # ------------------------- Temporal Comparisons -------------------------- + def temporal_equal(self, other: Pose | TPose) -> TBool: + """ + Returns the temporal equality relation between `self` and `other`. + + Args: + other: A :class:`Pose` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal equality relation. + + MEOS Functions: + teq_tpose_pose, teq_temporal_temporal + """ + if isinstance(other, Pose): + result = teq_tpose_pose(self._inner, other._inner) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other: Pose | TPose) -> TBool: + """ + Returns the temporal not equal relation between `self` and `other`. + + Args: + other: A :class:`Pose` or temporal object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal not equal + relation. + + MEOS Functions: + tne_tpose_pose, tne_temporal_temporal + """ + if isinstance(other, Pose): + result = tne_tpose_pose(self._inner, other._inner) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) + + # ------------------------- Restrictions ---------------------------------- + def at(self, other: Pose | shpb.BaseGeometry | STBox | Time) -> TPose: + """ + Returns a new temporal pose with the values of `self` restricted to + `other`. + + Args: + other: An object to restrict the values of `self` to. + + Returns: + A new :class:`TPose` with the values of `self` restricted to + `other`. + + MEOS Functions: + tpose_at_pose, tpose_at_geom, tpose_at_stbox, + temporal_at_timestamp, temporal_at_tstzset, temporal_at_tstzspan, + temporal_at_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Pose): + result = tpose_at_pose(self._inner, other._inner) + elif isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tpose_at_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tpose_at_stbox(self._inner, other._inner, True) + else: + return super().at(other) + return Temporal._factory(result) + + def minus(self, other: Pose | shpb.BaseGeometry | STBox | Time) -> TPose: + """ + Returns a new temporal pose with the values of `self` restricted to + the complement of `other`. + + Args: + other: An object to restrict the values of `self` to the + complement of. + + Returns: + A new :class:`TPose` with the values of `self` restricted to the + complement of `other`. + + MEOS Functions: + tpose_minus_pose, tpose_minus_geom, tpose_minus_stbox, + temporal_minus_timestamp, temporal_minus_tstzset, + temporal_minus_tstzspan, temporal_minus_tstzspanset + """ + from ..boxes import STBox + + if isinstance(other, Pose): + result = tpose_minus_pose(self._inner, other._inner) + elif isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tpose_minus_geom(self._inner, gs) + elif isinstance(other, STBox): + result = tpose_minus_stbox(self._inner, other._inner, True) + else: + return super().minus(other) + return Temporal._factory(result) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: shpb.BaseGeometry | Pose | TPose) -> TFloat: + """ + Returns the temporal distance between `self` and `other`. + + Args: + other: An object to check the distance to. + + Returns: + A new :class:`TFloat` with the temporal distance. + + MEOS Functions: + tdistance_tpose_geo, tdistance_tpose_pose, tdistance_tpose_tpose + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tdistance_tpose_geo(self._inner, gs) + elif isinstance(other, Pose): + result = tdistance_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = tdistance_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def nearest_approach_distance(self, other: shpb.BaseGeometry | Pose | STBox | TPose) -> float: + """ + Returns the nearest approach distance between `self` and `other`. + + Args: + other: An object to check the nearest approach distance to. + + Returns: + A :class:`float` with the nearest approach distance. + + MEOS Functions: + nad_tpose_geo, nad_tpose_pose, nad_tpose_stbox, nad_tpose_tpose + """ + from ..boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + return nad_tpose_geo(self._inner, gs) + elif isinstance(other, Pose): + return nad_tpose_pose(self._inner, other._inner) + elif isinstance(other, STBox): + return nad_tpose_stbox(self._inner, other._inner) + elif isinstance(other, TPose): + return nad_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + def nearest_approach_instant(self, other: shpb.BaseGeometry | Pose | TPose) -> TPoseInst: + """ + Returns the nearest approach instant between `self` and `other`. + + Args: + other: An object to check the nearest approach instant to. + + Returns: + A new :class:`TPoseInst` with the nearest approach instant. + + MEOS Functions: + nai_tpose_geo, nai_tpose_pose, nai_tpose_tpose + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = nai_tpose_geo(self._inner, gs) + elif isinstance(other, Pose): + result = nai_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = nai_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def shortest_line(self, other: shpb.BaseGeometry | Pose | TPose) -> shpb.BaseGeometry: + """ + Returns the shortest line between `self` and `other`. + + Args: + other: An object to check the shortest line to. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` with the + shortest line. + + MEOS Functions: + shortestline_tpose_geo, shortestline_tpose_pose, + shortestline_tpose_tpose + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = shortestline_tpose_geo(self._inner, gs) + elif isinstance(other, Pose): + result = shortestline_tpose_pose(self._inner, other._inner) + elif isinstance(other, TPose): + result = shortestline_tpose_tpose(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return gserialized_to_shapely_geometry(result, 10) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`TPose` from a database cursor. Used when automatically + loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + if value[0] != "{" and value[0] != "[" and value[0] != "(": + return TPoseInst(string=value) + elif value[0] == "[" or value[0] == "(": + return TPoseSeq(string=value) + elif value[0] == "{": + if value[1] == "[" or value[1] == "(": + return TPoseSeqSet(string=value) + else: + return TPoseSeq(string=value) + raise Exception("ERROR: Could not parse temporal pose value") + + +class TPoseInst(TInstant[Pose, "TPose", "TPoseInst", "TPoseSeq", "TPoseSeqSet"], TPose): + """ + Class for representing temporal poses at a single instant. + """ + + _make_function = lambda *args: None + _cast_function = lambda x: None + + def __init__( + self, + string: str | None = None, + *, + _inner=None, + ) -> None: + super().__init__(string=string, _inner=_inner) + + +class TPoseSeq(TSequence[Pose, "TPose", "TPoseInst", "TPoseSeq", "TPoseSeqSet"], TPose): + """ + Class for representing temporal poses over a tstzspan of time. + """ + + ComponentClass = TPoseInst + + def __init__( + self, + string: str | None = None, + *, + instant_list: list[str | TPoseInst] | None = None, + lower_inc: bool = True, + upper_inc: bool = False, + interpolation: TInterpolation = TInterpolation.LINEAR, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + instant_list=instant_list, + lower_inc=lower_inc, + upper_inc=upper_inc, + interpolation=interpolation, + normalize=normalize, + _inner=_inner, + ) + + +class TPoseSeqSet(TSequenceSet[Pose, "TPose", "TPoseInst", "TPoseSeq", "TPoseSeqSet"], TPose): + """ + Class for representing temporal poses over a tstzspan of time with gaps. + """ + + ComponentClass = TPoseSeq + + def __init__( + self, + string: str | None = None, + *, + sequence_list: list[str | TPoseSeq] | None = None, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + sequence_list=sequence_list, + normalize=normalize, + _inner=_inner, + ) diff --git a/pymeos/main/trgeometry.py b/pymeos/main/trgeometry.py new file mode 100644 index 00000000..5388cd10 --- /dev/null +++ b/pymeos/main/trgeometry.py @@ -0,0 +1,1299 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, TypeVar + +import shapely.geometry.base as shpb +from pymeos_cffi import * + +from ..collections import * +from ..collections.pose import Pose +from ..mixins import TTemporallyComparable +from ..temporal import Temporal, TInstant, TInterpolation, TSequence, TSequenceSet +from .tbool import TBool +from .tfloat import TFloat +from .tpoint import TPoint + +if TYPE_CHECKING: + from ..boxes import STBox + from .tpose import TPose + +Self = TypeVar("Self", bound="TRgeometry") + + +class TRgeometry( + Temporal[ + shpb.BaseGeometry, + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", + ], + TTemporallyComparable, + ABC, +): + """ + Abstract class for temporal rigid geometries. + """ + + _mobilitydb_name = "trgeometry" + + _parse_function = trgeometry_in + + BaseClass = shpb.BaseGeometry + + def __init__(self, _inner) -> None: + super().__init__() + + # ------------------------- Constructors ---------------------------------- + @staticmethod + def from_base_time(value: shpb.BaseGeometry, base: datetime, pose: Pose) -> TRgeometryInst: + """ + Create a temporal rigid geometry instant from a reference geometry, + the pose it takes and a timestamp. + + Args: + value: A :class:`~shapely.geometry.base.BaseGeometry` with the + reference geometry. + base: A :class:`datetime` with the timestamp. + pose: A :class:`Pose` with the pose of the geometry at `base`. + + Returns: + A new :class:`TRgeometryInst` object. + + MEOS Functions: + trgeometryinst_make + """ + if isinstance(base, datetime): + return TRgeometryInst( + _inner=trgeometryinst_make(geometry_to_gserialized(value), pose._inner, datetime_to_timestamptz(base)) + ) + raise TypeError(f"Operation not supported with type {base.__class__}") + + @classmethod + def from_mfjson(cls: type[Self], mfjson: str) -> Self: + """ + Returns a temporal rigid geometry from a MF-JSON string. + + Args: + mfjson: The MF-JSON string. + + Returns: + A temporal rigid geometry. + + MEOS Functions: + trgeometry_from_mfjson + """ + result = trgeometry_from_mfjson(mfjson) + return Temporal._factory(result) + + @staticmethod + def from_geometry_tpose(geometry: shpb.BaseGeometry, tpose: TPose) -> TRgeometry: + """ + Create a temporal rigid geometry from a reference geometry and a + temporal pose. + + Args: + geometry: A :class:`~shapely.geometry.base.BaseGeometry` with the + reference geometry. + tpose: A :class:`TPose` with the temporal pose. + + Returns: + A new :class:`TRgeometry` object. + + MEOS Functions: + geometry_tpose_to_trgeometry + """ + gs = geometry_to_gserialized(geometry) + result = geometry_tpose_to_trgeometry(gs, tpose._inner) + return Temporal._factory(result) + + # ------------------------- Output ---------------------------------------- + def __str__(self): + """ + Returns the string representation of `self`. + + Returns: + A :class:`str` with the string representation of `self`. + + MEOS Functions: + trgeometry_as_text + """ + return trgeometry_as_text(self._inner, 15) + + def as_wkt(self, precision: int = 15) -> str: + """ + Returns the temporal rigid geometry as a WKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal rigid geometry. + + MEOS Functions: + trgeometry_as_text + """ + return trgeometry_as_text(self._inner, precision) + + def as_ewkt(self, precision: int = 15) -> str: + """ + Returns the temporal rigid geometry as an EWKT string. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`str` representing the temporal rigid geometry. + + MEOS Functions: + trgeometry_as_ewkt + """ + return trgeometry_as_ewkt(self._inner, precision) + + # ------------------------- Conversions ----------------------------------- + def to_tpose(self) -> TPose: + """ + Returns the temporal pose of `self`. + + Returns: + A new :class:`TPose` object. + + MEOS Functions: + trgeometry_to_tpose + """ + result = trgeometry_to_tpose(self._inner) + return Temporal._factory(result) + + def to_tpoint(self) -> TPoint: + """ + Returns the temporal point of the positions of `self`. + + Returns: + A new :class:`TPoint` object. + + MEOS Functions: + trgeometry_to_tgeompoint + """ + result = trgeometry_to_tgeompoint(self._inner) + return Temporal._factory(result) + + def to_instant(self) -> TRgeometryInst: + """ + Returns `self` as a :class:`TRgeometryInst`. + + MEOS Functions: + trgeometry_as_tinstant + """ + return Temporal._factory(trgeometry_as_tinstant(self._inner)) + + # ------------------------- Accessors ------------------------------------- + def bounding_box(self) -> STBox: + """ + Returns the bounding box of `self`. + + Returns: + An :class:`~pymeos.boxes.STBox` representing the bounding box. + + MEOS Functions: + tspatial_to_stbox + """ + from ..boxes import STBox + + return STBox(_inner=tspatial_to_stbox(self._inner)) + + def geometry(self, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the reference geometry of `self` as a `shapely` + :class:`~shapely.geometry.base.BaseGeometry`. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` with the + reference geometry. + + MEOS Functions: + trgeometry_geom + """ + return gserialized_to_shapely_geometry(trgeometry_geom(self._inner), precision) + + def values(self, precision: int = 15) -> list[shpb.BaseGeometry]: + """ + Returns the values of `self`. + + Returns: + A :class:`list` of :class:`~shapely.geometry.base.BaseGeometry` + with the values. + + MEOS Functions: + trgeometry_value_n + """ + return [i.value(precision=precision) for i in self.instants()] + + def start_value(self, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the start value of `self`. + + Returns: + A :class:`~shapely.geometry.base.BaseGeometry` with the start + value. + + MEOS Functions: + trgeometry_start_value + """ + return gserialized_to_shapely_geometry(trgeometry_start_value(self._inner), precision) + + def end_value(self, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the end value of `self`. + + Returns: + A :class:`~shapely.geometry.base.BaseGeometry` with the end value. + + MEOS Functions: + trgeometry_end_value + """ + return gserialized_to_shapely_geometry(trgeometry_end_value(self._inner), precision) + + def value_set(self, precision: int = 15) -> set[shpb.BaseGeometry]: + """ + Returns the set of values of `self`. + + Returns: + A :class:`set` of :class:`~shapely.geometry.base.BaseGeometry` + with the values. + + MEOS Functions: + trgeometry_value_n + """ + return {i.value(precision=precision) for i in self.instants()} + + def value_at_timestamp(self, timestamp: datetime, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the value of `self` at the given timestamp. + + Args: + timestamp: A :class:`datetime` representing the timestamp. + precision: An :class:`int` representing the precision of the + coordinates. + + Returns: + A :class:`~shapely.geometry.base.BaseGeometry` with the value. + + MEOS Functions: + trgeometry_value_at_timestamptz + """ + return gserialized_to_shapely_geometry( + trgeometry_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True)[0], + precision, + ) + + def value_n(self, n: int, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the ``n``-th value of `self`. + + Args: + n: The 0-based index of the value to return. + precision: An :class:`int` representing the precision of the + coordinates. + + Returns: + A :class:`~shapely.geometry.base.BaseGeometry` with the value. + + MEOS Functions: + trgeometry_value_n + """ + return gserialized_to_shapely_geometry(trgeometry_value_n(self._inner, n + 1)[0], precision) + + def start_instant(self: Self) -> TRgeometryInst: + """ + Returns the first instant of `self`. + + Returns: + A new :class:`TRgeometryInst` object. + + MEOS Functions: + trgeometry_start_instant + """ + return Temporal._factory(trgeometry_start_instant(self._inner)) + + def end_instant(self: Self) -> TRgeometryInst: + """ + Returns the last instant of `self`. + + Returns: + A new :class:`TRgeometryInst` object. + + MEOS Functions: + trgeometry_end_instant + """ + return Temporal._factory(trgeometry_end_instant(self._inner)) + + def instant_n(self, n: int) -> TRgeometryInst: + """ + Returns the ``n``-th instant of `self`. + + Args: + n: The 0-based index of the instant to return. + + Returns: + A new :class:`TRgeometryInst` object. + + MEOS Functions: + trgeometry_instant_n + """ + return Temporal._factory(trgeometry_instant_n(self._inner, n + 1)) + + def instants(self) -> list[TRgeometryInst]: + """ + Returns the instants of `self`. + + Returns: + A :class:`list` of :class:`TRgeometryInst` objects. + + MEOS Functions: + trgeometry_instants + """ + ins, count = trgeometry_instants(self._inner) + return [Temporal._factory(ins[i]) for i in range(count)] + + def points(self) -> GeometrySet: + """ + Returns the set of points of `self`. + + Returns: + A :class:`GeometrySet` with the points. + + MEOS Functions: + trgeometry_points + """ + from ..factory import _CollectionFactory + + return _CollectionFactory.create_collection(trgeometry_points(self._inner)) + + def yaw(self) -> TFloat: + """ + Returns the rotation of `self` as a temporal float. + + Returns: + A new :class:`TFloat` with the rotation. + + MEOS Functions: + trgeometry_yaw + """ + result = trgeometry_yaw(self._inner) + return Temporal._factory(result) + + def traversed_area(self, unary_union: bool = True, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the traversed area of `self` as a `shapely` + :class:`~shapely.geometry.base.BaseGeometry`. + + Args: + unary_union: Whether to apply a unary union to the result. + precision: The precision of the returned geometry. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` with the + traversed area. + + MEOS Functions: + trgeometry_traversed_area + """ + return gserialized_to_shapely_geometry(trgeometry_traversed_area(self._inner, unary_union), precision) + + # ------------------------- Spatial Reference System ---------------------- + def srid(self) -> int: + """ + Returns the SRID of `self`. + + Returns: + An :class:`int` representing the SRID. + + MEOS Functions: + tspatial_srid + """ + return tspatial_srid(self._inner) + + def set_srid(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TRgeometry` with the given SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TRgeometry` instance. + + MEOS Functions: + tspatial_set_srid + """ + return self.__class__(_inner=tspatial_set_srid(self._inner, srid)) + + # ------------------------- Transformations ------------------------------- + def round(self, max_decimals: int = 0) -> TRgeometry: + """ + Round the coordinate values to a number of decimal places. + + Returns: + A new :class:`TRgeometry` object. + + MEOS Functions: + trgeometry_round + """ + result = trgeometry_round(self._inner, max_decimals) + return Temporal._factory(result) + + def transform(self: Self, srid: int) -> Self: + """ + Returns a new :class:`TRgeometry` transformed to another SRID. + + Args: + srid: The desired SRID. + + Returns: + A new :class:`TRgeometry` instance. + + MEOS Functions: + tspatial_transform + """ + result = tspatial_transform(self._inner, srid) + return Temporal._factory(result) + + def set_interpolation(self: Self, interpolation: TInterpolation) -> Self: + """ + Returns a new :class:`TRgeometry` with the given interpolation. + + Args: + interpolation: The desired interpolation. + + Returns: + A new :class:`TRgeometry` instance. + + MEOS Functions: + trgeometry_set_interp + """ + result = trgeometry_set_interp(self._inner, interpolation.value) + return Temporal._factory(result) + + def append_instant( + self: Self, + instant: TRgeometryInst, + max_dist: float | None = 0.0, + max_time: timedelta | None = None, + ) -> TRgeometry: + """ + Returns a new :class:`TRgeometry` with `instant` appended. + + Args: + instant: The :class:`TRgeometryInst` to append. + max_dist: The maximum distance between consecutive instants. + max_time: The maximum time between consecutive instants. + + Returns: + A new :class:`TRgeometry` object. + + MEOS Functions: + trgeometry_append_tinstant + """ + interp = self.interpolation() + mt = timedelta_to_interval(max_time) if max_time is not None else None + result = trgeometry_append_tinstant(self._inner, instant._inner, interp.value, max_dist, mt, False) + return Temporal._factory(result) + + def append_sequence(self: Self, sequence: TRgeometrySeq) -> TRgeometry: + """ + Returns a new :class:`TRgeometry` with `sequence` appended. + + Args: + sequence: The :class:`TRgeometrySeq` to append. + + Returns: + A new :class:`TRgeometry` object. + + MEOS Functions: + trgeometry_append_tsequence + """ + result = trgeometry_append_tsequence(self._inner, sequence._inner, False) + return Temporal._factory(result) + + # ------------------------- Ever and Always Comparisons ------------------- + def always_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are always equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are always equal to `value`, + `False` otherwise. + + MEOS Functions: + always_eq_trgeometry_geo, always_eq_trgeometry_trgeometry + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return always_eq_trgeometry_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return always_eq_trgeometry_trgeometry(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def always_not_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are always not equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are always not equal to `value`, + `False` otherwise. + + MEOS Functions: + always_ne_trgeometry_geo, always_ne_trgeometry_trgeometry + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return always_ne_trgeometry_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return always_ne_trgeometry_trgeometry(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are ever equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are ever equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_trgeometry_geo, ever_eq_trgeometry_trgeometry + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return ever_eq_trgeometry_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return ever_eq_trgeometry_trgeometry(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def ever_not_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are ever not equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are ever not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_trgeometry_geo, ever_ne_trgeometry_trgeometry + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return ever_ne_trgeometry_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return ever_ne_trgeometry_trgeometry(self._inner, value._inner) > 0 + else: + raise TypeError(f"Operation not supported with type {value.__class__}") + + def never_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are never equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are never equal to `value`, `False` + otherwise. + + MEOS Functions: + ever_eq_trgeometry_geo, ever_eq_trgeometry_trgeometry + """ + return not self.ever_equal(value) + + def never_not_equal(self, value: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether the values of `self` are never not equal to `value`. + + Args: + value: :class:`~shapely.geometry.base.BaseGeometry` or + :class:`TRgeometry` to compare. + + Returns: + `True` if the values of `self` are never not equal to `value`, + `False` otherwise. + + MEOS Functions: + ever_ne_trgeometry_geo, ever_ne_trgeometry_trgeometry + """ + return not self.ever_not_equal(value) + + # ------------------------- Temporal Comparisons -------------------------- + def temporal_equal(self, other: shpb.BaseGeometry | TRgeometry) -> TBool: + """ + Returns the temporal equality relation between `self` and `other`. + + Args: + other: A :class:`~shapely.geometry.base.BaseGeometry` or temporal + object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal equality relation. + + MEOS Functions: + teq_trgeometry_geo, teq_temporal_temporal + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = teq_trgeometry_geo(self._inner, gs) + else: + return super().temporal_equal(other) + return Temporal._factory(result) + + def temporal_not_equal(self, other: shpb.BaseGeometry | TRgeometry) -> TBool: + """ + Returns the temporal not equal relation between `self` and `other`. + + Args: + other: A :class:`~shapely.geometry.base.BaseGeometry` or temporal + object to compare to `self`. + + Returns: + A :class:`TBool` with the result of the temporal not equal + relation. + + MEOS Functions: + tne_trgeometry_geo, tne_temporal_temporal + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tne_trgeometry_geo(self._inner, gs) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) + + # ------------------------- Ever Spatial Relationships -------------------- + # The names and dispatch are those of #TCbuffer.is_ever_covers and its + # siblings, and #TPoint.is_ever_contained_in for a geometry containing self. + def is_ever_contained_in(self, container: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` is ever contained by `container`. + + MEOS Functions: + econtains_geo_trgeometry + """ + if isinstance(container, shpb.BaseGeometry): + result = econtains_geo_trgeometry(geo_to_gserialized(container, False), self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_ever_covers(self, other: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` ever covers `other`. + + MEOS Functions: + ecovers_trgeometry_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = ecovers_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_ever_coveredby(self, container: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` is ever covered by `container`: the covers + relationship with `container` first, as #TRgeometry.is_ever_contained_in + is for contains and as PostGIS's coveredby is for covers. + + MEOS Functions: + ecovers_geo_trgeometry + """ + if isinstance(container, shpb.BaseGeometry): + result = ecovers_geo_trgeometry(geo_to_gserialized(container, False), self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_ever_disjoint(self, other: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether `self` is ever disjoint from `other`. + + MEOS Functions: + edisjoint_trgeometry_geo, edisjoint_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = edisjoint_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, TRgeometry): + result = edisjoint_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_ever_within_distance(self, other: shpb.BaseGeometry | TRgeometry, distance: float) -> bool: + """ + Returns whether `self` is ever within `distance` of `other`. + + MEOS Functions: + edwithin_trgeometry_geo, edwithin_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = edwithin_trgeometry_geo(self._inner, geo_to_gserialized(other, False), distance) + elif isinstance(other, TRgeometry): + result = edwithin_trgeometry_trgeometry(self._inner, other._inner, distance) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def ever_intersects(self, other: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether `self` ever intersects `other`. + + MEOS Functions: + eintersects_trgeometry_geo, eintersects_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = eintersects_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, TRgeometry): + result = eintersects_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def ever_touches(self, other: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` ever touches `other`. + + MEOS Functions: + etouches_trgeometry_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = etouches_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + # ------------------------- Always Spatial Relationships ------------------ + def is_always_contained_in(self, container: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` is always contained by `container`. + + MEOS Functions: + acontains_geo_trgeometry + """ + if isinstance(container, shpb.BaseGeometry): + result = acontains_geo_trgeometry(geo_to_gserialized(container, False), self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_always_covers(self, other: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` always covers `other`. + + MEOS Functions: + acovers_trgeometry_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = acovers_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_coveredby(self, container: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` is always covered by `container`: the covers + relationship with `container` first, as + #TRgeometry.is_always_contained_in is for contains. + + MEOS Functions: + acovers_geo_trgeometry + """ + if isinstance(container, shpb.BaseGeometry): + result = acovers_geo_trgeometry(geo_to_gserialized(container, False), self._inner) + else: + raise TypeError(f"Operation not supported with type {container.__class__}") + return result == 1 + + def is_always_disjoint(self, other: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether `self` is always disjoint from `other`. + + MEOS Functions: + adisjoint_trgeometry_geo, adisjoint_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = adisjoint_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, TRgeometry): + result = adisjoint_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_within_distance(self, other: shpb.BaseGeometry | TRgeometry, distance: float) -> bool: + """ + Returns whether `self` is always within `distance` of `other`. + + MEOS Functions: + adwithin_trgeometry_geo, adwithin_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = adwithin_trgeometry_geo(self._inner, geo_to_gserialized(other, False), distance) + elif isinstance(other, TRgeometry): + result = adwithin_trgeometry_trgeometry(self._inner, other._inner, distance) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def always_intersects(self, other: shpb.BaseGeometry | TRgeometry) -> bool: + """ + Returns whether `self` always intersects `other`. + + MEOS Functions: + aintersects_trgeometry_geo, aintersects_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + result = aintersects_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + elif isinstance(other, TRgeometry): + result = aintersects_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def always_touches(self, other: shpb.BaseGeometry) -> bool: + """ + Returns whether `self` always touches `other`. + + MEOS Functions: + atouches_trgeometry_geo + """ + if isinstance(other, shpb.BaseGeometry): + result = atouches_trgeometry_geo(self._inner, geo_to_gserialized(other, False)) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + # ------------------------- Restrictions ---------------------------------- + def at(self, other: Time) -> TRgeometry: + """ + Returns a new temporal rigid geometry with the values of `self` + restricted to the time `other`. + + Args: + other: A time object to restrict the values of `self` to. + + Returns: + A new :class:`TRgeometry` with the values of `self` restricted to + `other`. + + MEOS Functions: + trgeometry_restrict_timestamptz, trgeometry_restrict_tstzset, + trgeometry_restrict_tstzspan, trgeometry_restrict_tstzspanset + """ + if isinstance(other, datetime): + result = trgeometry_restrict_timestamptz(self._inner, datetime_to_timestamptz(other), True) + elif isinstance(other, TsTzSet): + result = trgeometry_restrict_tstzset(self._inner, other._inner, True) + elif isinstance(other, TsTzSpan): + result = trgeometry_restrict_tstzspan(self._inner, other._inner, True) + elif isinstance(other, TsTzSpanSet): + result = trgeometry_restrict_tstzspanset(self._inner, other._inner, True) + else: + return super().at(other) + return Temporal._factory(result) + + def minus(self, other: Time) -> TRgeometry: + """ + Returns a new temporal rigid geometry with the values of `self` + restricted to the complement of the time `other`. + + Args: + other: A time object to restrict the values of `self` to the + complement of. + + Returns: + A new :class:`TRgeometry` with the values of `self` restricted to + the complement of `other`. + + MEOS Functions: + trgeometry_restrict_timestamptz, trgeometry_restrict_tstzset, + trgeometry_restrict_tstzspan, trgeometry_restrict_tstzspanset + """ + if isinstance(other, datetime): + result = trgeometry_restrict_timestamptz(self._inner, datetime_to_timestamptz(other), False) + elif isinstance(other, TsTzSet): + result = trgeometry_restrict_tstzset(self._inner, other._inner, False) + elif isinstance(other, TsTzSpan): + result = trgeometry_restrict_tstzspan(self._inner, other._inner, False) + elif isinstance(other, TsTzSpanSet): + result = trgeometry_restrict_tstzspanset(self._inner, other._inner, False) + else: + return super().minus(other) + return Temporal._factory(result) + + # ------------------------- Splitting ------------------------------------- + def segments(self) -> list[TRgeometrySeq]: + """ + Returns the temporal segments of `self`. + + MEOS Functions: + trgeometry_segments + """ + seqs, count = trgeometry_segments(self._inner) + return [Temporal._factory(seqs[i]) for i in range(count)] + + # ------------------------- Modifications --------------------------------- + def delete(self, other: Time, connect: bool = True) -> TRgeometry: + """ + Returns a new temporal rigid geometry equal to `self` with the + elements at `other` removed. + + Args: + other: A time object to remove from `self`. + connect: Whether to connect the resulting segments. + + MEOS Functions: + trgeometry_delete_timestamptz, trgeometry_delete_tstzset, + trgeometry_delete_tstzspan, trgeometry_delete_tstzspanset + """ + if isinstance(other, datetime): + result = trgeometry_delete_timestamptz(self._inner, datetime_to_timestamptz(other), connect) + elif isinstance(other, TsTzSet): + result = trgeometry_delete_tstzset(self._inner, other._inner, connect) + elif isinstance(other, TsTzSpan): + result = trgeometry_delete_tstzspan(self._inner, other._inner, connect) + elif isinstance(other, TsTzSpanSet): + result = trgeometry_delete_tstzspanset(self._inner, other._inner, connect) + else: + return super().delete(other, connect) + return Temporal._factory(result) + + def before(self, timestamp: datetime, strict: bool = False) -> TRgeometry: + """ + Returns a new temporal rigid geometry with the values of `self` + before `timestamp`. + + Args: + timestamp: A :class:`datetime` to restrict before. + strict: Whether the bound is strict. + + MEOS Functions: + trgeometry_before_timestamptz + """ + result = trgeometry_before_timestamptz(self._inner, datetime_to_timestamptz(timestamp), strict) + return Temporal._factory(result) + + def after(self, timestamp: datetime, strict: bool = False) -> TRgeometry: + """ + Returns a new temporal rigid geometry with the values of `self` + after `timestamp`. + + Args: + timestamp: A :class:`datetime` to restrict after. + strict: Whether the bound is strict. + + MEOS Functions: + trgeometry_after_timestamptz + """ + result = trgeometry_after_timestamptz(self._inner, datetime_to_timestamptz(timestamp), strict) + return Temporal._factory(result) + + # ------------------------- Distance Operations --------------------------- + def distance(self, other: shpb.BaseGeometry | TPoint | TRgeometry) -> TFloat: + """ + Returns the temporal distance between `self` and `other`. + + Args: + other: An object to check the distance to. + + Returns: + A new :class:`TFloat` with the temporal distance. + + MEOS Functions: + tdistance_trgeometry_geo, tdistance_trgeometry_tpoint, tdistance_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tdistance_trgeometry_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = tdistance_trgeometry_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = tdistance_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def nearest_approach_distance(self, other: shpb.BaseGeometry | STBox | TPoint | TRgeometry) -> float: + """ + Returns the nearest approach distance between `self` and `other`. + + Args: + other: An object to check the nearest approach distance to. + + Returns: + A :class:`float` with the nearest approach distance. + + MEOS Functions: + nad_trgeometry_geo, nad_trgeometry_stbox, nad_trgeometry_tpoint, nad_trgeometry_trgeometry + """ + from ..boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + return nad_trgeometry_geo(self._inner, gs) + elif isinstance(other, STBox): + return nad_trgeometry_stbox(self._inner, other._inner) + elif isinstance(other, TPoint): + return nad_trgeometry_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + return nad_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + + def nearest_approach_instant(self, other: shpb.BaseGeometry | TPoint | TRgeometry) -> TRgeometryInst: + """ + Returns the nearest approach instant between `self` and `other`. + + Args: + other: An object to check the nearest approach instant to. + + Returns: + A new :class:`TRgeometryInst` with the nearest approach instant. + + MEOS Functions: + nai_trgeometry_geo, nai_trgeometry_tpoint, nai_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = nai_trgeometry_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = nai_trgeometry_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = nai_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return Temporal._factory(result) + + def shortest_line(self, other: shpb.BaseGeometry | TPoint | TRgeometry) -> shpb.BaseGeometry: + """ + Returns the shortest line between `self` and `other`. + + Args: + other: An object to check the shortest line to. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` with the + shortest line. + + MEOS Functions: + shortestline_trgeometry_geo, shortestline_trgeometry_tpoint, + shortestline_trgeometry_trgeometry + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = shortestline_trgeometry_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = shortestline_trgeometry_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = shortestline_trgeometry_trgeometry(self._inner, other._inner) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return gserialized_to_shapely_geometry(result, 10) + + # ------------------------- Database Operations --------------------------- + @staticmethod + def read_from_cursor(value, _=None): + """ + Reads a :class:`TRgeometry` from a database cursor. Used when + automatically loading objects from the database. + Users should use the class constructor instead. + """ + if not value: + return None + if value[0] != "{" and value[0] != "[" and value[0] != "(": + return TRgeometryInst(string=value) + elif value[0] == "[" or value[0] == "(": + return TRgeometrySeq(string=value) + elif value[0] == "{": + if value[1] == "[" or value[1] == "(": + return TRgeometrySeqSet(string=value) + else: + return TRgeometrySeq(string=value) + raise Exception("ERROR: Could not parse temporal rigid geometry value") + + +class TRgeometryInst( + TInstant[ + shpb.BaseGeometry, + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", + ], + TRgeometry, +): + """ + Class for representing temporal rigid geometries at a single instant. + """ + + _make_function = lambda *args: None + _cast_function = lambda x: None + + def __init__( + self, + string: str | None = None, + *, + geometry: shpb.BaseGeometry | None = None, + pose: Pose | None = None, + timestamp: str | datetime | None = None, + _inner=None, + ) -> None: + if string is None and _inner is None: + gs = geometry_to_gserialized(geometry) + ts = ( + datetime_to_timestamptz(timestamp) if isinstance(timestamp, datetime) else timestamptz_in(timestamp, -1) + ) + _inner = trgeometryinst_make(gs, pose._inner, ts) + super().__init__(string=string, _inner=_inner) + + def value(self, precision: int = 15) -> shpb.BaseGeometry: + """ + Returns the value of the temporal rigid geometry instant, its geometry + placed by the pose of the instant, as #TPointInst.value does for a point. + + Args: + precision: The precision of the returned geometry. + + Returns: + A new :class:`~shapely.geometry.base.BaseGeometry` representing the + value of the temporal rigid geometry instant. + """ + return self.start_value(precision=precision) + + +class TRgeometrySeq( + TSequence[ + shpb.BaseGeometry, + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", + ], + TRgeometry, +): + """ + Class for representing temporal rigid geometries over a tstzspan of time. + """ + + ComponentClass = TRgeometryInst + + def __init__( + self, + string: str | None = None, + *, + instant_list: list[str | TRgeometryInst] | None = None, + lower_inc: bool = True, + upper_inc: bool = False, + interpolation: TInterpolation = TInterpolation.LINEAR, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + instant_list=instant_list, + lower_inc=lower_inc, + upper_inc=upper_inc, + interpolation=interpolation, + normalize=normalize, + _inner=_inner, + ) + + +class TRgeometrySeqSet( + TSequenceSet[ + shpb.BaseGeometry, + "TRgeometry", + "TRgeometryInst", + "TRgeometrySeq", + "TRgeometrySeqSet", + ], + TRgeometry, +): + """ + Class for representing temporal rigid geometries over a tstzspan of time + with gaps. + """ + + ComponentClass = TRgeometrySeq + + def __init__( + self, + string: str | None = None, + *, + sequence_list: list[str | TRgeometrySeq] | None = None, + normalize: bool = True, + _inner=None, + ): + super().__init__( + string=string, + sequence_list=sequence_list, + normalize=normalize, + _inner=_inner, + ) + + # ------------------------- Accessors ------------------------------------- + def start_sequence(self) -> TRgeometrySeq: + """ + Returns the first sequence of `self`. + + MEOS Functions: + trgeometry_start_sequence + """ + return self.ComponentClass(_inner=trgeometry_start_sequence(self._inner)) + + def end_sequence(self) -> TRgeometrySeq: + """ + Returns the last sequence of `self`. + + MEOS Functions: + trgeometry_end_sequence + """ + return self.ComponentClass(_inner=trgeometry_end_sequence(self._inner)) + + def sequence_n(self, n: int) -> TRgeometrySeq: + """ + Returns the ``n``-th sequence of `self` (0-based). + + MEOS Functions: + trgeometry_sequence_n + """ + return self.ComponentClass(_inner=trgeometry_sequence_n(self._inner, n + 1)) + + def sequences(self) -> list[TRgeometrySeq]: + """ + Returns the list of sequences of `self`. + + MEOS Functions: + trgeometry_sequences + """ + seqs, count = trgeometry_sequences(self._inner) + return [self.ComponentClass(_inner=seqs[i]) for i in range(count)] diff --git a/tests/main/tcbuffer_test.py b/tests/main/tcbuffer_test.py new file mode 100644 index 00000000..f30094e2 --- /dev/null +++ b/tests/main/tcbuffer_test.py @@ -0,0 +1,244 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest +from shapely import Point + +from pymeos import ( + Cbuffer, + CbufferSet, + TBool, + TCbuffer, + TCbufferInst, + TCbufferSeq, + TCbufferSeqSet, + TInterpolation, + TsTzSet, + TsTzSpan, + TsTzSpanSet, +) +from tests.conftest import TestPyMEOS + + +class TestTCbuffer(TestPyMEOS): + pass + + +class TestTCbufferConstructors(TestTCbuffer): + tci = TCbufferInst("Cbuffer(Point(1 1), 0.5)@2019-09-01") + tcds = TCbufferSeq("{Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02}") + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + tcss = TCbufferSeqSet( + "{[Cbuffer(Point(1 1), 0.5)@2019-09-01," + " Cbuffer(Point(2 2), 0.3)@2019-09-02]," + "[Cbuffer(Point(1 1), 0.5)@2019-09-03," + " Cbuffer(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "temporal, expected_type", + [ + (tci, TCbufferInst), + (tcds, TCbufferSeq), + (tcs, TCbufferSeq), + (tcss, TCbufferSeqSet), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_string_constructor(self, temporal, expected_type): + assert isinstance(temporal, expected_type) + + @pytest.mark.parametrize( + "temporal", + [tci, tcds, tcs, tcss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_as_hexwkb_constructor(self, temporal): + assert temporal == temporal.__class__.from_hexwkb(temporal.as_hexwkb()) + + @pytest.mark.parametrize( + "temporal", + [tci, tcds, tcs, tcss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_copy_constructor(self, temporal): + other = copy(temporal) + assert temporal == other + assert temporal is not other + + +class TestTCbufferOutputs(TestTCbuffer): + tci = TCbufferInst("Cbuffer(Point(1 1), 0.5)@2019-09-01") + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + + def test_repr(self): + assert isinstance(repr(self.tci), str) + + def test_as_wkt(self): + assert isinstance(self.tcs.as_wkt(), str) + + +class TestTCbufferAccessors(TestTCbuffer): + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + + def test_interpolation(self): + assert self.tcs.interpolation() == TInterpolation.LINEAR + + def test_srid(self): + assert isinstance(self.tcs.srid(), int) + + def test_to_tfloat(self): + assert self.tcs.to_tfloat() is not None + + def test_to_tgeompoint(self): + assert self.tcs.to_tgeompoint() is not None + + +class TestTCbufferEverAlways(TestTCbuffer): + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + + def test_ever_equal(self): + assert isinstance(self.tcs.ever_equal(Cbuffer("Cbuffer(Point(1 1), 0.5)")), bool) + + def test_never_not_equal(self): + assert isinstance(self.tcs.never_not_equal(Cbuffer("Cbuffer(Point(1 1), 0.5)")), bool) + + +class TestTCbufferDistance(TestTCbuffer): + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + + def test_nearest_approach_distance(self): + assert isinstance( + self.tcs.nearest_approach_distance(Cbuffer("Cbuffer(Point(0 0), 0.0)")), + float, + ) + + +class TestTCbufferSpatialRelationships(TestTCbuffer): + tcs = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + other = Cbuffer("Cbuffer(Point(1 1), 0.5)") + + def test_ever_relationships(self): + assert isinstance(self.tcs.ever_intersects(self.other), bool) + assert isinstance(self.tcs.is_ever_disjoint(self.other), bool) + assert isinstance(self.tcs.ever_intersects(Point(1, 1)), bool) + assert isinstance(self.tcs.is_ever_within_distance(self.other, 1.0), bool) + + def test_always_relationships(self): + assert isinstance(self.tcs.always_intersects(self.other), bool) + assert isinstance(self.tcs.is_always_disjoint(self.other), bool) + + # As #TestTRgeometrySpatialRelationships.test_ever_relationships, values + # decided by a buffer around the whole motion and a motion far from it. + def test_containment_and_temporal_arms(self): + around = Cbuffer("Cbuffer(Point(1.5 1.5), 10)") + far = TCbufferSeq("[Cbuffer(Point(100 100), 0.5)@2019-09-01, Cbuffer(Point(101 101), 0.5)@2019-09-02]") + assert self.tcs.is_ever_contained_in(around) is True + assert self.tcs.is_always_contained_in(around) is True + assert self.tcs.is_ever_contained_in(Point(100, 100)) is False + assert self.tcs.is_always_contained_in(Point(100, 100)) is False + assert self.tcs.is_ever_coveredby(around) is True + assert self.tcs.is_always_coveredby(around) is True + assert self.tcs.is_ever_coveredby(Point(100, 100)) is False + assert self.tcs.is_always_coveredby(Point(100, 100)) is False + assert self.tcs.is_ever_contains(far) is False + assert self.tcs.is_always_contains(far) is False + assert self.tcs.is_always_covers(far) is False + assert self.tcs.is_ever_disjoint(far) is True + + def test_temporal_relationships(self): + r = self.tcs.intersects(self.other) + assert r is None or isinstance(r, TBool) + r = self.tcs.within_distance(self.other, 1.0) + assert r is None or isinstance(r, TBool) + r = self.tcs.disjoint(Point(1, 1)) + assert r is None or isinstance(r, TBool) + + +class TestCbuffer(TestPyMEOS): + c = Cbuffer("Cbuffer(Point(1 1), 0.5)") + + def test_string_constructor(self): + assert isinstance(self.c, Cbuffer) + + def test_str(self): + assert str(self.c) == "Cbuffer(POINT(1 1),0.5)" + + def test_from_as_hexwkb(self): + assert self.c == Cbuffer.from_hexwkb(self.c.as_hexwkb()) + + def test_copy(self): + other = copy(self.c) + assert self.c == other + assert self.c is not other + + +class TestCbufferSet(TestPyMEOS): + cs = CbufferSet('{"Cbuffer(Point(1 1),0.5)", "Cbuffer(Point(2 2),0.3)"}') + cs_srid = CbufferSet('SRID=4326;{"Cbuffer(Point(1 1),0.5)"}') + + def test_str(self): + assert str(self.cs) == '{"Cbuffer(POINT(1 1),0.5)", "Cbuffer(POINT(2 2),0.3)"}' + + def test_as_text(self): + assert self.cs.as_text() == '{"Cbuffer(POINT(1 1),0.5)", "Cbuffer(POINT(2 2),0.3)"}' + assert self.cs.as_wkt() == self.cs.as_text() + + def test_as_ewkt(self): + assert self.cs_srid.as_ewkt() == 'SRID=4326;{"Cbuffer(POINT(1 1),0.5)"}' + assert self.cs_srid.as_text() == '{"Cbuffer(POINT(1 1),0.5)"}' + + +class TestTCbufferValueFunctions(TestTCbuffer): + value = Cbuffer("Cbuffer(Point(1 1), 0.5)") + other = Cbuffer("Cbuffer(Point(2 2), 0.3)") + ti = TCbufferInst("Cbuffer(Point(1 1), 0.5)@2019-09-01") + ts = TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02]") + tss = TCbufferSeqSet( + "{[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(2 2), 0.3)@2019-09-02], [Cbuffer(Point(1 1), 0.5)@2019-09-03, Cbuffer(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "base, expected", + [ + (datetime(2019, 9, 1, tzinfo=timezone.utc), TCbufferInst("Cbuffer(Point(1 1), 0.5)@2019-09-01")), + ( + TsTzSet("{2019-09-01, 2019-09-02}"), + TCbufferSeq("{Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(1 1), 0.5)@2019-09-02}"), + ), + ( + TsTzSpan("[2019-09-01, 2019-09-02]"), + TCbufferSeq("[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(1 1), 0.5)@2019-09-02]"), + ), + ( + TsTzSpanSet("{[2019-09-01, 2019-09-02], [2019-09-03, 2019-09-05]}"), + TCbufferSeqSet( + "{[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(1 1), 0.5)@2019-09-02], [Cbuffer(Point(1 1), 0.5)@2019-09-03, Cbuffer(Point(1 1), 0.5)@2019-09-05]}" + ), + ), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_base_time_constructor(self, base, expected): + assert TCbuffer.from_base_time(self.value, base, TInterpolation.LINEAR) == expected + + def test_from_base_temporal_constructor(self): + assert TCbuffer.from_base_temporal(self.value, self.tss) == TCbufferSeqSet( + "{[Cbuffer(Point(1 1), 0.5)@2019-09-01, Cbuffer(Point(1 1), 0.5)@2019-09-02], [Cbuffer(Point(1 1), 0.5)@2019-09-03, Cbuffer(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize("temporal", [ti, ts, tss], ids=["Instant", "Sequence", "SequenceSet"]) + def test_from_as_mfjson_constructor(self, temporal): + assert temporal.__class__.from_mfjson(temporal.as_mfjson()) == temporal + + def test_start_value(self): + assert self.tss.start_value() == self.value + + def test_end_value(self): + assert self.tss.end_value() == self.value + + def test_value_set(self): + assert self.tss.value_set() == {self.value, self.other} + + def test_value_at_timestamp(self): + assert self.ts.value_at_timestamp(datetime(2019, 9, 2, tzinfo=timezone.utc)) == self.other diff --git a/tests/main/tnpoint_test.py b/tests/main/tnpoint_test.py new file mode 100644 index 00000000..c53fbaa9 --- /dev/null +++ b/tests/main/tnpoint_test.py @@ -0,0 +1,324 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest + +from pymeos import ( + Npoint, + NpointSet, + Nsegment, + TFloat, + TInterpolation, + TNpoint, + TNpointInst, + TNpointSeq, + TNpointSeqSet, + TsTzSet, + TsTzSpan, + TsTzSpanSet, +) +from tests.conftest import TestPyMEOS + + +class TestTNpoint(TestPyMEOS): + pass + + +class TestTNpointConstructors(TestTNpoint): + tpi = TNpointInst("NPoint(1, 0.5)@2019-09-01") + tpds = TNpointSeq("{NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02}") + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + tpss = TNpointSeqSet( + "{[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]," + "[NPoint(1, 0.5)@2019-09-03, NPoint(1, 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "source, type, interpolation", + [ + (tpi, TNpointInst, TInterpolation.NONE), + (tpds, TNpointSeq, TInterpolation.DISCRETE), + (tps, TNpointSeq, TInterpolation.LINEAR), + (tpss, TNpointSeqSet, TInterpolation.LINEAR), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_string_constructor(self, source, type, interpolation): + assert isinstance(source, type) + assert source.interpolation() == interpolation + + @pytest.mark.parametrize( + "temporal", + [tpi, tpds, tps, tpss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_as_constructor(self, temporal): + assert temporal == temporal.from_wkb(temporal.as_wkb()) + assert temporal == temporal.from_hexwkb(temporal.as_hexwkb()) + + @pytest.mark.parametrize( + "temporal", + [tpi, tpds, tps, tpss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_copy_constructor(self, temporal): + other = copy(temporal) + assert temporal == other + assert temporal is not other + + def test_instant_value_timestamp_constructor(self): + tpi = TNpointInst(value=Npoint(route=1, position=0.5), timestamp="2019-09-01") + assert isinstance(tpi, TNpointInst) + assert tpi.route() == 1 + + +class TestTNpointOutputs(TestTNpoint): + tpi = TNpointInst("NPoint(1, 0.5)@2019-09-01") + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + + @pytest.mark.parametrize( + "temporal", + [tpi, tps], + ids=["Instant", "Sequence"], + ) + def test_str_round_trip(self, temporal): + assert isinstance(str(temporal), str) + assert temporal == temporal.__class__(str(temporal)) + + @pytest.mark.parametrize( + "temporal", + [tpi, tps], + ids=["Instant", "Sequence"], + ) + def test_as_wkt(self, temporal): + assert isinstance(temporal.as_wkt(), str) + + @pytest.mark.parametrize( + "temporal, expected", + [ + (tpi, "TNpointInst(NPoint(1,0.5)@2019-09-01 00:00:00+00)"), + (tps, "TNpointSeq([NPoint(1,0.5)@2019-09-01 00:00:00+00, NPoint(1,0.7)@2019-09-02 00:00:00+00])"), + ], + ids=["Instant", "Sequence"], + ) + def test_repr(self, temporal, expected): + assert repr(temporal) == expected + + +class TestTNpointAccessors(TestTNpoint): + tpi = TNpointInst("NPoint(1, 0.5)@2019-09-01") + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + + def test_route(self): + assert self.tpi.route() == 1 + assert self.tps.route() == 1 + + def test_srid(self): + assert isinstance(self.tps.srid(), int) + + def test_length(self): + assert isinstance(self.tps.length(), float) + + def test_cumulative_length(self): + assert isinstance(self.tps.cumulative_length(), TFloat) + + def test_speed(self): + assert isinstance(self.tps.speed(), TFloat) + + def test_bounding_box(self): + from pymeos import STBox + + assert isinstance(self.tps.bounding_box(), STBox) + + def test_to_tgeompoint(self): + from pymeos import TGeomPoint + + assert isinstance(self.tps.to_tgeompoint(), TGeomPoint) + + +class TestTNpointComparisons(TestTNpoint): + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + other = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + + def test_eq(self): + assert self.tps == self.other + + def test_ever_equal(self): + assert self.tps.ever_equal(Npoint(route=1, position=0.5)) + + def test_never_not_equal(self): + assert isinstance(self.tps.never_not_equal(Npoint(route=1, position=0.5)), bool) + + def test_temporal_equal(self): + from pymeos import TBool + + assert isinstance(self.tps.temporal_equal(Npoint(route=1, position=0.5)), TBool) + + def test_distance(self): + assert isinstance(self.tps.distance(self.other), TFloat) + + def test_nearest_approach_distance(self): + assert isinstance(self.tps.nearest_approach_distance(self.other), float) + + def test_nearest_approach_instant(self): + assert isinstance(self.tps.nearest_approach_instant(self.other), TNpointInst) + + +class TestTNpointRestrictions(TestTNpoint): + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + + def test_at_timestamp(self): + result = self.tps.at(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert isinstance(result, TNpoint) + + def test_minus_timestamp(self): + result = self.tps.minus(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert isinstance(result, TNpoint) + + +class TestNpoint(TestTNpoint): + np = Npoint("NPoint(1, 0.5)") + + def test_string_constructor(self): + assert isinstance(self.np, Npoint) + + def test_str(self): + assert str(self.np) == "NPoint(1,0.5)" + + def test_route_position(self): + assert self.np.route() == 1 + assert self.np.position() == 0.5 + + def test_make_constructor(self): + assert Npoint(route=1, position=0.5) == self.np + + def test_from_as_wkb(self): + assert self.np == Npoint.from_wkb(self.np.as_wkb()) + assert self.np == Npoint.from_hexwkb(self.np.as_hexwkb()) + + def test_str_round_trip(self): + assert self.np == Npoint(str(self.np)) + + def test_hash(self): + assert isinstance(hash(self.np), int) + + def test_to_stbox(self): + from pymeos import STBox + + assert isinstance(self.np.to_stbox(), STBox) + + +class TestNpointSet(TestTNpoint): + nps = NpointSet('{"NPoint(1, 0.5)", "NPoint(2, 0.3)"}') + + def test_string_constructor(self): + assert isinstance(self.nps, NpointSet) + + def test_elements_constructor(self): + other = NpointSet(elements=[Npoint(route=1, position=0.5), Npoint(route=2, position=0.3)]) + assert other == self.nps + + def test_start_end_element(self): + assert isinstance(self.nps.start_element(), Npoint) + assert isinstance(self.nps.end_element(), Npoint) + + def test_element_n(self): + assert isinstance(self.nps.element_n(0), Npoint) + + def test_elements(self): + elems = self.nps.elements() + assert all(isinstance(e, Npoint) for e in elems) + + def test_str_round_trip(self): + assert self.nps == NpointSet(str(self.nps)) + + def test_str(self): + assert str(self.nps) == '{"NPoint(1,0.5)", "NPoint(2,0.3)"}' + + def test_as_text(self): + assert self.nps.as_text() == '{"NPoint(1,0.5)", "NPoint(2,0.3)"}' + assert self.nps.as_wkt() == self.nps.as_text() + + def test_as_ewkt(self): + assert self.nps.as_ewkt() == 'SRID=5676;{"NPoint(1,0.5)", "NPoint(2,0.3)"}' + + +class TestTNpointPositions(TestTNpoint): + tps = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + + def test_positions(self): + pos = self.tps.positions() + assert all(isinstance(p, Nsegment) for p in pos) + + +class TestNsegment(TestPyMEOS): + ns = Nsegment("NSegment(1, 0.2, 0.6)") + + def test_constructors(self): + assert isinstance(self.ns, Nsegment) + assert isinstance(Nsegment(rid=1, pos1=0.2, pos2=0.6), Nsegment) + + def test_accessors(self): + assert isinstance(self.ns.route(), int) + assert isinstance(self.ns.start_position(), float) + assert isinstance(self.ns.end_position(), float) + assert isinstance(self.ns.srid(), int) + + def test_copy_eq(self): + other = copy(self.ns) + assert self.ns == other + + def test_round(self): + assert isinstance(self.ns.round(2), Nsegment) + + +class TestTNpointValueFunctions(TestTNpoint): + value = Npoint("NPoint(1, 0.5)") + other = Npoint("NPoint(1, 0.7)") + ti = TNpointInst("NPoint(1, 0.5)@2019-09-01") + ts = TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02]") + tss = TNpointSeqSet( + "{[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.7)@2019-09-02], [NPoint(1, 0.5)@2019-09-03, NPoint(1, 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "base, expected", + [ + (datetime(2019, 9, 1, tzinfo=timezone.utc), TNpointInst("NPoint(1, 0.5)@2019-09-01")), + (TsTzSet("{2019-09-01, 2019-09-02}"), TNpointSeq("{NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.5)@2019-09-02}")), + ( + TsTzSpan("[2019-09-01, 2019-09-02]"), + TNpointSeq("[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.5)@2019-09-02]"), + ), + ( + TsTzSpanSet("{[2019-09-01, 2019-09-02], [2019-09-03, 2019-09-05]}"), + TNpointSeqSet( + "{[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.5)@2019-09-02], [NPoint(1, 0.5)@2019-09-03, NPoint(1, 0.5)@2019-09-05]}" + ), + ), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_base_time_constructor(self, base, expected): + assert TNpoint.from_base_time(self.value, base, TInterpolation.LINEAR) == expected + + def test_from_base_temporal_constructor(self): + assert TNpoint.from_base_temporal(self.value, self.tss) == TNpointSeqSet( + "{[NPoint(1, 0.5)@2019-09-01, NPoint(1, 0.5)@2019-09-02], [NPoint(1, 0.5)@2019-09-03, NPoint(1, 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize("temporal", [ti, ts, tss], ids=["Instant", "Sequence", "SequenceSet"]) + def test_from_as_mfjson_constructor(self, temporal): + assert temporal.__class__.from_mfjson(temporal.as_mfjson()) == temporal + + def test_start_value(self): + assert self.tss.start_value() == self.value + + def test_end_value(self): + assert self.tss.end_value() == self.value + + def test_value_set(self): + assert self.tss.value_set() == {self.value, self.other} + + def test_value_at_timestamp(self): + assert self.ts.value_at_timestamp(datetime(2019, 9, 2, tzinfo=timezone.utc)) == self.other diff --git a/tests/main/tpose_test.py b/tests/main/tpose_test.py new file mode 100644 index 00000000..b93d463b --- /dev/null +++ b/tests/main/tpose_test.py @@ -0,0 +1,241 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest + +from pymeos import ( + Pose, + PoseSet, + TInterpolation, + TPose, + TPoseInst, + TPoseSeq, + TPoseSeqSet, + TsTzSet, + TsTzSpan, + TsTzSpanSet, +) +from tests.conftest import TestPyMEOS + + +class TestTPose(TestPyMEOS): + pass + + +class TestTPoseConstructors(TestTPose): + tpi = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + tpds = TPoseSeq("{Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02}") + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + tpss = TPoseSeqSet( + "{[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]," + "[Pose(Point(1 1), 0.5)@2019-09-03, Pose(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "temporal, expected_type", + [ + (tpi, TPoseInst), + (tpds, TPoseSeq), + (tps, TPoseSeq), + (tpss, TPoseSeqSet), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_string_constructor(self, temporal, expected_type): + assert isinstance(temporal, expected_type) + + @pytest.mark.parametrize( + "temporal", + [tpi, tpds, tps, tpss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_as_hexwkb_constructor(self, temporal): + assert temporal == temporal.__class__.from_hexwkb(temporal.as_hexwkb()) + + @pytest.mark.parametrize( + "temporal", + [tpi, tpds, tps, tpss], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_copy_constructor(self, temporal): + other = copy(temporal) + assert temporal == other + assert temporal is not other + + +class TestTPoseOutputs(TestTPose): + tpi = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + + def test_str(self): + assert str(self.tpi) == "Pose(POINT(1 1),0.5)@2019-09-01 00:00:00+00" + + def test_repr(self): + assert isinstance(repr(self.tpi), str) + + def test_as_wkt(self): + assert isinstance(self.tps.as_wkt(), str) + + +class TestTPoseAccessors(TestTPose): + tpi = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + + def test_interpolation(self): + assert self.tps.interpolation() == TInterpolation.LINEAR + + def test_start_value(self): + assert isinstance(self.tps.start_value(), Pose) + + def test_end_value(self): + assert isinstance(self.tps.end_value(), Pose) + + def test_values(self): + values = self.tps.values() + assert all(isinstance(v, Pose) for v in values) + + def test_value_at_timestamp(self): + value = self.tps.value_at_timestamp(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert isinstance(value, Pose) + + def test_srid(self): + assert isinstance(self.tpi.srid(), int) + + def test_to_tpoint(self): + assert self.tps.to_tpoint() is not None + + +class TestTPoseEverAlways(TestTPose): + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + + def test_ever_equal(self): + assert self.tps.ever_equal(Pose("Pose(Point(1 1), 0.5)")) + + def test_never_not_equal(self): + assert isinstance(self.tps.never_not_equal(Pose("Pose(Point(1 1), 0.5)")), bool) + + +class TestTPoseRestrictions(TestTPose): + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + + def test_at_pose(self): + result = self.tps.at(Pose("Pose(Point(1 1), 0.5)")) + assert result is None or isinstance(result, TPose) + + def test_minus_pose(self): + result = self.tps.minus(Pose("Pose(Point(1 1), 0.5)")) + assert result is None or isinstance(result, TPose) + + +class TestTPoseDistance(TestTPose): + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + + def test_nearest_approach_distance(self): + assert isinstance( + self.tps.nearest_approach_distance(Pose("Pose(Point(0 0), 0.0)")), + float, + ) + + +class TestPose(TestPyMEOS): + p = Pose("Pose(Point(1 1), 0.5)") + + def test_string_constructor(self): + assert isinstance(self.p, Pose) + + def test_str(self): + assert str(self.p) == "Pose(POINT(1 1),0.5)" + + def test_from_as_hexwkb(self): + assert self.p == Pose.from_hexwkb(self.p.as_hexwkb()) + + def test_copy(self): + other = copy(self.p) + assert self.p == other + assert self.p is not other + + def test_yaw(self): + assert isinstance(self.p.yaw(), float) + + def test_srid(self): + assert isinstance(self.p.srid(), int) + + def test_round(self): + assert isinstance(self.p.round(2), Pose) + + def test_comparisons(self): + other = Pose("Pose(Point(2 2), 0.3)") + assert self.p != other + assert self.p == copy(self.p) + assert isinstance(self.p.cmp(other), int) + + +class TestTPoseValueFunctions(TestTPose): + value = Pose("Pose(Point(1 1), 0.5)") + other = Pose("Pose(Point(2 2), 0.3)") + ti = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + ts = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + tss = TPoseSeqSet( + "{[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02], [Pose(Point(1 1), 0.5)@2019-09-03, Pose(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize( + "base, expected", + [ + (datetime(2019, 9, 1, tzinfo=timezone.utc), TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01")), + ( + TsTzSet("{2019-09-01, 2019-09-02}"), + TPoseSeq("{Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(1 1), 0.5)@2019-09-02}"), + ), + ( + TsTzSpan("[2019-09-01, 2019-09-02]"), + TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(1 1), 0.5)@2019-09-02]"), + ), + ( + TsTzSpanSet("{[2019-09-01, 2019-09-02], [2019-09-03, 2019-09-05]}"), + TPoseSeqSet( + "{[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(1 1), 0.5)@2019-09-02], [Pose(Point(1 1), 0.5)@2019-09-03, Pose(Point(1 1), 0.5)@2019-09-05]}" + ), + ), + ], + ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], + ) + def test_from_base_time_constructor(self, base, expected): + assert TPose.from_base_time(self.value, base, TInterpolation.LINEAR) == expected + + def test_from_base_temporal_constructor(self): + assert TPose.from_base_temporal(self.value, self.tss) == TPoseSeqSet( + "{[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(1 1), 0.5)@2019-09-02], [Pose(Point(1 1), 0.5)@2019-09-03, Pose(Point(1 1), 0.5)@2019-09-05]}" + ) + + @pytest.mark.parametrize("temporal", [ti, ts, tss], ids=["Instant", "Sequence", "SequenceSet"]) + def test_from_as_mfjson_constructor(self, temporal): + assert temporal.__class__.from_mfjson(temporal.as_mfjson()) == temporal + + def test_start_value(self): + assert self.tss.start_value() == self.value + + def test_end_value(self): + assert self.tss.end_value() == self.value + + def test_value_set(self): + assert self.tss.value_set() == {self.value, self.other} + + def test_value_at_timestamp(self): + assert self.ts.value_at_timestamp(datetime(2019, 9, 2, tzinfo=timezone.utc)) == self.other + + +class TestPoseSet(TestPyMEOS): + ps = PoseSet('{"Pose(Point(1 1),0.5)", "Pose(Point(2 2),0.3)"}') + ps_srid = PoseSet('SRID=3812;{"Pose(Point(1 1),0.5)"}') + + def test_str(self): + assert str(self.ps) == '{"Pose(POINT(1 1),0.5)", "Pose(POINT(2 2),0.3)"}' + + def test_as_text(self): + assert self.ps.as_text() == '{"Pose(POINT(1 1),0.5)", "Pose(POINT(2 2),0.3)"}' + assert self.ps.as_wkt() == self.ps.as_text() + + def test_as_ewkt(self): + assert self.ps_srid.as_ewkt() == 'SRID=3812;{"Pose(POINT(1 1),0.5)"}' + assert self.ps_srid.as_text() == '{"Pose(POINT(1 1),0.5)"}' diff --git a/tests/main/trgeometry_test.py b/tests/main/trgeometry_test.py new file mode 100644 index 00000000..291ce0cd --- /dev/null +++ b/tests/main/trgeometry_test.py @@ -0,0 +1,254 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest +from shapely import Point, Polygon + +from pymeos import ( + Pose, + TInterpolation, + TPoseInst, + TPoseSeq, + TRgeometry, + TRgeometryInst, + TRgeometrySeq, + TRgeometrySeqSet, +) +from tests.conftest import TestPyMEOS + + +class TestTRgeometry(TestPyMEOS): + pass + + +class TestTRgeometryConstructors(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tpi = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + tri = TRgeometry.from_geometry_tpose(geometry, tpi) + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + @pytest.mark.parametrize( + "temporal, expected_type", + [ + (tri, TRgeometryInst), + (trs, TRgeometrySeq), + ], + ids=["Instant", "Sequence"], + ) + def test_from_geometry_tpose_constructor(self, temporal, expected_type): + assert isinstance(temporal, expected_type) + + @pytest.mark.parametrize( + "source, expected_type", + [ + ("Polygon((0 0,1 0,1 1,0 1,0 0));Pose(Point(1 1), 0.5)@2019-09-01", TRgeometryInst), + ( + "Polygon((0 0,1 0,1 1,0 1,0 0));[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]", + TRgeometrySeq, + ), + ( + "Polygon((0 0,1 0,1 1,0 1,0 0));{[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]}", + TRgeometrySeqSet, + ), + ], + ids=["Instant", "Sequence", "SequenceSet"], + ) + def test_string_constructor(self, source, expected_type): + temporal = expected_type(source) + assert isinstance(temporal, expected_type) + assert temporal == expected_type(str(temporal)) + + def test_instant_constructor(self): + inst = TRgeometryInst( + geometry=self.geometry, + pose=Pose("Pose(Point(1 1), 0.5)"), + timestamp="2019-09-01", + ) + assert isinstance(inst, TRgeometryInst) + + @pytest.mark.parametrize( + "temporal", + [tri, trs], + ids=["Instant", "Sequence"], + ) + def test_from_as_hexwkb_constructor(self, temporal): + assert temporal == temporal.__class__.from_hexwkb(temporal.as_hexwkb()) + + @pytest.mark.parametrize( + "temporal", + [tri, trs], + ids=["Instant", "Sequence"], + ) + def test_copy_constructor(self, temporal): + other = copy(temporal) + assert temporal == other + assert temporal is not other + + +class TestTRgeometryOutputs(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tpi = TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01") + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + tri = TRgeometry.from_geometry_tpose(geometry, tpi) + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_str(self): + assert str(self.tri) == "POLYGON((0 0,1 0,1 1,0 1,0 0));Pose(POINT(1 1),0.5)@2019-09-01 00:00:00+00" + + def test_repr(self): + assert isinstance(repr(self.tri), str) + + def test_as_wkt(self): + assert self.trs.as_wkt() == ( + "POLYGON((0 0,1 0,1 1,0 1,0 0));[Pose(POINT(1 1),0.5)@2019-09-01 00:00:00+00, " + "Pose(POINT(2 2),0.3)@2019-09-02 00:00:00+00]" + ) + + def test_as_ewkt(self): + assert self.trs.as_ewkt() == self.trs.as_wkt() + + +class TestTRgeometryAccessors(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_interpolation(self): + assert self.trs.interpolation() == TInterpolation.LINEAR + + def test_start_value(self): + assert self.trs.start_value() is not None + + def test_end_value(self): + assert self.trs.end_value() is not None + + def test_values(self): + assert self.trs.values() is not None + + def test_geometry(self): + assert self.trs.geometry() is not None + + def test_value_at_timestamp(self): + value = self.trs.value_at_timestamp(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert value is not None + + def test_srid(self): + assert isinstance(self.trs.srid(), int) + + def test_to_tpose(self): + assert self.trs.to_tpose() is not None + + def test_to_tpoint(self): + assert self.trs.to_tpoint() is not None + + def test_yaw(self): + assert self.trs.yaw() is not None + + +class TestTRgeometryEverAlways(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_never_not_equal(self): + assert isinstance(self.trs.never_not_equal(self.geometry), bool) + + +# As #TestTCbufferSpatialRelationships, with values decided by a polygon around +# the whole motion and a point far from it. +class TestTRgeometrySpatialRelationships(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + around = Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10)]) + far = Point(100, 100) + far_trs = TRgeometry.from_geometry_tpose( + geometry, TPoseSeq("[Pose(Point(100 100), 0)@2019-09-01, Pose(Point(101 101), 0)@2019-09-02]") + ) + + def test_ever_relationships(self): + assert self.trs.is_ever_contained_in(self.around) is True + assert self.trs.is_ever_coveredby(self.around) is True + assert self.trs.is_ever_coveredby(self.far) is False + assert self.trs.is_ever_covers(self.far) is False + assert self.trs.is_ever_disjoint(self.far) is True + assert self.trs.is_ever_disjoint(self.far_trs) is True + assert self.trs.is_ever_within_distance(self.far, 1.0) is False + assert self.trs.is_ever_within_distance(self.far_trs, 1000.0) is True + assert self.trs.ever_intersects(self.around) is True + assert self.trs.ever_intersects(self.far_trs) is False + assert self.trs.ever_touches(self.far) is False + + def test_always_relationships(self): + assert self.trs.is_always_contained_in(self.around) is True + assert self.trs.is_always_coveredby(self.around) is True + assert self.trs.is_always_coveredby(self.far) is False + assert self.trs.is_always_covers(self.far) is False + assert self.trs.is_always_disjoint(self.far) is True + assert self.trs.is_always_disjoint(self.far_trs) is True + assert self.trs.is_always_within_distance(self.far, 1.0) is False + assert self.trs.is_always_within_distance(self.far_trs, 1000.0) is True + assert self.trs.always_intersects(self.around) is True + assert self.trs.always_intersects(self.far_trs) is False + assert self.trs.always_touches(self.far) is False + + +class TestTRgeometryRestrictions(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_at_timestamp(self): + result = self.trs.at(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert result is None or isinstance(result, TRgeometry) + + def test_minus_timestamp(self): + result = self.trs.minus(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert result is None or isinstance(result, TRgeometry) + + +class TestTRgeometryDistance(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_nearest_approach_distance(self): + assert isinstance( + self.trs.nearest_approach_distance(Polygon([(5, 5), (6, 5), (6, 6), (5, 6)])), + float, + ) + + +class TestTRgeometryGapMethods(TestTRgeometry): + geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + tps = TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01, Pose(Point(2 2), 0.3)@2019-09-02]") + trs = TRgeometry.from_geometry_tpose(geometry, tps) + + def test_segments(self): + assert isinstance(self.trs.segments(), list) + + # As #TestTGeomPointTransformations.test_to_instant, over values of one instant. + @pytest.mark.parametrize( + "temporal", + [ + TRgeometry.from_geometry_tpose(geometry, TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01")), + TRgeometry.from_geometry_tpose(geometry, TPoseSeq("[Pose(Point(1 1), 0.5)@2019-09-01]")), + ], + ids=["Instant", "Sequence"], + ) + def test_to_instant(self, temporal): + expected = TRgeometry.from_geometry_tpose(self.geometry, TPoseInst("Pose(Point(1 1), 0.5)@2019-09-01")) + temp = temporal.to_instant() + assert isinstance(temp, TRgeometryInst) + assert temp == expected + + def test_delete(self): + r = self.trs.delete(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert r is None or isinstance(r, TRgeometry) + + def test_before_after(self): + b = self.trs.before(datetime(2019, 9, 2, tzinfo=timezone.utc)) + a = self.trs.after(datetime(2019, 9, 1, tzinfo=timezone.utc)) + assert b is None or isinstance(b, TRgeometry) + assert a is None or isinstance(a, TRgeometry)