From df78ef65c0e37008016881710773bd45afdd4f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 1/7] Bind PyMEOS to the MEOS master surface PyMEOS 1.4.0a1 requires pymeos-cffi 1.4.0a1 or later and calls MEOS master's names and signatures. Text, timestamp, date and interval input and output go through cstring_to_text, text_to_cstring, timestamptz_in, date_in and interval_in; multiplication through the mul_* family; the conversions to an instant, sequence or sequence set through temporal_as_*; the temporal distance of numbers through the tdistance_* family, which takes an int for a TInt and a float for a TFloat; and the box nearest approach distance of a temporal point through nad_tgeo_stbox. The spatial relationships drop their restr and atvalue arguments, and the distance, length, speed and nearest approach functions take spheroid=True, the MobilityDB default. The tiling and split functions pass border_inc=True, the MobilityDB default, so a value on the upper border lands in the tile that starts there and time_split_n answers an (n+1)th tile holding the end value when the end is inclusive. Set and span set accessors read the array from the (array, count) pair the functions return, and the time distances convert the returned interval to a timedelta. --- pymeos/__init__.py | 2 +- pymeos/aggregators/aggregator.py | 4 +- pymeos/boxes/stbox.py | 18 +++---- pymeos/boxes/tbox.py | 8 ++-- pymeos/collections/base/spanset.py | 2 +- pymeos/collections/geo/geoset.py | 2 +- pymeos/collections/number/floatset.py | 2 +- pymeos/collections/number/intset.py | 2 +- pymeos/collections/text/textset.py | 8 ++-- pymeos/collections/time/dateset.py | 6 +-- pymeos/collections/time/datespan.py | 4 +- pymeos/collections/time/tstzset.py | 8 ++-- pymeos/collections/time/tstzspan.py | 8 ++-- pymeos/collections/time/tstzspanset.py | 6 +-- pymeos/main/tfloat.py | 10 ++-- pymeos/main/tint.py | 10 ++-- pymeos/main/tnumber.py | 33 +++++++------ pymeos/main/tpoint.py | 66 +++++++++++++------------- pymeos/main/ttext.py | 4 +- pymeos/temporal/temporal.py | 41 ++++++++-------- pymeos/temporal/tinstant.py | 4 +- pyproject.toml | 2 +- 22 files changed, 126 insertions(+), 124 deletions(-) diff --git a/pymeos/__init__.py b/pymeos/__init__.py index 1924c21b..004ad8ab 100644 --- a/pymeos/__init__.py +++ b/pymeos/__init__.py @@ -30,7 +30,7 @@ from .meos_init import * from .temporal import * -__version__ = "1.3.0-alpha-1" +__version__ = "1.4.0-alpha-1" __all__ = [ # initialization "pymeos_initialize", diff --git a/pymeos/aggregators/aggregator.py b/pymeos/aggregators/aggregator.py index be86e04d..96d0dc63 100644 --- a/pymeos/aggregators/aggregator.py +++ b/pymeos/aggregators/aggregator.py @@ -243,12 +243,12 @@ def _add( interval_converted = ( timedelta_to_interval(interval) if isinstance(interval, timedelta) - else pg_interval_in(interval, -1) + else interval_in(interval, -1) if isinstance(interval, str) else None ) origin_converted = ( - datetime_to_timestamptz(origin) if isinstance(origin, datetime) else pg_timestamptz_in(origin, -1) + datetime_to_timestamptz(origin) if isinstance(origin, datetime) else timestamptz_in(origin, -1) ) return cls._add_function(state, temporal._inner, interval_converted, origin_converted) diff --git a/pymeos/boxes/stbox.py b/pymeos/boxes/stbox.py index 9d24f7bc..e53b1b4b 100644 --- a/pymeos/boxes/stbox.py +++ b/pymeos/boxes/stbox.py @@ -178,7 +178,7 @@ def from_geometry(geom: shp.BaseGeometry, geodetic: bool = False) -> STBox: A new :class:`STBox` instance. MEOS Functions: - pgis_geometry_in, geo_to_stbox + geom_in, geog_in, geo_to_stbox """ gs = geo_to_gserialized(geom, geodetic) return STBox(_inner=geo_to_stbox(gs)) @@ -1157,15 +1157,15 @@ def nearest_approach_distance(self, other: shp.BaseGeometry | STBox | TPoint) -> ``self`` and ``other``. MEOS Functions: - nad_stbox_geo, nad_stbox_stbox + nad_stbox_geo, nad_stbox_stbox, nad_tgeo_stbox """ if isinstance(other, shp.BaseGeometry): gs = geo_to_gserialized(other, self.geodetic()) - return nad_stbox_geo(self._inner, gs) + return nad_stbox_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_stbox_stbox(self._inner, other._inner) + return nad_stbox_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tpoint_stbox(other._inner, self._inner) + return nad_tgeo_stbox(other._inner, self._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -1285,7 +1285,7 @@ def tile( dt = ( timedelta_to_interval(duration) if isinstance(duration, timedelta) - else pg_interval_in(duration, -1) + else interval_in(duration, -1) if isinstance(duration, str) else None ) @@ -1293,9 +1293,9 @@ def tile( datetime_to_timestamptz(start) if isinstance(start, datetime) else ( - pg_timestamptz_in(start, -1) + timestamptz_in(start, -1) if isinstance(start, str) - else pg_timestamptz_in("2000-01-03", -1) + else timestamptz_in("2000-01-03", -1) if self.has_t() else 0 ) @@ -1303,7 +1303,7 @@ def tile( gs = ( geo_to_gserialized(origin, self.geodetic()) if origin is not None - else (pgis_geography_in("Point(0 0 0)", -1) if self.geodetic() else pgis_geometry_in("Point(0 0 0)", -1)) + else (geog_in("Point(0 0 0)", -1) if self.geodetic() else geom_in("Point(0 0 0)", -1)) ) tiles, count = stbox_space_time_tiles(self._inner, sz, sz, sz, dt, gs, st, border_inc) return [STBox(_inner=tiles + i) for i in range(count)] diff --git a/pymeos/boxes/tbox.py b/pymeos/boxes/tbox.py index 336ecca1..bc4b2967 100644 --- a/pymeos/boxes/tbox.py +++ b/pymeos/boxes/tbox.py @@ -1086,16 +1086,16 @@ def tile( MEOS Functions: tintbox_tile_list, tfloabox_tile_list """ - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) st = ( datetime_to_timestamptz(start) if isinstance(start, datetime) - else (pg_timestamptz_in(start, -1) if isinstance(start, str) else pg_timestamptz_in("2000-01-03", -1)) + else (timestamptz_in(start, -1) if isinstance(start, str) else timestamptz_in("2000-01-03", -1)) ) if self._is_float(): - tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st) + tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st, border_inc=True) else: - tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st) + tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st, border_inc=True) return [TBox(_inner=tiles + c) for c in range(count)] # ------------------------- Comparisons ----------------------------------- diff --git a/pymeos/collections/base/spanset.py b/pymeos/collections/base/spanset.py index 232fb5f3..9b63c0d0 100644 --- a/pymeos/collections/base/spanset.py +++ b/pymeos/collections/base/spanset.py @@ -211,7 +211,7 @@ def spans(self) -> list[Span]: MEOS Functions: spanset_spans """ - return spanset_spanarr(self._inner) + return spanset_spanarr(self._inner)[0] def __hash__(self) -> int: """ diff --git a/pymeos/collections/geo/geoset.py b/pymeos/collections/geo/geoset.py index 5b794d0a..fd3efde4 100644 --- a/pymeos/collections/geo/geoset.py +++ b/pymeos/collections/geo/geoset.py @@ -161,7 +161,7 @@ def elements(self) -> list[shp.Geometry]: MEOS Functions: geoset_values """ - elems = geoset_values(self._inner) + elems = geoset_values(self._inner)[0] return [gserialized_to_shapely_geometry(elems[i]) for i in range(self.num_elements())] def srid(self) -> int: diff --git a/pymeos/collections/number/floatset.py b/pymeos/collections/number/floatset.py index f1332d30..23a77774 100644 --- a/pymeos/collections/number/floatset.py +++ b/pymeos/collections/number/floatset.py @@ -141,7 +141,7 @@ def elements(self) -> list[float]: MEOS Functions: floattset_values """ - elems = floatset_values(self._inner) + elems = floatset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/number/intset.py b/pymeos/collections/number/intset.py index a512c7d5..299ad444 100644 --- a/pymeos/collections/number/intset.py +++ b/pymeos/collections/number/intset.py @@ -140,7 +140,7 @@ def elements(self) -> list[int]: MEOS Functions: inttset_values """ - elems = intset_values(self._inner) + elems = intset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/text/textset.py b/pymeos/collections/text/textset.py index 0a1caddf..28eb0e8b 100644 --- a/pymeos/collections/text/textset.py +++ b/pymeos/collections/text/textset.py @@ -95,7 +95,7 @@ def element_n(self, n: int): textset_value_n """ super().element_n(n) - return text2cstring(textset_value_n(self._inner, n + 1)[0]) + return text_to_cstring(textset_value_n(self._inner, n + 1)[0]) def elements(self): """ @@ -107,8 +107,8 @@ def elements(self): MEOS Functions: textset_values """ - elems = textset_values(self._inner) - return [text2cstring(elems[i]) for i in range(self.num_elements())] + elems = textset_values(self._inner)[0] + return [text_to_cstring(elems[i]) for i in range(self.num_elements())] # ------------------------- Topological Operations -------------------------------- @@ -224,7 +224,7 @@ def subtract_from(self, other: str) -> str | None: :meth:`minus` """ result = minus_text_set(other, self._inner) - return text2cstring(result[0]) if result is not None else None + return text_to_cstring(result[0]) if result is not None else None def union(self, other: TextSet | str) -> TextSet: """ diff --git a/pymeos/collections/time/dateset.py b/pymeos/collections/time/dateset.py index 0675eeb2..29e64672 100644 --- a/pymeos/collections/time/dateset.py +++ b/pymeos/collections/time/dateset.py @@ -8,6 +8,7 @@ before_set_date, contains_set_date, date_adt_to_date, + date_in, date_to_date_adt, dateset_end_value, dateset_in, @@ -26,7 +27,6 @@ minus_set_set, overafter_set_date, overbefore_set_date, - pg_date_in, union_set_date, union_set_set, ) @@ -63,7 +63,7 @@ class DateSet(Set[date], TimeCollection[date]): _mobilitydb_name = "dateset" _parse_function = dateset_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = dateset_make # ------------------------- Constructors ---------------------------------- @@ -162,7 +162,7 @@ def elements(self) -> list[date]: MEOS Functions: dateset_values """ - tss = dateset_values(self._inner) + tss = dateset_values(self._inner)[0] return [date_adt_to_date(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- diff --git a/pymeos/collections/time/datespan.py b/pymeos/collections/time/datespan.py index 934e2529..45f24c3b 100644 --- a/pymeos/collections/time/datespan.py +++ b/pymeos/collections/time/datespan.py @@ -9,6 +9,7 @@ before_span_date, contains_span_date, date_adt_to_date, + date_in, date_to_date_adt, datespan_duration, datespan_in, @@ -30,7 +31,6 @@ minus_span_spanset, overafter_span_date, overbefore_span_date, - pg_date_in, union_span_date, ) @@ -69,7 +69,7 @@ class DateSpan(Span[date], TimeCollection[date]): _mobilitydb_name = "datespan" _parse_function = datespan_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = datespan_make # ------------------------- Constructors ---------------------------------- diff --git a/pymeos/collections/time/tstzset.py b/pymeos/collections/time/tstzset.py index 545b885e..08d0fa15 100644 --- a/pymeos/collections/time/tstzset.py +++ b/pymeos/collections/time/tstzset.py @@ -39,7 +39,7 @@ class TsTzSet(Set[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzset" _parse_function = tstzset_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzset_make # ------------------------- Constructors ---------------------------------- @@ -116,7 +116,7 @@ def elements(self) -> list[datetime]: MEOS Functions: set_timestamptzs """ - tss = tstzset_values(self._inner) + tss = tstzset_values(self._inner)[0] return [timestamptz_to_datetime(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- @@ -512,9 +512,9 @@ def distance(self, other: Time | Temporal | Box) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): - return timedelta(seconds=distance_tstzset_tstzset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzset_tstzset(self._inner, other._inner)) elif isinstance(other, (TsTzSpan, TsTzSpanSet)): return self.to_spanset().distance(other) elif isinstance(other, (Temporal, get_args(Box))): diff --git a/pymeos/collections/time/tstzspan.py b/pymeos/collections/time/tstzspan.py index 966e5c98..62c8b5d9 100644 --- a/pymeos/collections/time/tstzspan.py +++ b/pymeos/collections/time/tstzspan.py @@ -41,7 +41,7 @@ class TsTzSpan(Span[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzspan" _parse_function = tstzspan_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzspan_make # ------------------------- Constructors ---------------------------------- @@ -505,13 +505,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspan_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspan_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspan(other._inner, self._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(other._inner, self._inner)) elif isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/collections/time/tstzspanset.py b/pymeos/collections/time/tstzspanset.py index 9e5f10fb..0b824080 100644 --- a/pymeos/collections/time/tstzspanset.py +++ b/pymeos/collections/time/tstzspanset.py @@ -631,13 +631,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspan import TsTzSpan if isinstance(other, datetime): - return timedelta(seconds=distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspanset_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspanset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspanset(self._inner, other._inner)) if isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/main/tfloat.py b/pymeos/main/tfloat.py index 242b3513..acf659da 100644 --- a/pymeos/main/tfloat.py +++ b/pymeos/main/tfloat.py @@ -896,7 +896,7 @@ def value_split(self, size: float, start: float | None = 0) -> list[Temporal]: MEOS Functions: tfloat_value_split """ - fragments, _, count = tfloat_value_split(self._inner, size, start) + fragments, _, count = tfloat_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -924,15 +924,15 @@ def value_time_split( tfloat_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tint.py b/pymeos/main/tint.py index 658e457e..7d218c3b 100644 --- a/pymeos/main/tint.py +++ b/pymeos/main/tint.py @@ -833,7 +833,7 @@ def value_split(self, size: int, start: int | None = 0) -> list[TInt]: MEOS Functions: tint_value_split """ - fragments, values, count = tint_value_split(self._inner, size, start) + fragments, values, count = tint_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -861,15 +861,15 @@ def value_time_split( tint_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(tiles[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tnumber.py b/pymeos/main/tnumber.py index c8e33e42..854db1c9 100644 --- a/pymeos/main/tnumber.py +++ b/pymeos/main/tnumber.py @@ -401,17 +401,17 @@ def mul(self, other: int | float | TNumber) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_tint_int(self._inner, other) + result = mul_tint_int(self._inner, other) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_tfloat_float(self._inner, float(other)) + result = mul_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = mult_tnumber_tnumber(self._inner, other._inner) + result = mul_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -428,15 +428,15 @@ def rmul(self, other: int | float) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_int_tint(other, self._inner) + result = mul_int_tint(other, self._inner) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_float_tfloat(float(other), self._inner) + result = mul_float_tfloat(float(other), self._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -566,7 +566,7 @@ def __mul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ return self.mul(other) @@ -582,7 +582,7 @@ def __rmul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ return self.rmul(other) @@ -660,14 +660,17 @@ def distance(self, other: int | float | TNumber) -> TFloat: A :class:`TFloat` with the distance between `self` and `other`. MEOS Functions: - distance_tfloat_float, distance_tnumber_tnumber + tdistance_tint_int, tdistance_tfloat_float, tdistance_tnumber_tnumber """ - if isinstance(other, int): - result = distance_tfloat_float(self._inner, float(other)) - elif isinstance(other, float): - result = distance_tfloat_float(self._inner, other) + from .tfloat import TFloat + from .tint import TInt + + if isinstance(self, TInt) and isinstance(other, int): + result = tdistance_tint_int(self._inner, other) + elif isinstance(self, TFloat) and isinstance(other, (int, float)): + result = tdistance_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = distance_tnumber_tnumber(self._inner, other._inner) + result = tdistance_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/tpoint.py b/pymeos/main/tpoint.py index 9ae1441b..bfa01329 100644 --- a/pymeos/main/tpoint.py +++ b/pymeos/main/tpoint.py @@ -235,7 +235,7 @@ def length(self) -> float: MEOS Functions: tpoint_length """ - return tpoint_length(self._inner) + return tpoint_length(self._inner, spheroid=True) def cumulative_length(self) -> TFloat: """ @@ -247,7 +247,7 @@ def cumulative_length(self) -> TFloat: MEOS Functions: tpoint_cumulative_length """ - result = tpoint_cumulative_length(self._inner) + result = tpoint_cumulative_length(self._inner, spheroid=True) return Temporal._factory(result) def speed(self) -> TFloat: @@ -260,7 +260,7 @@ def speed(self) -> TFloat: MEOS Functions: tpoint_speed """ - result = tpoint_speed(self._inner) + result = tpoint_speed(self._inner, spheroid=True) return Temporal._factory(result) def x(self) -> TF: @@ -787,11 +787,11 @@ def is_ever_within_distance(self, other: shpb.BaseGeometry | TPoint | STBox, dis if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = edwithin_tgeo_geo(self._inner, gs, distance) + result = edwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance) + result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TPoint): - result = edwithin_tgeo_tgeo(self._inner, other._inner, distance) + result = edwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return result == 1 @@ -866,10 +866,10 @@ def is_spatially_contained_in(self, container: shpb.BaseGeometry | STBox) -> TBo if isinstance(container, shpb.BaseGeometry): gs = geo_to_gserialized(container, isinstance(self, TGeogPoint)) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) elif isinstance(container, STBox): gs = stbox_to_geo(container._inner) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) else: raise TypeError(f"Operation not supported with type {container.__class__}") return Temporal._factory(result) @@ -892,11 +892,11 @@ def distance(self, other: shpb.BaseGeometry | TPoint | STBox) -> TFloat: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdistance_tgeo_geo(self._inner, gs) + result = tdistance_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner)) + result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner), spheroid=True) elif isinstance(other, TPoint): - result = tdistance_tgeo_tgeo(self._inner, other._inner) + result = tdistance_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -918,11 +918,11 @@ def nearest_approach_distance(self, other: shpb.BaseGeometry | STBox | TPoint) - if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - return nad_tgeo_geo(self._inner, gs) + return nad_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_tgeo_stbox(self._inner, other._inner) + return nad_tgeo_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tgeo_tgeo(self._inner, other._inner) + return nad_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -941,9 +941,9 @@ def nearest_approach_instant(self, other: shpb.BaseGeometry | TPoint) -> TI: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = nai_tgeo_geo(self._inner, gs) + result = nai_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = nai_tgeo_tgeo(self._inner, other._inner) + result = nai_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -964,9 +964,9 @@ def shortest_line(self, other: shpb.BaseGeometry | TPoint) -> shpb.BaseGeometry: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = shortestline_tgeo_geo(self._inner, gs) + result = shortestline_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = shortestline_tgeo_tgeo(self._inner, other._inner) + result = shortestline_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return gserialized_to_shapely_geometry(result, 10) @@ -1086,19 +1086,19 @@ def space_time_split( """ ysz = ysize if ysize is not None else xsize zsz = zsize if zsize is not None else xsize - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) gs = ( geo_to_gserialized(origin, isinstance(self, TGeogPoint)) if origin is not None else (geog_in("Point(0 0 0)", -1) if isinstance(self, TGeogPoint) else geom_in("Point(0 0 0)", -1)) ) if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) fragments, points, times, count = tgeo_space_time_split( self._inner, xsize, ysz, zsz, dt, gs, st, bitmatrix, include_border @@ -1365,11 +1365,11 @@ def disjoint(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdisjoint_tgeo_geo(self._inner, gs, False, False) + result = tdisjoint_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tdisjoint_tgeo_tgeo(self._inner, other._inner, False, False) + result = tdisjoint_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1391,11 +1391,11 @@ def intersects(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tintersects_tgeo_geo(self._inner, gs, False, False) + result = tintersects_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tintersects_tgeo_tgeo(self._inner, other._inner, False, False) + result = tintersects_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1417,11 +1417,11 @@ def touches(self, other: shpb.BaseGeometry | STBox | TGeomPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = ttouches_tgeo_geo(self._inner, gs, False, False) + result = ttouches_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TGeomPoint): - result = ttouches_tgeo_tgeo(self._inner, other._inner, False, False) + result = ttouches_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1444,11 +1444,11 @@ def within_distance(self, other: shpb.BaseGeometry | TGeomPoint | STBox, distanc if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdwithin_tgeo_geo(self._inner, gs, distance, False, False) + result = tdwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, False, False) + result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TGeomPoint): - result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, False, False) + result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/ttext.py b/pymeos/main/ttext.py index e956e732..2dfd521b 100644 --- a/pymeos/main/ttext.py +++ b/pymeos/main/ttext.py @@ -143,7 +143,7 @@ def value_set(self) -> set[str]: ttext_values """ values, count = ttext_values(self._inner) - return {text2cstring(values[i]) for i in range(count)} + return {text_to_cstring(values[i]) for i in range(count)} def min_value(self) -> str: """ @@ -233,7 +233,7 @@ def value_at_timestamp(self, timestamp: datetime) -> str: ttext_value_at_timestamp """ result = ttext_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True) - return text2cstring(result[0]) + return text_to_cstring(result[0]) # ------------------------- Ever and Always Comparisons ------------------- def always_less(self, value: str | TText) -> bool: diff --git a/pymeos/temporal/temporal.py b/pymeos/temporal/temporal.py index b168ae66..8acd3504 100644 --- a/pymeos/temporal/temporal.py +++ b/pymeos/temporal/temporal.py @@ -592,15 +592,15 @@ def temporal_sample( temporal_tsample """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) if interpolation is None: intrp = self.interpolation() else: @@ -627,15 +627,15 @@ def temporal_precision( temporal_tprecision """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) result = temporal_tprecision(self._inner, dt, st) return Temporal._factory(result) @@ -644,9 +644,9 @@ def to_instant(self) -> TI: Returns `self` as a :class:`TInstant`. MEOS Functions: - temporal_to_tinstant + temporal_as_tinstant """ - inst = temporal_to_tinstant(self._inner) + inst = temporal_as_tinstant(self._inner) return Temporal._factory(inst) def to_sequence(self, interpolation: TInterpolation) -> TS: @@ -656,7 +656,7 @@ def to_sequence(self, interpolation: TInterpolation) -> TS: MEOS Functions: temporal_to_sequence """ - seq = temporal_to_tsequence(self._inner, interpolation) + seq = temporal_as_tsequence(self._inner, interpolation) return Temporal._factory(seq) def to_sequenceset(self, interpolation: TInterpolation) -> TSS: @@ -664,9 +664,9 @@ def to_sequenceset(self, interpolation: TInterpolation) -> TSS: Returns `self` as a :class:`TSequenceSet`. MEOS Functions: - temporal_to_tsequenceset + temporal_as_tsequenceset """ - ss = temporal_to_tsequenceset(self._inner, interpolation) + ss = temporal_as_tsequenceset(self._inner, interpolation) return Temporal._factory(ss) def to_dataframe(self) -> pd.DataFrame: @@ -1160,7 +1160,7 @@ def frechet_distance(self, other: Temporal) -> float: MEOS Functions: temporal_frechet_distance """ - return temporal_frechet_distance(self._inner, other._inner) + return temporal_frechet_distance(self._inner, other._inner, spheroid=True) def dyntimewarp_distance(self, other: Temporal) -> float: """ @@ -1175,7 +1175,7 @@ def dyntimewarp_distance(self, other: Temporal) -> float: MEOS Functions: temporal_dyntimewarp_distance """ - return temporal_dyntimewarp_distance(self._inner, other._inner) + return temporal_dyntimewarp_distance(self._inner, other._inner, spheroid=True) def hausdorff_distance(self, other: Temporal) -> float: """ @@ -1190,7 +1190,7 @@ def hausdorff_distance(self, other: Temporal) -> float: MEOS Functions: temporal_hausdorff_distance """ - return temporal_hausdorff_distance(self._inner, other._inner) + return temporal_hausdorff_distance(self._inner, other._inner, spheroid=True) # ------------------------- Split Operations ------------------------------ def time_split( @@ -1217,11 +1217,11 @@ def time_split( temporal_time_split """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: - st = datetime_to_timestamptz(start) if isinstance(start, datetime) else pg_timestamptz_in(start, -1) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, times, count = temporal_time_split(self._inner, dt, st) + st = datetime_to_timestamptz(start) if isinstance(start, datetime) else timestamptz_in(start, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] @@ -1230,7 +1230,8 @@ def time_split_n(self, n: int) -> list[TG]: """ Returns a list of temporal objects of the same subtype as `self` with the same values as `self` but split in n temporal tiles of equal - duration. + duration. When `self` includes its end timestamp, that timestamp + starts an (n+1)th tile, which holds only the end value. Args: n: An :class:`int` with the number of temporal tiles. @@ -1245,7 +1246,7 @@ def time_split_n(self, n: int) -> list[TG]: return [self] st = temporal_start_timestamptz(self._inner) dt = timedelta_to_interval((self.end_timestamp() - self.start_timestamp()) / n) - fragments, times, count = temporal_time_split(self._inner, dt, st) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] diff --git a/pymeos/temporal/tinstant.py b/pymeos/temporal/tinstant.py index d79ffbce..5bd2f2d6 100644 --- a/pymeos/temporal/tinstant.py +++ b/pymeos/temporal/tinstant.py @@ -44,9 +44,7 @@ def __init__( self._inner = as_tinstant(self.__class__._parse_function(string)) else: ts = ( - datetime_to_timestamptz(timestamp) - if isinstance(timestamp, datetime) - else pg_timestamptz_in(timestamp, -1) + datetime_to_timestamptz(timestamp) if isinstance(timestamp, datetime) else timestamptz_in(timestamp, -1) ) self._inner = self.__class__._make_function(self.__class__._cast_function(value), ts) diff --git a/pyproject.toml b/pyproject.toml index 066b9e79..c7eb7fd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ license = { file = 'LICENSE' } requires-python = '>=3.10' dependencies = [ - 'pymeos-cffi >=1.3.0, <2', + 'pymeos-cffi >=1.4.0a1, <2', 'python-dateutil', 'shapely>=2.0.0', ] From af8ab8a5c3754ef696fa76762af37ad753c488c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 2/7] Build the all-families MEOS and its catalog in the test workflow The PyMEOS-CFFI cdef covers every optional MEOS family and is projected from the MEOS-API catalog, so the test workflow derives the catalog once with the shared provision-meos action, from the MobilityDB branch the package version maps to, and each test job builds libmeos with every family: through provision-meos on Linux and with -DALL=ON on macOS. PyMEOS-CFFI's header is projected from that catalog and the installed library before it is built. --- .github/workflows/test.yml | 93 ++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edd7300c..db79c921 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,51 @@ on: - ".github/ISSUE_TEMPLATE/**" jobs: + # Derive the MEOS catalog once from the MobilityDB branch the package version + # maps to, through the shared provision-meos action. PyMEOS-CFFI projects its + # cdef header from this catalog, so every test job builds the same surface. + # The catalog parses the headers a libmeos build installs, as PyMEOS-CFFI's + # pr_build does, so it declares no internal header the extension cannot include. + catalog: + name: Derive the MEOS catalog + runs-on: ubuntu-latest + outputs: + meos_branch: ${{ steps.meos_branch.outputs.meos_branch }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Derive MEOS branch from package version + id: meos_branch + shell: bash + run: | + pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) + major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') + candidate="stable-${major_minor}" + if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then + meos_branch=$candidate + else + meos_branch="master" + fi + echo "Package version $pymeos_version => MEOS branch $meos_branch" + echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" + + - name: Provision the MEOS catalog + id: provision + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master + with: + mobilitydb-ref: ${{ steps.meos_branch.outputs.meos_branch }} + build-libmeos: "true" + + - name: Upload the catalog + uses: actions/upload-artifact@v4 + with: + name: meos-idl + path: ${{ steps.provision.outputs.catalog-path }} + test: name: Test PyMEOS - Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: catalog runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -34,18 +77,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get dependencies from apt (cache) - uses: awalsh128/cache-apt-pkgs-action@v1.4.3 + # The CFFI cdef covers every optional MEOS family, so libmeos is built + # with all of them: on Linux by the provision-meos action, from the same + # MobilityDB branch as the catalog. + - name: Build and install the all-families MEOS (Linux) if: runner.os == 'Linux' + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master with: - packages: build-essential cmake postgresql-server-dev-16 libproj-dev libjson-c-dev libgsl-dev libgeos-dev - version: 1.1 + mobilitydb-ref: ${{ needs.catalog.outputs.meos_branch }} + build-libmeos: "true" - name: Get dependencies from homebrew (cache) uses: tecolicom/actions-use-homebrew-tools@v1 if: runner.os == 'macOS' with: - tools: cmake libpq proj json-c gsl geos + tools: cmake libpq proj json-c gsl geos h3 gdal - name: Update brew if: matrix.os == 'macos-15-intel' @@ -56,31 +102,14 @@ jobs: brew reinstall python@3.11 || brew link --overwrite python@3.11 brew update - - name: Derive MEOS branch from package version - id: meos_branch - shell: bash - run: | - pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) - major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') - candidate="stable-${major_minor}" - if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then - meos_branch=$candidate - else - meos_branch="master" - fi - echo "Package version $pymeos_version => MEOS branch $meos_branch" - echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" - - - name: Fetch MEOS sources - run: | - git clone --branch ${{ steps.meos_branch.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB - - - name: Install MEOS + - name: Build and install the all-families MEOS (macOS) + if: runner.os == 'macOS' run: | + git clone --branch ${{ needs.catalog.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB mkdir MobilityDB/build cd MobilityDB/build - cmake .. -DMEOS=on -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} - make -j + cmake .. -DMEOS=ON -DALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} + make -j"$(sysctl -n hw.ncpu)" sudo make install - name: Set up Python ${{ matrix.python-version }} @@ -96,6 +125,12 @@ jobs: git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI \ || git clone --branch ${{ github.base_ref || 'master' }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI + - name: Download the catalog + uses: actions/download-artifact@v4 + with: + name: meos-idl + path: PyMEOS-CFFI/builder + - name: Install python dependencies run: | python -m pip install --upgrade pip @@ -103,9 +138,11 @@ jobs: pip install -r dev-requirements.txt - name: Install pymeos_cffi + shell: bash run: | cd PyMEOS-CFFI - python ./builder/build_header.py + python ./builder/build_header.py builder/meos-idl.json \ + ${{ matrix.ld_prefix }}/lib/${{ runner.os == 'macOS' && 'libmeos.dylib' || 'libmeos.so' }} builder/meos.h python ./builder/build_pymeos_functions.py pip install . From 4c66e771ac8c32bf56374c8d36a78d8c8af0c823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 3/7] Align the test expectations with MEOS master MF-JSON writes the full ISO 8601 offset (+00:00) and states the CRS of a geography; the temporal hash values are those temporal_hash answers; the split tests expect the tile that holds a value on the upper border; and the min and max restrictions, which MobilityDB defines for tint, tbigint, tfloat and ttext, raise for tbool and the temporal points. The adjacent instant of the tint topological test takes the value 2 at the same timestamp: two instants at different timestamps are apart in time, so their boxes are not adjacent. --- tests/main/tbool_test.py | 102 ++++++-------------------- tests/main/tfloat_test.py | 68 +++++++++--------- tests/main/tgeogpoint_test.py | 84 ++++++++++++++-------- tests/main/tgeompoint_test.py | 132 +++++++++------------------------- tests/main/tint_test.py | 50 ++++++------- tests/main/ttext_test.py | 36 +++++----- 6 files changed, 194 insertions(+), 278 deletions(-) diff --git a/tests/main/tbool_test.py b/tests/main/tbool_test.py index 92400c62..46e95ac4 100644 --- a/tests/main/tbool_test.py +++ b/tests/main/tbool_test.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone import pytest +from pymeos_cffi import MeosInvalidArgTypeError from pymeos import ( TBool, @@ -345,8 +346,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -354,7 +355,7 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -364,8 +365,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -374,8 +375,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -387,8 +388,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -397,8 +398,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -410,8 +411,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -422,8 +423,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -434,8 +435,8 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -822,7 +823,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tbi, 440045287), (tbds, 2385901957), (tbs, 2385901957), (tbss, 1543175996)], + [(tbi, 440045318), (tbds, 3614979951), (tbs, 3614979951), (tbss, 1190925880)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -1490,35 +1491,6 @@ def test_when_false(self, temporal, expected): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -1589,35 +1561,6 @@ def test_at_min(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - assert temporal.minus_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - assert temporal.minus_min() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -1681,9 +1624,10 @@ def test_at_minus(self, temporal, restrictor): [tbi, tbds, tbs, tbss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TBool.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TBool.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTBoolComparisons(TestTBool): diff --git a/tests/main/tfloat_test.py b/tests/main/tfloat_test.py index b3e97c59..41c1089a 100644 --- a/tests/main/tfloat_test.py +++ b/tests/main/tfloat_test.py @@ -401,8 +401,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -410,7 +410,7 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -424,8 +424,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -434,8 +434,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -451,8 +451,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -461,8 +461,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -478,8 +478,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -490,8 +490,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -502,8 +502,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -958,12 +958,12 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tfi, 1307112078), - (tfds, 1935376725), - (tfs, 1935376725), - (tfss, 4247071962), - (tfs, 1935376725), - (tfss, 4247071962), + (tfi, 1307112109), + (tfds, 3164454719), + (tfs, 3164454719), + (tfss, 1518183798), + (tfs, 3164454719), + (tfss, 1518183798), ], ids=[ "Instant", @@ -2836,8 +2836,9 @@ def test_value_split(self, temporal, expected): ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2854,15 +2855,17 @@ def test_time_split(self, temporal, expected): ( tfs, [ - TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00:00+00)"), - TFloatSeq("[1.5@2019-09-01 12:00:00+00, 2@2019-09-02]"), + TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00)"), + TFloatSeq("[1.5@2019-09-01 12:00, 2@2019-09-02)"), + TFloatSeq("[2@2019-09-02]"), ], ), ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2886,9 +2889,10 @@ def test_time_split_n(self, temporal, expected): ( tfss, [ - TFloatSeq("{[1@2019-09-01, 2@2019-09-02)}"), - TFloatSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TFloatSeq("{[2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02)}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), + TFloatSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/tgeogpoint_test.py b/tests/main/tgeogpoint_test.py index 4e415c37..4f3abeb2 100644 --- a/tests/main/tgeogpoint_test.py +++ b/tests/main/tgeogpoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest import shapely.geometry -from pymeos_cffi import MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import LineString, Point, set_srid from pymeos import ( @@ -436,6 +436,12 @@ def test_as_hexwkb(self, temporal, expected): tpi, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -447,8 +453,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -459,7 +465,7 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -468,6 +474,12 @@ def test_as_hexwkb(self, temporal, expected): tpds, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -479,8 +491,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -495,8 +507,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -507,6 +519,12 @@ def test_as_hexwkb(self, temporal, expected): tps, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -518,8 +536,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -534,8 +552,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -546,6 +564,12 @@ def test_as_hexwkb(self, temporal, expected): tpss, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -557,8 +581,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -575,8 +599,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -593,8 +617,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -984,7 +1008,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tpi, 1181779687), (tpds, 1545137628), (tps, 1545137628), (tpss, 1008965061)], + [(tpi, 1181779718), (tpds, 2774215622), (tps, 2774215622), (tpss, 429936847)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2235,9 +2259,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeogPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeogPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeogPointEverSpatialOperations(TestTGeogPoint): @@ -2470,8 +2495,9 @@ class TestTGeogPointSplitOperations(TestTGeogPoint): ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -2494,15 +2520,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeogPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeogPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeogPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeogPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeogPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tgeompoint_test.py b/tests/main/tgeompoint_test.py index d08c6a03..fa647db8 100644 --- a/tests/main/tgeompoint_test.py +++ b/tests/main/tgeompoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from pymeos_cffi import MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon from pymeos import ( @@ -466,8 +466,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -478,7 +478,7 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -498,8 +498,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -514,8 +514,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -537,8 +537,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -553,8 +553,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -576,8 +576,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -594,8 +594,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -612,8 +612,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -1010,10 +1010,10 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tpi, 382694564), - (tpds, 1664033448), - (tps, 1664033448), - (tpss, 2878566103), + (tpi, 382694595), + (tpds, 2893111442), + (tps, 2893111442), + (tpss, 1203500793), ], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) @@ -2130,35 +2130,6 @@ class TestTGeomPointRestrictors(TestTGeomPoint): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - ( - tpss, - TGeomPointSeqSet("{[Point(1 1)@2019-09-01],[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -2259,43 +2230,6 @@ def test_at_max(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - if expected is None: - assert temporal.minus_min() is None - else: - assert temporal.minus_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02)}")), - ( - tpss, - TGeomPointSeqSet( - "{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02),[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}" - ), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - if expected is None: - assert temporal.minus_max() is None - else: - assert temporal.minus_max() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -2359,9 +2293,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeomPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeomPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeomPointTopologicalFunctions(TestTGeomPoint): @@ -3140,8 +3075,9 @@ class TestTGeomPointSplitOperations(TestTGeomPoint): ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -3164,15 +3100,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeomPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeomPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeomPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeomPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeomPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tint_test.py b/tests/main/tint_test.py index 42431b0c..6380fce6 100644 --- a/tests/main/tint_test.py +++ b/tests/main/tint_test.py @@ -355,8 +355,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -364,7 +364,7 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -378,8 +378,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -388,8 +388,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -405,8 +405,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -415,8 +415,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -432,8 +432,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -444,8 +444,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -456,8 +456,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -882,7 +882,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tii, 440045287), (tids, 3589664982), (tis, 3589664982), (tiss, 205124107)], + [(tii, 440045318), (tids, 523775680), (tis, 523775680), (tiss, 2660990281)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2196,7 +2196,7 @@ class TestTIntTopologicalFunctions(TestTInt): "temporal, argument, expected", [ (tii, TIntInst("1@2019-09-02"), False), - (tii, TIntInst("2@2019-09-02"), True), + (tii, TIntInst("2@2019-09-01"), True), (tids, TIntInst("1@2019-09-03"), False), (tids, TIntInst("3@2019-09-02"), True), (tis, TIntInst("1@2019-09-03"), False), @@ -2573,8 +2573,9 @@ def test_value_split(self, temporal, expected): ( tiss, [ - TIntSeq("[1@2019-09-01,2@2019-09-02]"), - TIntSeq("[1@2019-09-03, 1@2019-09-05]"), + TIntSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2592,9 +2593,10 @@ def test_time_split(self, temporal, expected): ( tiss, [ - TIntSeq("{[1@2019-09-01, 1@2019-09-02)}"), - TIntSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TIntSeq("{[2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-01, 1@2019-09-02)}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), + TIntSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/ttext_test.py b/tests/main/ttext_test.py index c575a447..0d109bf0 100644 --- a/tests/main/ttext_test.py +++ b/tests/main/ttext_test.py @@ -358,8 +358,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -367,7 +367,7 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -377,8 +377,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -387,8 +387,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -400,8 +400,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -410,8 +410,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -423,8 +423,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -435,8 +435,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -447,8 +447,8 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -838,7 +838,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tti, 1893808825), (ttds, 1223816819), (tts, 1223816819), (ttss, 2199213310)], + [(tti, 1893808856), (ttds, 2452894813), (tts, 2452894813), (ttss, 3439543446)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): From 579e8eff9b5a96398af48bc33d790422a435d375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Tue, 20 Oct 2026 13:03:24 +0200 Subject: [PATCH 4/7] Test the space tiles and the value-time tiles STBox.tile of a trip's box answers the 5 km space tiles of the trip, with their space dimension, and TGeomPoint.tile answers the trip's pieces in them. TBox.tile answers the value-time tiles in value order within each day, covering the box's last day and no value band beyond its upper value. --- tests/boxes/stbox_test.py | 21 +++++++++++++++++++++ tests/boxes/tbox_test.py | 12 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/boxes/stbox_test.py b/tests/boxes/stbox_test.py index bcc676d9..77588045 100644 --- a/tests/boxes/stbox_test.py +++ b/tests/boxes/stbox_test.py @@ -1261,3 +1261,24 @@ def test_gt(self): def test_ge(self): _ = self.stbxt >= self.other + + +class TestSTBoxTiling(TestSTBox): + # Mirrors #test_time_split of the temporal split tests, on the space tiles + trip = TGeomPointSeq( + "SRID=3857;[Point(481000 6586000)@2020-06-01 08:00:00+00, Point(487000 6588000)@2020-06-01 08:30:00+00]" + ) + + def test_tile(self): + tiles = STBox.from_tpoint(self.trip).tile(5e3) + assert [str(tile) for tile in tiles] == [ + "SRID=3857;STBOX X((480000,6585000),(485000,6590000))", + "SRID=3857;STBOX X((485000,6585000),(490000,6590000))", + ] + + def test_tpoint_tile(self): + pieces = self.trip.tile(5e3, remove_empty=True) + assert [str(piece) for piece in pieces] == [ + "{[POINT(481000 6586000)@2020-06-01 08:00:00+00, POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00]}", + "{[POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00, POINT(487000 6588000)@2020-06-01 08:30:00+00]}", + ] diff --git a/tests/boxes/tbox_test.py b/tests/boxes/tbox_test.py index c25a86e2..1ec4edc9 100644 --- a/tests/boxes/tbox_test.py +++ b/tests/boxes/tbox_test.py @@ -1123,3 +1123,15 @@ def test_gt(self): def test_ge(self): _ = self.tbfxt >= self.other + + +class TestTBoxTiling(TestTBox): + # Mirrors #test_value_time_split of the temporal split tests, on the value-time tiles + def test_tile(self): + box = TBox("TBOXFLOAT XT([0, 33.35670867892772],[2020-06-01 08:00:15.533+02, 2020-06-06 01:53:54.08198+02])") + tiles = box.tile(10, "1 day", 0.0, "2020-06-01 00:00:00+02") + assert tiles == [ + TBox(f"TBOXFLOAT XT([{v}, {v + 10}),[2020-06-0{d} 00:00:00+02, 2020-06-0{d + 1} 00:00:00+02))") + for d in range(1, 7) + for v in (0, 10, 20, 30) + ] From d4de97edf48a6f1b38244570c456b090b90f5cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Wed, 21 Oct 2026 01:37:36 +0200 Subject: [PATCH 5/7] Test every aggregator on two overlapping inputs tests/aggregators runs each aggregator of pymeos.aggregators on two inputs that overlap in time, so every result holds a stretch where both contribute and a stretch where one does: the boolean, number and text temporal aggregates, the three extents, the instant and period counts, the instantaneous time union, and an incremental aggregation against the one-call form. Witness: on PyMEOS-CFFI regenerated from MobilityDB master 1eca005aeb the 18 cases pass, and 16 of them fail on the surface of master adbb2ebad1, where each aggregation starts from a None state the transition wrapper casts unguarded. Measured: each expected value checks by its definition on the two inputs, the integer sum being 1, then 1 + 2 = 3 from 2000-01-02, then 3 + 2 = 5 at 2000-01-03, then 2 to 2000-01-04. The suite passes 4471 of 4471. --- tests/aggregators/__init__.py | 0 tests/aggregators/aggregators_test.py | 170 ++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tests/aggregators/__init__.py create mode 100644 tests/aggregators/aggregators_test.py diff --git a/tests/aggregators/__init__.py b/tests/aggregators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/aggregators/aggregators_test.py b/tests/aggregators/aggregators_test.py new file mode 100644 index 00000000..b2967997 --- /dev/null +++ b/tests/aggregators/aggregators_test.py @@ -0,0 +1,170 @@ +import pytest + +from pymeos import ( + STBox, + TBoolSeq, + TBoolSeqSet, + TBox, + TFloatSeq, + TFloatSeqSet, + TGeomPointSeq, + TIntSeq, + TIntSeqSet, + TsTzSet, + TsTzSpan, + TTextSeq, + TTextSeqSet, +) +from pymeos.aggregators import ( + TemporalAndAggregator, + TemporalAverageAggregator, + TemporalExtentAggregator, + TemporalFloatMaxAggregator, + TemporalFloatMinAggregator, + TemporalFloatSumAggregator, + TemporalInstantCountAggregator, + TemporalIntMaxAggregator, + TemporalIntMinAggregator, + TemporalIntSumAggregator, + TemporalNumberExtentAggregator, + TemporalOrAggregator, + TemporalPeriodCountAggregator, + TemporalPointExtentAggregator, + TemporalTextMaxAggregator, + TemporalTextMinAggregator, + TimeInstantaneousUnionAggregator, +) +from tests.conftest import TestPyMEOS + + +class TestAggregators(TestPyMEOS): + """Every aggregator on two overlapping inputs, laid out as #TestTBoxConstructors is.""" + + ints = [TIntSeq("[1@2000-01-01, 3@2000-01-03]"), TIntSeq("[2@2000-01-02, 2@2000-01-04]")] + floats = [TFloatSeq("[1@2000-01-01, 3@2000-01-03]"), TFloatSeq("[2@2000-01-02, 2@2000-01-04]")] + bools = [TBoolSeq("[t@2000-01-01, f@2000-01-03]"), TBoolSeq("[t@2000-01-02, t@2000-01-04]")] + texts = [TTextSeq('["a"@2000-01-01, "c"@2000-01-03]'), TTextSeq('["b"@2000-01-02, "b"@2000-01-04]')] + points = [ + TGeomPointSeq("[POINT(0 0)@2000-01-01, POINT(2 2)@2000-01-03]"), + TGeomPointSeq("[POINT(1 3)@2000-01-02, POINT(1 3)@2000-01-04]"), + ] + spans = [TsTzSpan("[2000-01-01, 2000-01-03]"), TsTzSpan("[2000-01-02, 2000-01-04]")] + sets = [TsTzSet("{2000-01-01, 2000-01-03}"), TsTzSet("{2000-01-02, 2000-01-03}")] + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalAndAggregator, + bools, + TBoolSeqSet("{[t@2000-01-01, f@2000-01-03], (t@2000-01-03, t@2000-01-04]}"), + ), + (TemporalOrAggregator, bools, TBoolSeqSet("{[t@2000-01-01, t@2000-01-04]}")), + ( + TemporalIntMaxAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntMinAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntSumAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 3@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMaxAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMinAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-04]}"), + ), + ( + TemporalFloatSumAggregator, + floats, + TFloatSeqSet( + "{[1@2000-01-01, 2@2000-01-02), [4@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}" + ), + ), + ( + TemporalAverageAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2.5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalTextMaxAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-02, "c"@2000-01-03], ("b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalTextMinAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalInstantCountAggregator, + sets, + TIntSeq("{1@2000-01-01, 1@2000-01-02, 2@2000-01-03}"), + ), + ( + TemporalPeriodCountAggregator, + spans, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-03], (1@2000-01-03, 1@2000-01-04]}"), + ), + ( + TimeInstantaneousUnionAggregator, + sets, + TsTzSet("{2000-01-01, 2000-01-02, 2000-01-03}"), + ), + ], + ids=[ + "And", + "Or", + "IntMax", + "IntMin", + "IntSum", + "FloatMax", + "FloatMin", + "FloatSum", + "Average", + "TextMax", + "TextMin", + "InstantCount", + "PeriodCount", + "InstantaneousUnion", + ], + ) + def test_aggregate(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalNumberExtentAggregator, + floats, + TBox("TBOXFLOAT XT([1, 3],[2000-01-01, 2000-01-04])"), + ), + ( + TemporalPointExtentAggregator, + points, + STBox("STBOX XT(((0,0),(2,3)),[2000-01-01, 2000-01-04])"), + ), + (TemporalExtentAggregator, ints, TsTzSpan("[2000-01-01, 2000-01-04]")), + ], + ids=["NumberExtent", "PointExtent", "TimeExtent"], + ) + def test_extent(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + def test_incremental_aggregation(self): + aggregation = TemporalIntSumAggregator.start_aggregation() + for temporal in self.ints: + aggregation.add(temporal) + assert aggregation.aggregation() == TemporalIntSumAggregator.aggregate(self.ints) From 39f5fdd650bdf98e5a3f08e6fbf2365bbf620bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Wed, 21 Oct 2026 01:44:02 +0200 Subject: [PATCH 6/7] Aggregate continuous time through the span union transition functions TimeContinuousUnionAggregator aggregates as TimeInstantaneousUnionAggregator does: span_union_transfn and spanset_union_transfn add each span or span set to the state, which starts as None, and spanset_union_finalfn finishes it. union_spanset_span and union_spanset_spanset are the binary unions of two values, and MEOS refuses a NULL span set there, so they cannot start an aggregation. Witness: the union of [2000-01-01, 2000-01-03] and [2000-01-02, 2000-01-04] aggregates to {[2000-01-01, 2000-01-04]}, a case added to tests/aggregators; through union_spanset_span the first add raises TypeError: an integer is required, from casting the None state the wrapper does not guard. The suite passes 4472 of 4472. --- pymeos/aggregators/time_aggregators.py | 17 +++++++++++------ tests/aggregators/aggregators_test.py | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pymeos/aggregators/time_aggregators.py b/pymeos/aggregators/time_aggregators.py index 159503d4..a730ae8e 100644 --- a/pymeos/aggregators/time_aggregators.py +++ b/pymeos/aggregators/time_aggregators.py @@ -4,9 +4,10 @@ datetime_to_timestamptz, set_union_finalfn, set_union_transfn, + span_union_transfn, + spanset_union_finalfn, + spanset_union_transfn, timestamptz_union_transfn, - union_spanset_span, - union_spanset_spanset, ) from ..collections import TsTzSet, TsTzSpan, TsTzSpanSet @@ -43,20 +44,24 @@ class TimeContinuousUnionAggregator(BaseAggregator[TsTzSpan | TsTzSpanSet, TsTzS Temporal union of continuous time objects (:class:`~pymeos.time.tstzspan.TsTzSpan` and :class:`~pymeos.time.tstzspanset.TsTzSpanSet`). + Aggregates as #TimeInstantaneousUnionAggregator does, through the transition and final + functions MEOS provides for the union of spans. + MEOS Functions: - union_spanset_span, union_spanset_spanset + span_union_transfn, spanset_union_transfn, spanset_union_finalfn """ @classmethod def _add(cls, state, temporal): if isinstance(temporal, TsTzSpan): - state = union_spanset_span(state, temporal._inner) + state = span_union_transfn(state, temporal._inner) elif isinstance(temporal, TsTzSpanSet): - state = union_spanset_spanset(state, temporal._inner) + state = spanset_union_transfn(state, temporal._inner) else: cls._error(temporal) return state @classmethod def _finish(cls, state) -> TsTzSpanSet: - return TsTzSpanSet(_inner=state) + result = spanset_union_finalfn(state) + return TsTzSpanSet(_inner=result) diff --git a/tests/aggregators/aggregators_test.py b/tests/aggregators/aggregators_test.py index b2967997..51741a08 100644 --- a/tests/aggregators/aggregators_test.py +++ b/tests/aggregators/aggregators_test.py @@ -12,6 +12,7 @@ TIntSeqSet, TsTzSet, TsTzSpan, + TsTzSpanSet, TTextSeq, TTextSeqSet, ) @@ -32,6 +33,7 @@ TemporalPointExtentAggregator, TemporalTextMaxAggregator, TemporalTextMinAggregator, + TimeContinuousUnionAggregator, TimeInstantaneousUnionAggregator, ) from tests.conftest import TestPyMEOS @@ -122,6 +124,11 @@ class TestAggregators(TestPyMEOS): sets, TsTzSet("{2000-01-01, 2000-01-02, 2000-01-03}"), ), + ( + TimeContinuousUnionAggregator, + spans, + TsTzSpanSet("{[2000-01-01, 2000-01-04]}"), + ), ], ids=[ "And", @@ -138,6 +145,7 @@ class TestAggregators(TestPyMEOS): "InstantCount", "PeriodCount", "InstantaneousUnion", + "ContinuousUnion", ], ) def test_aggregate(self, aggregator, inputs, expected): From 201a0c172264b3426a88632651262b374b6546c0 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Tue, 19 May 2026 09:24:44 +0200 Subject: [PATCH 7/7] feat: object-oriented support for the cbuffer, npoint, pose and rgeometry temporal types Add first-class PyMEOS classes for the four extended temporal type families, mirroring the existing tpoint/ttext pattern and reusing the exact pymeos_cffi backing functions (no reimplementation): - main: TNpoint/TCbuffer/TPose/TRgeometry (+ Inst/Seq/SeqSet) - collections: base value types Npoint/Cbuffer/Pose (+ their *Set) and Nsegment; rgeometry's base is a shapely geometry (reuses GeometrySet) - factory + package exports wired for all four families; tests added Each type covers 100% of its real MEOS-backed surface, verified against the MEOS headers: - TCbuffer exposes the full spatial-relationship surface MEOS provides (ever / always / temporal: contains, covers, disjoint, within-distance, intersects, touches), mirroring TPoint. - MEOS defines no spatial-relationship API for npoint, pose or rgeometry, so their relationship surface is complete at zero by construction (documented, not excluded, not faked). - TRgeometry additionally exposes its full backed surface: segments, delete (timestamptz/tstzset/tstzspan/tstzspanset), before/after a timestamp, to_instant, and the sequence-set accessors (start_sequence/end_sequence/sequence_n/sequences) -- each delegating to the rigid-geometry-specific trgeo_* function. - TNpoint.positions() exposes tnpoint_positions via a new Nsegment wrapper (full nsegment_* surface). Omitted only where MEOS provides no usable backing: trgeometry has no string input parser (built via from_geometry_tpose / hexwkb), and the Datum-bearing trgeo_restrict_value/values helpers are MEOS-internal (consistent with how the ecosystem treats Datum helpers). The factory resolves the four type tags through the compiled binding (`_meos_cffi.lib`, the same source pymeos_cffi builds its enums from), so the build imports cleanly and the types activate even where pymeos_cffi's MeosType enum does not yet wrap the cbuffer/pose/rgeometry tags. TCbuffer's value accessors and value-based constructors (start_value/end_value/value_set/value_at_timestamp, from_base_time, from_mfjson) raise NotImplementedError citing the missing MEOS backing (MEOS exposes no typed tcbuffer_* value accessor / tcbufferinst_make; MF-JSON input is tracked upstream by MobilityDB#1051). This keeps TCbuffer/Inst/Seq/SeqSet concrete and instantiable rather than abstract. --- pymeos/__init__.py | 23 + pymeos/collections/__init__.py | 10 + pymeos/collections/cbuffer/__init__.py | 4 + pymeos/collections/cbuffer/cbuffer.py | 662 ++++++++++++++ pymeos/collections/cbuffer/cbufferset.py | 227 +++++ pymeos/collections/npoint/__init__.py | 5 + pymeos/collections/npoint/npoint.py | 415 +++++++++ pymeos/collections/npoint/npointset.py | 125 +++ pymeos/collections/npoint/nsegment.py | 150 ++++ pymeos/collections/pose/__init__.py | 4 + pymeos/collections/pose/pose.py | 546 +++++++++++ pymeos/collections/pose/poseset.py | 226 +++++ pymeos/factory.py | 66 ++ pymeos/main/__init__.py | 25 + pymeos/main/tcbuffer.py | 1047 ++++++++++++++++++++++ pymeos/main/tnpoint.py | 684 ++++++++++++++ pymeos/main/tpose.py | 722 +++++++++++++++ pymeos/main/trgeometry.py | 1042 +++++++++++++++++++++ tests/main/tcbuffer_test.py | 146 +++ tests/main/tnpoint_test.py | 253 ++++++ tests/main/tpose_test.py | 166 ++++ tests/main/trgeometry_test.py | 179 ++++ 22 files changed, 6727 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..7cfade69 --- /dev/null +++ b/pymeos/collections/cbuffer/cbuffer.py @@ -0,0 +1,662 @@ +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, len(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_out + """ + return cbuffer_out(self._inner, 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)[0] + + 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, None) + + # ------------------------- 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) + + def same(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` is spatially the same as ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` is the same as ``other``, ``False`` otherwise. + + MEOS Functions: + cbuffer_same + """ + return cbuffer_same(self._inner, other._inner) + + def not_same(self, other: Cbuffer) -> bool: + """ + Returns whether ``self`` is spatially not the same as ``other``. + + Args: + other: A :class:`Cbuffer` instance. + + Returns: + ``True`` if ``self`` is not the same as ``other``, ``False`` + otherwise. + + MEOS Functions: + cbuffer_nsame + """ + return cbuffer_nsame(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..61ad5311 --- /dev/null +++ b/pymeos/collections/cbuffer/cbufferset.py @@ -0,0 +1,227 @@ +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: + cbufferset_out + """ + return cbufferset_out(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..5cd4d5a1 --- /dev/null +++ b/pymeos/collections/npoint/npoint.py @@ -0,0 +1,415 @@ +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, len(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_out + """ + return npoint_out(self._inner, 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)[0] + + 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 same(self, other: Npoint) -> bool: + """ + Returns whether ``self`` and ``other`` are spatially equal. + + Args: + other: The :class:`Npoint` to compare with ``self``. + + Returns: + ``True`` if ``self`` is spatially equal to ``other``, ``False`` + otherwise. + + MEOS Functions: + npoint_same + """ + return npoint_same(self._inner, other._inner) + + 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..6399068f --- /dev/null +++ b/pymeos/collections/npoint/npointset.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from pymeos_cffi import * + +from ..base import Set +from .npoint import Npoint + + +def _npointset_make(values: list[CData]) -> CData: + return npointset_make(values, len(values)) + + +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(1, 0.5), Npoint(2, 0.3)]) + + """ + + __slots__ = ["_inner"] + + _mobilitydb_name = "npointset" + + _parse_function = npointset_in + _parse_value_function = lambda x: npoint_in(x)._inner if isinstance(x, str) else x._inner + _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: + npointset_out + """ + return npointset_out(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)) 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..2797c7af --- /dev/null +++ b/pymeos/collections/pose/pose.py @@ -0,0 +1,546 @@ +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_out + """ + return pose_out(self._inner, 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)[0] + + 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 orientation(self) -> shp.BaseGeometry: + """ + Returns the orientation of ``self`` as a unit quaternion. + + Returns: + The orientation of ``self``. + + MEOS Functions: + pose_orientation + """ + return pose_orientation(self._inner) + + def rotation(self) -> float: + """ + Returns the rotation angle of ``self``. + + Returns: + A :class:`float` instance. + + MEOS Functions: + pose_rotation + """ + return pose_rotation(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) + + def same(self, other: Pose) -> bool: + """ + Returns whether ``self`` and ``other`` are spatially the same. + + Args: + other: The :class:`Pose` to compare with ``self``. + + Returns: + ``True`` if the same, ``False`` otherwise. + + MEOS Functions: + pose_same + """ + return pose_same(self._inner, other._inner) + + def not_same(self, other: Pose) -> bool: + """ + Returns whether ``self`` and ``other`` are not spatially the same. + + Args: + other: The :class:`Pose` to compare with ``self``. + + Returns: + ``True`` if not the same, ``False`` otherwise. + + MEOS Functions: + pose_nsame + """ + return pose_nsame(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..061d0ea6 --- /dev/null +++ b/pymeos/collections/pose/poseset.py @@ -0,0 +1,226 @@ +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 = lambda elements: poseset_make(elements, len(elements)) + + # ------------------------- 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: + poseset_out + """ + return poseset_out(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..b3be7aa0 --- /dev/null +++ b/pymeos/main/tcbuffer.py @@ -0,0 +1,1047 @@ +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.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_trav_area + """ + return gserialized_to_shapely_geometry(tcbuffer_trav_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 ------------------------------- + # MEOS exposes NO typed value accessor or value-based constructor for the + # temporal circular buffer: there is no tcbuffer_start_value/end_value/ + # value_set/value_at_timestamp, no tcbufferinst_make, and no + # tcbuffer_from_base_*; the only generic accessor returns an opaque Datum + # that cannot be rebuilt into a Cbuffer without Datum-hiding internals. + # These overrides keep the class concrete and fail loudly instead of + # making the whole type uninstantiable. from_mfjson is being added + # upstream by MobilityDB#1051 (feat/tcbuffer-mfjson); the others remain + # unimplemented in MEOS and are a separate upstream feature. + def start_value(self) -> Cbuffer: + """Not exposed by MEOS for the temporal circular buffer.""" + raise NotImplementedError( + "MEOS exposes no typed start_value for the temporal circular " + "buffer (no tcbuffer_start_value); pending upstream." + ) + + def end_value(self) -> Cbuffer: + """Not exposed by MEOS for the temporal circular buffer.""" + raise NotImplementedError( + "MEOS exposes no typed end_value for the temporal circular " + "buffer (no tcbuffer_end_value); pending upstream." + ) + + def value_set(self) -> set[Cbuffer]: + """Not exposed by MEOS for the temporal circular buffer.""" + raise NotImplementedError( + "MEOS exposes no typed value_set for the temporal circular buffer (no tcbuffer_values); pending upstream." + ) + + def value_at_timestamp(self, timestamp: datetime) -> Cbuffer: + """Not exposed by MEOS for the temporal circular buffer.""" + raise NotImplementedError( + "MEOS exposes no typed value_at_timestamp for the temporal " + "circular buffer (no tcbuffer_value_at_timestamp); pending " + "upstream." + ) + + @staticmethod + def from_base_time(value: Cbuffer, base: Time) -> TCbuffer: + """Not exposed by MEOS for the temporal circular buffer.""" + raise NotImplementedError( + "MEOS exposes no value-based constructor for the temporal " + "circular buffer (no tcbufferinst_make / tcbuffer_from_base_*); " + "pending upstream." + ) + + @classmethod + def from_mfjson(cls, mfjson: str) -> TCbuffer: + """MF-JSON input is being added upstream (MobilityDB#1051).""" + raise NotImplementedError( + "MEOS MF-JSON input for the temporal circular buffer is being " + "added upstream by MobilityDB#1051 (feat/tcbuffer-mfjson)." + ) + + # ------------------------- 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) -> bool: + """ + Returns whether `self` ever contains `other`. + + MEOS Functions: + econtains_tcbuffer_geo, econtains_tcbuffer_cbuffer + """ + 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) + else: + raise TypeError(f"Operation not supported with type {other.__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_disjoint(self, other: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` is ever disjoint from `other`. + + MEOS Functions: + edisjoint_tcbuffer_geo, edisjoint_tcbuffer_cbuffer + """ + 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) + 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) -> bool: + """ + Returns whether `self` always contains `other`. + + MEOS Functions: + acontains_tcbuffer_geo, acontains_tcbuffer_cbuffer + """ + 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) + else: + raise TypeError(f"Operation not supported with type {other.__class__}") + return result == 1 + + def is_always_covers(self, other: shpb.BaseGeometry | Cbuffer) -> bool: + """ + Returns whether `self` always covers `other`. + + MEOS Functions: + acovers_tcbuffer_geo, acovers_tcbuffer_cbuffer + """ + 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) + else: + raise TypeError(f"Operation not supported with type {other.__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..218791a3 --- /dev/null +++ b/pymeos/main/tnpoint.py @@ -0,0 +1,684 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, TypeVar + +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 ---------------------------------- + + # ------------------------- 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) + + # ------------------------- 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_point, 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_point(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..4be2d109 --- /dev/null +++ b/pymeos/main/tpose.py @@ -0,0 +1,722 @@ +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, 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_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 rotation(self) -> TFloat: + """ + Returns the rotation of `self` as a temporal float. + + Returns: + A new :class:`TFloat` with the rotation. + + MEOS Functions: + tpose_rotation + """ + result = tpose_rotation(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_point, tdistance_tpose_pose, tdistance_tpose_tpose + """ + if isinstance(other, shpb.BaseGeometry): + gs = geo_to_gserialized(other, False) + result = tdistance_tpose_point(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..d954076b --- /dev/null +++ b/pymeos/main/trgeometry.py @@ -0,0 +1,1042 @@ +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" + + BaseClass = shpb.BaseGeometry + + def __init__(self, _inner) -> None: + super().__init__() + + # ------------------------- Constructors ---------------------------------- + @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: + geo_tpose_to_trgeo + """ + gs = geometry_to_gserialized(geometry) + result = geo_tpose_to_trgeo(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: + trgeo_out + """ + return trgeo_out(self._inner) + + 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: + tspatial_as_text + """ + return tspatial_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: + tspatial_as_ewkt + """ + return tspatial_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: + trgeo_to_tpose + """ + result = trgeo_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: + trgeo_to_tpoint + """ + result = trgeo_to_tpoint(self._inner) + return Temporal._factory(result) + + def to_instant(self) -> TRgeometryInst: + """ + Returns `self` as a :class:`TRgeometryInst`. + + MEOS Functions: + trgeo_to_tinstant + """ + return Temporal._factory(trgeo_to_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: + trgeo_geom + """ + return gserialized_to_shapely_geometry(trgeo_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: + trgeo_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: + trgeo_start_value + """ + return gserialized_to_shapely_geometry(trgeo_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: + trgeo_end_value + """ + return gserialized_to_shapely_geometry(trgeo_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: + trgeo_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: + tgeo_value_at_timestamptz + """ + return gserialized_to_shapely_geometry( + tgeo_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: + trgeo_value_n + """ + return gserialized_to_shapely_geometry(trgeo_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: + trgeo_start_instant + """ + return Temporal._factory(trgeo_start_instant(self._inner)) + + def end_instant(self: Self) -> TRgeometryInst: + """ + Returns the last instant of `self`. + + Returns: + A new :class:`TRgeometryInst` object. + + MEOS Functions: + trgeo_end_instant + """ + return Temporal._factory(trgeo_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: + trgeo_instant_n + """ + return Temporal._factory(trgeo_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: + trgeo_instants + """ + ins, count = trgeo_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: + trgeo_points + """ + from ..factory import _CollectionFactory + + return _CollectionFactory.create_collection(trgeo_points(self._inner)) + + def rotation(self) -> TFloat: + """ + Returns the rotation of `self` as a temporal float. + + Returns: + A new :class:`TFloat` with the rotation. + + MEOS Functions: + trgeo_rotation + """ + result = trgeo_rotation(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: + trgeo_traversed_area + """ + return gserialized_to_shapely_geometry(trgeo_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: + trgeo_round + """ + result = trgeo_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: + trgeo_set_interp + """ + result = trgeo_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: + trgeo_append_tinstant + """ + interp = self.interpolation() + mt = timedelta_to_interval(max_time) if max_time is not None else None + result = trgeo_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: + trgeo_append_tsequence + """ + result = trgeo_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_trgeo_geo, always_eq_trgeo_trgeo + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return always_eq_trgeo_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return always_eq_trgeo_trgeo(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_trgeo_geo, always_ne_trgeo_trgeo + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return always_ne_trgeo_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return always_ne_trgeo_trgeo(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_trgeo_geo, ever_eq_trgeo_trgeo + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return ever_eq_trgeo_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return ever_eq_trgeo_trgeo(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_trgeo_geo, ever_ne_trgeo_trgeo + """ + if isinstance(value, shpb.BaseGeometry): + gs = geometry_to_gserialized(value) + return ever_ne_trgeo_geo(self._inner, gs) > 0 + elif isinstance(value, TRgeometry): + return ever_ne_trgeo_trgeo(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_trgeo_geo, ever_eq_trgeo_trgeo + """ + 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_trgeo_geo, ever_ne_trgeo_trgeo + """ + 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_trgeo_geo, teq_temporal_temporal + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = teq_trgeo_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_trgeo_geo, tne_temporal_temporal + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tne_trgeo_geo(self._inner, gs) + else: + return super().temporal_not_equal(other) + return Temporal._factory(result) + + # ------------------------- 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: + trgeo_restrict_timestamptz, trgeo_restrict_tstzset, + trgeo_restrict_tstzspan, trgeo_restrict_tstzspanset + """ + if isinstance(other, datetime): + result = trgeo_restrict_timestamptz(self._inner, datetime_to_timestamptz(other), True) + elif isinstance(other, TsTzSet): + result = trgeo_restrict_tstzset(self._inner, other._inner, True) + elif isinstance(other, TsTzSpan): + result = trgeo_restrict_tstzspan(self._inner, other._inner, True) + elif isinstance(other, TsTzSpanSet): + result = trgeo_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: + trgeo_restrict_timestamptz, trgeo_restrict_tstzset, + trgeo_restrict_tstzspan, trgeo_restrict_tstzspanset + """ + if isinstance(other, datetime): + result = trgeo_restrict_timestamptz(self._inner, datetime_to_timestamptz(other), False) + elif isinstance(other, TsTzSet): + result = trgeo_restrict_tstzset(self._inner, other._inner, False) + elif isinstance(other, TsTzSpan): + result = trgeo_restrict_tstzspan(self._inner, other._inner, False) + elif isinstance(other, TsTzSpanSet): + result = trgeo_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: + trgeo_segments + """ + seqs, count = trgeo_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: + trgeo_delete_timestamptz, trgeo_delete_tstzset, + trgeo_delete_tstzspan, trgeo_delete_tstzspanset + """ + if isinstance(other, datetime): + result = trgeo_delete_timestamptz(self._inner, datetime_to_timestamptz(other), connect) + elif isinstance(other, TsTzSet): + result = trgeo_delete_tstzset(self._inner, other._inner, connect) + elif isinstance(other, TsTzSpan): + result = trgeo_delete_tstzspan(self._inner, other._inner, connect) + elif isinstance(other, TsTzSpanSet): + result = trgeo_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: + trgeo_before_timestamptz + """ + result = trgeo_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: + trgeo_after_timestamptz + """ + result = trgeo_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_trgeo_geo, tdistance_trgeo_tpoint, tdistance_trgeo_trgeo + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = tdistance_trgeo_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = tdistance_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = tdistance_trgeo_trgeo(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_trgeo_geo, nad_trgeo_stbox, nad_trgeo_tpoint, nad_trgeo_trgeo + """ + from ..boxes import STBox + + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + return nad_trgeo_geo(self._inner, gs) + elif isinstance(other, STBox): + return nad_trgeo_stbox(self._inner, other._inner) + elif isinstance(other, TPoint): + return nad_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + return nad_trgeo_trgeo(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_trgeo_geo, nai_trgeo_tpoint, nai_trgeo_trgeo + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = nai_trgeo_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = nai_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = nai_trgeo_trgeo(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_trgeo_geo, shortestline_trgeo_tpoint, + shortestline_trgeo_trgeo + """ + if isinstance(other, shpb.BaseGeometry): + gs = geometry_to_gserialized(other) + result = shortestline_trgeo_geo(self._inner, gs) + elif isinstance(other, TPoint): + result = shortestline_trgeo_tpoint(self._inner, other._inner) + elif isinstance(other, TRgeometry): + result = shortestline_trgeo_trgeo(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: + super().__init__(string=string, _inner=_inner) + if self._inner is None: + gs = geometry_to_gserialized(geometry) + ts = ( + datetime_to_timestamptz(timestamp) + if isinstance(timestamp, datetime) + else pg_timestamptz_in(timestamp, -1) + ) + self._inner = trgeoinst_make(gs, pose._inner, ts) + + +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: + trgeo_start_sequence + """ + return self.ComponentClass(_inner=trgeo_start_sequence(self._inner)) + + def end_sequence(self) -> TRgeometrySeq: + """ + Returns the last sequence of `self`. + + MEOS Functions: + trgeo_end_sequence + """ + return self.ComponentClass(_inner=trgeo_end_sequence(self._inner)) + + def sequence_n(self, n: int) -> TRgeometrySeq: + """ + Returns the ``n``-th sequence of `self` (0-based). + + MEOS Functions: + trgeo_sequence_n + """ + return self.ComponentClass(_inner=trgeo_sequence_n(self._inner, n + 1)) + + def sequences(self) -> list[TRgeometrySeq]: + """ + Returns the list of sequences of `self`. + + MEOS Functions: + trgeo_sequences + """ + seqs, count = trgeo_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..b0f28174 --- /dev/null +++ b/tests/main/tcbuffer_test.py @@ -0,0 +1,146 @@ +from copy import copy + +import pytest +from shapely import Point + +from pymeos import ( + Cbuffer, + TBool, + TCbufferInst, + TCbufferSeq, + TCbufferSeqSet, + TInterpolation, +) +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) + + 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_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 diff --git a/tests/main/tnpoint_test.py b/tests/main/tnpoint_test.py new file mode 100644 index 00000000..06559b6b --- /dev/null +++ b/tests/main/tnpoint_test.py @@ -0,0 +1,253 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest + +from pymeos import ( + Npoint, + NpointSet, + Nsegment, + TFloat, + TInterpolation, + TNpoint, + TNpointInst, + TNpointSeq, + TNpointSeqSet, +) +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(1, 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", + [tpi, tps], + ids=["Instant", "Sequence"], + ) + def test_repr_round_trip(self, temporal): + assert temporal == temporal.__class__(repr(temporal)) + + +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(1, 0.5)) + + def test_never_not_equal(self): + assert isinstance(self.tps.never_not_equal(Npoint(1, 0.5)), bool) + + def test_temporal_equal(self): + from pymeos import TBool + + assert isinstance(self.tps.temporal_equal(Npoint(1, 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_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(1, 0.5), Npoint(2, 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)) + + +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) diff --git a/tests/main/tpose_test.py b/tests/main/tpose_test.py new file mode 100644 index 00000000..15ea50a7 --- /dev/null +++ b/tests/main/tpose_test.py @@ -0,0 +1,166 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest + +from pymeos import ( + Pose, + TInterpolation, + TPose, + TPoseInst, + TPoseSeq, + TPoseSeqSet, +) +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_rotation(self): + assert isinstance(self.p.rotation(), 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) diff --git a/tests/main/trgeometry_test.py b/tests/main/trgeometry_test.py new file mode 100644 index 00000000..9fce74e1 --- /dev/null +++ b/tests/main/trgeometry_test.py @@ -0,0 +1,179 @@ +from copy import copy +from datetime import datetime, timezone + +import pytest +from shapely import Polygon + +from pymeos import ( + Pose, + TInterpolation, + TPoseInst, + TPoseSeq, + TRgeometry, + TRgeometryInst, + TRgeometrySeq, +) +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) + + 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 isinstance(str(self.tri), str) + + def test_repr(self): + assert isinstance(repr(self.tri), str) + + def test_as_wkt(self): + assert isinstance(self.trs.as_wkt(), str) + + def test_as_ewkt(self): + assert isinstance(self.trs.as_ewkt(), str) + + +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_rotation(self): + assert self.trs.rotation() 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) + + +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) + + def test_to_instant(self): + assert self.trs.to_instant() is not None + + 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)