From 405699f03e39818a42c53cfa766f08898a266a2c Mon Sep 17 00:00:00 2001 From: "H. Thomson Comer" Date: Thu, 6 Apr 2023 16:13:16 -0500 Subject: [PATCH] Intersection only predicates (#1016) Closes #1015 Depends on #1009 This PR implements `intersects` and all of the feature combinations that depend exclusively on intersects, as listed in #1015. Authors: - H. Thomson Comer (https://github.com/thomcom) Approvers: - Michael Wang (https://github.com/isVoid) URL: https://github.com/rapidsai/cuspatial/pull/1016 --- .../cuspatial/core/binops/intersection.py | 12 +- .../core/binpreds/binpred_dispatch.py | 6 + .../core/binpreds/binpred_interface.py | 28 +- .../core/binpreds/feature_contains.py | 31 +- .../cuspatial/core/binpreds/feature_covers.py | 8 +- .../core/binpreds/feature_crosses.py | 14 +- .../core/binpreds/feature_disjoint.py | 76 ++ .../cuspatial/core/binpreds/feature_equals.py | 17 +- .../core/binpreds/feature_intersects.py | 158 ++- .../core/binpreds/feature_overlaps.py | 31 +- .../core/binpreds/feature_touches.py | 50 + .../cuspatial/core/binpreds/feature_within.py | 23 +- python/cuspatial/cuspatial/core/geoseries.py | 54 +- .../binpreds/test_intersects_only_binpreds.py | 1017 +++++++++++++++++ 14 files changed, 1449 insertions(+), 76 deletions(-) create mode 100644 python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py create mode 100644 python/cuspatial/cuspatial/core/binpreds/feature_touches.py create mode 100644 python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py diff --git a/python/cuspatial/cuspatial/core/binops/intersection.py b/python/cuspatial/cuspatial/core/binops/intersection.py index ecf32d0aa..978921532 100644 --- a/python/cuspatial/cuspatial/core/binops/intersection.py +++ b/python/cuspatial/cuspatial/core/binops/intersection.py @@ -1,5 +1,7 @@ # Copyright (c) 2023, NVIDIA CORPORATION. +from typing import TYPE_CHECKING + import cudf from cudf.core.column import arange, build_list_column @@ -8,15 +10,17 @@ ) from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.geoseries import GeoSeries from cuspatial.utils.column_utils import ( contains_only_linestrings, empty_geometry_column, ) +if TYPE_CHECKING: + from cuspatial.core.geoseries import GeoSeries + def pairwise_linestring_intersection( - linestrings1: GeoSeries, linestrings2: GeoSeries + linestrings1: "GeoSeries", linestrings2: "GeoSeries" ): """ Compute the intersection of two GeoSeries of linestrings. @@ -45,6 +49,8 @@ def pairwise_linestring_intersection( the intersection results came from. """ + from cuspatial.core.geoseries import GeoSeries + if len(linestrings1) == 0 and len(linestrings2) == 0: return ( cudf.Series([0]), @@ -97,6 +103,8 @@ def pairwise_linestring_intersection( meta = GeoMeta( {"input_types": types_buffer, "union_offsets": offset_buffer} ) + from cuspatial.core.geoseries import GeoSeries + geometries = GeoSeries( GeoColumn( ( diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py b/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py index 5a53abaca..f1cd0c51a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_dispatch.py @@ -17,6 +17,9 @@ from cuspatial.core.binpreds.feature_crosses import ( # NOQA F401 DispatchDict as CROSSES_DISPATCH, ) +from cuspatial.core.binpreds.feature_disjoint import ( # NOQA F401 + DispatchDict as DISJOINT_DISPATCH, +) from cuspatial.core.binpreds.feature_equals import ( # NOQA F401 DispatchDict as EQUALS_DISPATCH, ) @@ -26,6 +29,9 @@ from cuspatial.core.binpreds.feature_overlaps import ( # NOQA F401 DispatchDict as OVERLAPS_DISPATCH, ) +from cuspatial.core.binpreds.feature_touches import ( # NOQA F401 + DispatchDict as TOUCHES_DISPATCH, +) from cuspatial.core.binpreds.feature_within import ( # NOQA F401 DispatchDict as WITHIN_DISPATCH, ) diff --git a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py index 35db8c827..f380c425a 100644 --- a/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py +++ b/python/cuspatial/cuspatial/core/binpreds/binpred_interface.py @@ -1,9 +1,11 @@ # Copyright (c) 2022-2023, NVIDIA CORPORATION. -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple from cudf import Series +from cuspatial.utils.binpred_utils import _false_series + if TYPE_CHECKING: from cuspatial.core.geoseries import GeoSeries @@ -139,6 +141,19 @@ def __str__(self): return self.__repr__() +class IntersectsOpResult(OpResult): + """Result of an Intersection binary predicate operation.""" + + def __init__(self, result: Tuple): + self.result = result + + def __repr__(self): + return f"OpResult(result={self.result})" + + def __str__(self): + return self.__repr__() + + class BinPred: """Base class for binary predicates. This class is an abstract base class and can not be instantiated directly. `BinPred` is the base class that @@ -241,7 +256,7 @@ def __init__(self, **kwargs): 0 False dtype: bool """ - self.kwargs = kwargs + self.config = BinPredConfig(**kwargs) def __call__(self, lhs: "GeoSeries", rhs: "GeoSeries") -> Series: """System call for the binary predicate. Calls the _call method, @@ -441,3 +456,12 @@ class NotImplementedPredicate(BinPred): def __init__(self, *args, **kwargs): raise NotImplementedError + + +class ImpossiblePredicate(BinPred): + """There are many combinations that are impossible. This is the base class + to simply return a series of False values for these cases. + """ + + def _preprocess(self, lhs, rhs): + return _false_series(len(lhs)) diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py index 28e783c7d..c5852698b 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_contains.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_contains.py @@ -11,15 +11,11 @@ from cuspatial.core._column.geocolumn import GeoColumn from cuspatial.core.binpreds.binpred_interface import ( BinPred, - BinPredConfig, ContainsOpResult, NotImplementedPredicate, PreprocessorResult, ) from cuspatial.core.binpreds.contains import contains_properly -from cuspatial.core.binpreds.feature_equals import ( - DispatchDict as EQUALS_DISPATCH_DICT, -) from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -57,7 +53,7 @@ def __init__(self, **kwargs): right-hand GeoSeries. If False, the feature will be compared in a 1:1 fashion with the corresponding feature in the other GeoSeries. """ - self.config = BinPredConfig(**kwargs) + super().__init__(**kwargs) self.config.allpairs = kwargs.get("allpairs", False) def _preprocess(self, lhs, rhs): @@ -307,6 +303,11 @@ class PolygonComplexContains(ContainsPredicateBase): This class is shared by the Polygon*Contains classes that use a non-points object on the right hand side: MultiPoint, LineString, MultiLineString, Polygon, and MultiPolygon. + + Used by: + (Polygon, MultiPoint) + (Polygon, LineString) + (Polygon, Polygon) """ def _postprocess(self, lhs, rhs, preprocessor_output): @@ -334,12 +335,20 @@ def _postprocess(self, lhs, rhs, preprocessor_output): return final_result -class PointPointContains(ContainsPredicateBase): +class ContainsByIntersection(BinPred): + """Point types are contained only by an intersection test. + + Used by: + (Point, Point) + (LineString, Point) + """ + def _preprocess(self, lhs, rhs): - """PointPointContains that simply calls the equals predicate on the - points.""" + from cuspatial.core.binpreds.binpred_dispatch import ( + INTERSECTS_DISPATCH, + ) - predicate = EQUALS_DISPATCH_DICT[(lhs.column_type, rhs.column_type)]( + predicate = INTERSECTS_DISPATCH[(lhs.column_type, rhs.column_type)]( align=self.config.align ) return predicate(lhs, rhs) @@ -348,7 +357,7 @@ def _preprocess(self, lhs, rhs): """DispatchDict listing the classes to use for each combination of left and right hand side types. """ DispatchDict = { - (Point, Point): PointPointContains, + (Point, Point): ContainsByIntersection, (Point, MultiPoint): NotImplementedPredicate, (Point, LineString): NotImplementedPredicate, (Point, Polygon): NotImplementedPredicate, @@ -356,7 +365,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, + (LineString, Point): ContainsByIntersection, (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): NotImplementedPredicate, (LineString, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py index 02a0dcf3d..8c32ce9e4 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_covers.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_covers.py @@ -2,6 +2,10 @@ from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase +from cuspatial.core.binpreds.feature_intersects import ( + LineStringPointIntersects, + PointLineStringIntersects, +) from cuspatial.utils.binpred_utils import ( LineString, MultiPoint, @@ -35,13 +39,13 @@ class CoversPredicateBase(EqualsPredicateBase): DispatchDict = { (Point, Point): CoversPredicateBase, (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, LineString): PointLineStringIntersects, (Point, Polygon): CoversPredicateBase, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, + (LineString, Point): LineStringPointIntersects, (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): NotImplementedPredicate, (LineString, Polygon): CoversPredicateBase, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py index 0d3c0fe08..e1ea40a92 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_crosses.py @@ -1,6 +1,9 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate +from cuspatial.core.binpreds.binpred_interface import ( + ImpossiblePredicate, + NotImplementedPredicate, +) from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( LineString, @@ -15,6 +18,13 @@ class CrossesPredicateBase(EqualsPredicateBase): """Base class for binary predicates that are defined in terms of a the equals binary predicate. For example, a Point-Point Crosses predicate is defined in terms of a Point-Point Equals predicate. + + Used by: + (Point, Polygon) + (Polygon, Point) + (Polygon, MultiPoint) + (Polygon, LineString) + (Polygon, Polygon) """ pass @@ -35,7 +45,7 @@ def _preprocess(self, lhs, rhs): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, + (LineString, Point): ImpossiblePredicate, (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): NotImplementedPredicate, (LineString, Polygon): NotImplementedPredicate, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py new file mode 100644 index 000000000..92541b95f --- /dev/null +++ b/python/cuspatial/cuspatial/core/binpreds/feature_disjoint.py @@ -0,0 +1,76 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + NotImplementedPredicate, +) +from cuspatial.core.binpreds.feature_intersects import ( + IntersectsPredicateBase, + PointLineStringIntersects, +) +from cuspatial.utils.binpred_utils import ( + LineString, + MultiPoint, + Point, + Polygon, +) + + +class ContainsDisjoint(BinPred): + def _preprocess(self, lhs, rhs): + """Disjoint is the opposite of contains, so just implement contains + and then negate the result. + + Used by: + (Point, Point) + (Point, Polygon) + (Polygon, Point) + """ + from cuspatial.core.binpreds.binpred_dispatch import CONTAINS_DISPATCH + + predicate = CONTAINS_DISPATCH[(lhs.column_type, rhs.column_type)]( + align=self.config.align + ) + return ~predicate(lhs, rhs) + + +class PointLineStringDisjoint(PointLineStringIntersects): + def _postprocess(self, lhs, rhs, op_result): + """Disjoint is the opposite of intersects, so just implement intersects + and then negate the result.""" + result = super()._postprocess(lhs, rhs, op_result) + return ~result + + +class LineStringPointDisjoint(PointLineStringDisjoint): + def _preprocess(self, lhs, rhs): + """Swap ordering for Intersects.""" + return super()._preprocess(rhs, lhs) + + +class LineStringLineStringDisjoint(IntersectsPredicateBase): + def _postprocess(self, lhs, rhs, op_result): + """Disjoint is the opposite of intersects, so just implement intersects + and then negate the result.""" + result = super()._postprocess(lhs, rhs, op_result) + return ~result + + +DispatchDict = { + (Point, Point): ContainsDisjoint, + (Point, MultiPoint): NotImplementedPredicate, + (Point, LineString): PointLineStringDisjoint, + (Point, Polygon): ContainsDisjoint, + (MultiPoint, Point): NotImplementedPredicate, + (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, LineString): NotImplementedPredicate, + (MultiPoint, Polygon): NotImplementedPredicate, + (LineString, Point): LineStringPointDisjoint, + (LineString, MultiPoint): NotImplementedPredicate, + (LineString, LineString): LineStringLineStringDisjoint, + (LineString, Polygon): NotImplementedPredicate, + (Polygon, Point): ContainsDisjoint, + (Polygon, MultiPoint): NotImplementedPredicate, + (Polygon, LineString): NotImplementedPredicate, + (Polygon, Polygon): NotImplementedPredicate, +} diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py index 8b1480b97..b188ab226 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_equals.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_equals.py @@ -31,6 +31,15 @@ class EqualsPredicateBase(BinPred, Generic[GeoSeries]): """Base class for binary predicates that are defined in terms of the equals basic predicate. `EqualsPredicateBase` implements utility functions that are used within many equals-related binary predicates. + + Used by: + (Point, Point) + (Point, Polygon) + (LineString, Polygon) + (Polygon, Point) + (Polygon, MultiPoint) + (Polygon, LineString) + (Polygon, Polygon) """ def _offset_equals(self, lhs, rhs): @@ -320,6 +329,12 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): ) +class LineStringPointEquals(EqualsPredicateBase): + def _preprocess(self, lhs, rhs): + """A LineString cannot be equal to a point. So, return False.""" + return _false_series(len(lhs)) + + """DispatchDict for Equals operations.""" DispatchDict = { (Point, Point): EqualsPredicateBase, @@ -330,7 +345,7 @@ def _compute_predicate(self, lhs, rhs, preprocessor_result): (MultiPoint, MultiPoint): MultiPointMultiPointEquals, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, + (LineString, Point): LineStringPointEquals, (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): LineStringLineStringEquals, (LineString, Polygon): EqualsPredicateBase, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py index ca339b1dd..ecc3673f9 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_intersects.py @@ -1,6 +1,18 @@ # Copyright (c) 2023, NVIDIA CORPORATION. -from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate + +import cupy as cp + +import cudf + +import cuspatial +from cuspatial.core.binops.intersection import pairwise_linestring_intersection +from cuspatial.core.binpreds.binpred_interface import ( + BinPred, + IntersectsOpResult, + NotImplementedPredicate, + PreprocessorResult, +) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -8,20 +20,83 @@ MultiPoint, Point, Polygon, + _false_series, ) -class IntersectsPredicateBase(EqualsPredicateBase): +class IntersectsPredicateBase(BinPred): """Base class for binary predicates that are defined in terms of - the intersects basic predicate. These predicates are defined in terms - of the equals basic predicate. The type dispatches here that depend - on `IntersectsPredicateBase` use the `PredicateEquals` class for their - complete implementation, unmodified. - - point.intersects(polygon) is equivalent to polygon.contains(point) - with the left and right hand sides reversed. + the intersection primitive. """ + def _preprocess(self, lhs, rhs): + """Convert input lhs and rhs into LineStrings. + + The intersection basic predicate requires that the input + geometries be LineStrings or MultiLineStrings. This function + converts the input geometries into LineStrings and stores them + in the PreprocessorResult object that is passed into + _compute_predicate. + + Parameters + ---------- + lhs : GeoSeries + The left hand side of the binary predicate. + rhs : GeoSeries + The right hand side of the binary predicate. + + Returns + ------- + GeoSeries + A boolean Series containing the computed result of the + final binary predicate operation, + """ + return self._compute_predicate(lhs, rhs, PreprocessorResult(lhs, rhs)) + + def _compute_predicate(self, lhs, rhs, preprocessor_result): + """Compute the predicate using the intersects basic predicate. + lhs and rhs must both be LineStrings or MultiLineStrings. + """ + basic_result = pairwise_linestring_intersection( + preprocessor_result.lhs, preprocessor_result.rhs + ) + computed_result = IntersectsOpResult(basic_result) + return self._postprocess(lhs, rhs, computed_result) + + def _get_intersecting_geometry_indices(self, lhs, op_result): + """Naively computes the indices of matches by constructing + a set of lengths from the returned offsets buffer, then + returns an integer index for all of the offset sizes that + are larger than 0.""" + is_offsets = cudf.Series(op_result.result[0]) + is_sizes = is_offsets[1:].reset_index(drop=True) - is_offsets[ + :-1 + ].reset_index(drop=True) + return cp.arange(len(lhs))[is_sizes > 0] + + def _linestrings_from_polygons(self, geoseries): + xy = geoseries.polygons.xy + parts = geoseries.polygons.part_offset.take( + geoseries.polygons.geometry_offset + ) + rings = geoseries.polygons.ring_offset + return cuspatial.GeoSeries.from_linestrings_xy( + xy, + rings, + parts, + ) + + def _postprocess(self, lhs, rhs, op_result): + """Postprocess the output GeoSeries to ensure that they are of the + correct type for the predicate.""" + match_indices = self._get_intersecting_geometry_indices(lhs, op_result) + result = _false_series(len(lhs)) + if len(op_result.result[1]) > 0: + result[match_indices] = True + return result + + +class IntersectsByEquals(EqualsPredicateBase): pass @@ -33,22 +108,69 @@ def _preprocess(self, lhs, rhs): return super()._preprocess(rhs, lhs) +class LineStringPointIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + """Convert rhs to linestrings by making a linestring that has + the same start and end point.""" + x = cp.repeat(rhs.points.x, 2) + y = cp.repeat(rhs.points.y, 2) + xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() + parts = cp.arange((len(lhs) + 1)) * 2 + geometries = cp.arange(len(lhs) + 1) + ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) + return self._compute_predicate( + lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) + ) + + +class LineStringMultiPointIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + """Convert rhs to linestrings.""" + xy = rhs.multipoints.xy + parts = rhs.multipoints.geometry_offset + geometries = cp.arange(len(lhs) + 1) + ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) + return self._compute_predicate( + lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) + ) + + +class PointLineStringIntersects(LineStringPointIntersects): + def _preprocess(self, lhs, rhs): + """Swap LHS and RHS and call the normal contains processing.""" + return super()._preprocess(rhs, lhs) + + +class LineStringPointIntersects(IntersectsPredicateBase): + def _preprocess(self, lhs, rhs): + """Convert rhs to linestrings.""" + x = cp.repeat(rhs.points.x, 2) + y = cp.repeat(rhs.points.y, 2) + xy = cudf.DataFrame({"x": x, "y": y}).interleave_columns() + parts = cp.arange((len(lhs) + 1)) * 2 + geometries = cp.arange(len(lhs) + 1) + ls_rhs = cuspatial.GeoSeries.from_linestrings_xy(xy, parts, geometries) + return self._compute_predicate( + lhs, ls_rhs, PreprocessorResult(lhs, ls_rhs) + ) + + """ Type dispatch dictionary for intersects binary predicates. """ DispatchDict = { - (Point, Point): IntersectsPredicateBase, + (Point, Point): IntersectsByEquals, (Point, MultiPoint): NotImplementedPredicate, - (Point, LineString): NotImplementedPredicate, + (Point, LineString): PointLineStringIntersects, (Point, Polygon): PointPolygonIntersects, (MultiPoint, Point): NotImplementedPredicate, (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, - (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, + (LineString, Point): LineStringPointIntersects, + (LineString, MultiPoint): LineStringMultiPointIntersects, + (LineString, LineString): IntersectsPredicateBase, (LineString, Polygon): NotImplementedPredicate, - (Polygon, Point): IntersectsPredicateBase, - (Polygon, MultiPoint): IntersectsPredicateBase, - (Polygon, LineString): IntersectsPredicateBase, - (Polygon, Polygon): IntersectsPredicateBase, + (Polygon, Point): NotImplementedPredicate, + (Polygon, MultiPoint): NotImplementedPredicate, + (Polygon, LineString): NotImplementedPredicate, + (Polygon, Polygon): NotImplementedPredicate, } diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py index 6956c9cbf..20bad01a3 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_overlaps.py @@ -2,7 +2,10 @@ import cudf -from cuspatial.core.binpreds.binpred_interface import NotImplementedPredicate +from cuspatial.core.binpreds.binpred_interface import ( + ImpossiblePredicate, + NotImplementedPredicate, +) from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase from cuspatial.core.binpreds.feature_equals import EqualsPredicateBase from cuspatial.utils.binpred_utils import ( @@ -17,20 +20,22 @@ class OverlapsPredicateBase(EqualsPredicateBase): """Base class for overlaps binary predicate. Depends on the - equals predicate for all implementations up to this point. - For example, a Point-Point Crosses predicate is defined in terms + equals predicate for all implementations up to this point in + time. + For example, a Point-Point Overlaps predicate is defined in terms of a Point-Point Equals predicate. + + Used by: + (Point, Polygon) + (Polygon, Point) + (Polygon, MultiPoint) + (Polygon, LineString) + (Polygon, Polygon) """ pass -class PointPointOverlaps(OverlapsPredicateBase): - def _preprocess(self, lhs, rhs): - """Points can't overlap other points, so we return False.""" - return _false_series(len(lhs)) - - class PolygonPointOverlaps(ContainsPredicateBase): def _postprocess(self, lhs, rhs, op_result): if not has_same_geometry(lhs, rhs) or len(op_result.point_result) == 0: @@ -56,7 +61,7 @@ def _postprocess(self, lhs, rhs, op_result): """Dispatch table for overlaps binary predicate.""" DispatchDict = { - (Point, Point): PointPointOverlaps, + (Point, Point): ImpossiblePredicate, (Point, MultiPoint): NotImplementedPredicate, (Point, LineString): NotImplementedPredicate, (Point, Polygon): OverlapsPredicateBase, @@ -64,10 +69,10 @@ def _postprocess(self, lhs, rhs, op_result): (MultiPoint, MultiPoint): NotImplementedPredicate, (MultiPoint, LineString): NotImplementedPredicate, (MultiPoint, Polygon): NotImplementedPredicate, - (LineString, Point): NotImplementedPredicate, + (LineString, Point): ImpossiblePredicate, (LineString, MultiPoint): NotImplementedPredicate, - (LineString, LineString): NotImplementedPredicate, - (LineString, Polygon): NotImplementedPredicate, + (LineString, LineString): ImpossiblePredicate, + (LineString, Polygon): ImpossiblePredicate, (Polygon, Point): OverlapsPredicateBase, (Polygon, MultiPoint): OverlapsPredicateBase, (Polygon, LineString): OverlapsPredicateBase, diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_touches.py b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py new file mode 100644 index 000000000..8b4844577 --- /dev/null +++ b/python/cuspatial/cuspatial/core/binpreds/feature_touches.py @@ -0,0 +1,50 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. + +from cuspatial.core.binpreds.binpred_interface import ( + ImpossiblePredicate, + NotImplementedPredicate, +) +from cuspatial.core.binpreds.feature_contains import ContainsPredicateBase +from cuspatial.utils.binpred_utils import ( + LineString, + MultiPoint, + Point, + Polygon, +) + + +class TouchesPredicateBase(ContainsPredicateBase): + """Base class for binary predicates that use the contains predicate + to implement the touches predicate. For example, a Point-Polygon + Touches predicate is defined in terms of a Point-Polygon Contains + predicate. + + Used by: + (Point, Polygon) + (Polygon, Point) + (Polygon, MultiPoint) + (Polygon, LineString) + (Polygon, Polygon) + """ + + pass + + +DispatchDict = { + (Point, Point): ImpossiblePredicate, + (Point, MultiPoint): NotImplementedPredicate, + (Point, LineString): NotImplementedPredicate, + (Point, Polygon): TouchesPredicateBase, + (MultiPoint, Point): NotImplementedPredicate, + (MultiPoint, MultiPoint): NotImplementedPredicate, + (MultiPoint, LineString): NotImplementedPredicate, + (MultiPoint, Polygon): NotImplementedPredicate, + (LineString, Point): NotImplementedPredicate, + (LineString, MultiPoint): NotImplementedPredicate, + (LineString, LineString): NotImplementedPredicate, + (LineString, Polygon): NotImplementedPredicate, + (Polygon, Point): TouchesPredicateBase, + (Polygon, MultiPoint): TouchesPredicateBase, + (Polygon, LineString): TouchesPredicateBase, + (Polygon, Polygon): TouchesPredicateBase, +} diff --git a/python/cuspatial/cuspatial/core/binpreds/feature_within.py b/python/cuspatial/cuspatial/core/binpreds/feature_within.py index e438e5cea..66bc21943 100644 --- a/python/cuspatial/cuspatial/core/binpreds/feature_within.py +++ b/python/cuspatial/cuspatial/core/binpreds/feature_within.py @@ -15,16 +15,20 @@ ) -class RootWithin(EqualsPredicateBase): +class WithinPredicateBase(EqualsPredicateBase): """Base class for binary predicates that are defined in terms of a root-level binary predicate. For example, a Point-Point Within predicate is defined in terms of a Point-Point Contains predicate. + Used by: + (Polygon, Point) + (Polygon, MultiPoint) + (Polygon, LineString) """ pass -class PointPointWithin(RootWithin): +class PointPointWithin(WithinPredicateBase): def _postprocess(self, lhs, rhs, op_result): return cudf.Series(op_result.result) @@ -36,6 +40,15 @@ def _preprocess(self, lhs, rhs): class ComplexPolygonWithin(ContainsPredicateBase): + """Implements within for complex polygons. Depends on contains result + for the types. + + Used by: + (MultiPoint, Polygon) + (LineString, Polygon) + (Polygon, Polygon) + """ + def _preprocess(self, lhs, rhs): # Note the order of arguments is reversed. return super()._preprocess(rhs, lhs) @@ -75,8 +88,8 @@ def _postprocess(self, lhs, rhs, op_result): (LineString, MultiPoint): NotImplementedPredicate, (LineString, LineString): NotImplementedPredicate, (LineString, Polygon): ComplexPolygonWithin, - (Polygon, Point): RootWithin, - (Polygon, MultiPoint): RootWithin, - (Polygon, LineString): RootWithin, + (Polygon, Point): WithinPredicateBase, + (Polygon, MultiPoint): WithinPredicateBase, + (Polygon, LineString): WithinPredicateBase, (Polygon, Polygon): ComplexPolygonWithin, } diff --git a/python/cuspatial/cuspatial/core/geoseries.py b/python/cuspatial/cuspatial/core/geoseries.py index 944a12486..9ff85a7dc 100644 --- a/python/cuspatial/cuspatial/core/geoseries.py +++ b/python/cuspatial/cuspatial/core/geoseries.py @@ -26,26 +26,15 @@ import cuspatial.io.pygeoarrow as pygeoarrow from cuspatial.core._column.geocolumn import ColumnType, GeoColumn from cuspatial.core._column.geometa import Feature_Enum, GeoMeta -from cuspatial.core.binpreds.feature_contains import ( - DispatchDict as CONTAINS_DISPATCH, -) -from cuspatial.core.binpreds.feature_covers import ( - DispatchDict as COVERS_DISPATCH, -) -from cuspatial.core.binpreds.feature_crosses import ( - DispatchDict as CROSSES_DISPATCH, -) -from cuspatial.core.binpreds.feature_equals import ( - DispatchDict as EQUALS_DISPATCH, -) -from cuspatial.core.binpreds.feature_intersects import ( - DispatchDict as INTERSECTS_DISPATCH, -) -from cuspatial.core.binpreds.feature_overlaps import ( - DispatchDict as OVERLAPS_DISPATCH, -) -from cuspatial.core.binpreds.feature_within import ( - DispatchDict as WITHIN_DISPATCH, +from cuspatial.core.binpreds.binpred_dispatch import ( + CONTAINS_DISPATCH, + COVERS_DISPATCH, + CROSSES_DISPATCH, + DISJOINT_DISPATCH, + EQUALS_DISPATCH, + INTERSECTS_DISPATCH, + OVERLAPS_DISPATCH, + WITHIN_DISPATCH, ) from cuspatial.utils.column_utils import ( contains_only_linestrings, @@ -1226,3 +1215,28 @@ def crosses(self, other, align=True): align=align ) return predicate(self, other) + + def disjoint(self, other, align=True): + """Returns True for all aligned geometries that are disjoint from + other, else False. + + An object is said to be disjoint to other if its boundary and + interior does not intersect at all with those of the other. + + Parameters + ---------- + other + a cuspatial.GeoSeries + align=True + align the GeoSeries indexes before calling the binpred + + Returns + ------- + result : cudf.Series + A Series of boolean values indicating whether each pair of + corresponding geometries is disjoint. + """ + predicate = DISJOINT_DISPATCH[(self.column_type, other.column_type)]( + align=align + ) + return predicate(self, other) diff --git a/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py b/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py new file mode 100644 index 000000000..69a99b6c6 --- /dev/null +++ b/python/cuspatial/cuspatial/tests/binpreds/test_intersects_only_binpreds.py @@ -0,0 +1,1017 @@ +# Copyright (c) 2020-2023, NVIDIA CORPORATION + +import pandas as pd +import pytest +from shapely.geometry import ( + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +) + +import cuspatial + + +def test_point_intersects_point(): + g1 = cuspatial.GeoSeries([Point(0.0, 0.0)]) + g2 = cuspatial.GeoSeries([Point(0.0, 0.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_points_intersects_points(): + g1 = cuspatial.GeoSeries([Point(0.0, 0.0), Point(0.0, 0.0)]) + g2 = cuspatial.GeoSeries([Point(0.0, 0.0), Point(0.0, 1.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_point(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (0.0, 1.0)])]) + g2 = cuspatial.GeoSeries([Point(0.0, 0.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_extra_linestring_intersects_point(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([Point(1.0, 1.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_extra_linestring_intersects_point_2(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([Point(0.5, 0.5)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestrings_intersects_points(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries([Point(0.0, 0.0), Point(0.0, 0.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestrings_intersects_points_only_one(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries([Point(0.0, 0.0), Point(2.0, 2.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestrings_intersects_points_only_one_reversed(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries([Point(2.0, 2.0), Point(0.0, 0.0)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_three_points_match_middle(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(2.0, 2.0), + Point(0.0, 0.0), + Point(2.0, 2.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_three_points_exclude_middle(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(2.0, 2.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_100_linestrings_intersects_100_points( + linestring_generator, point_generator +): + g1 = cuspatial.GeoSeries([*linestring_generator(100, 4)]) + g2 = cuspatial.GeoSeries([*point_generator(100)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multipoint(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([MultiPoint([(0.0, 0.0), (1.0, 1.0)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multipoint_midpoint(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([MultiPoint([(0.5, 0.5), (0.5, 0.5)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multipoint_midpoint_disordered(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([MultiPoint([(0.0, 0.0), (0.5, 0.5)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multipoint_midpoint_disordered_2(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([MultiPoint([(0.5, 0.5), (0.0, 0.0)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_middle_multipoint(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiPoint([(2.0, 2.0), (3.0, 3.0)]), + MultiPoint([(0.0, 0.0), (0.0, 0.0)]), + MultiPoint([(2.0, 2.0), (4.0, 4.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_not_middle_multipoint(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiPoint([(0.0, 0.0), (0.0, 0.0)]), + MultiPoint([(2.0, 2.0), (3.0, 3.0)]), + MultiPoint([(0.0, 0.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multipoint_cross_intersection(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries( + [MultiPoint([(0.0, 1.0), (0.5, 0.5), (1.0, 0.0)])] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="NotImplemented. Depends on allpairs_multipoint_equals_count" +) +def test_linestring_intersects_multipoint_implicit_cross_intersection(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([MultiPoint([(0.0, 1.0), (1.0, 0.0)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="NotImplemented. Depends on allpairs_multipoint_equals_count" +) +def test_100_linestrings_intersects_100_multipoints( + linestring_generator, multipoint_generator +): + g1 = cuspatial.GeoSeries([*linestring_generator(15, 4)]) + g2 = cuspatial.GeoSeries([*multipoint_generator(15, 4)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_linestring_crosses(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([LineString([(0.0, 1.0), (1.0, 0.0)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestering_intersects_linestring_parallel(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (0.0, 1.0)])]) + g2 = cuspatial.GeoSeries([LineString([(1.0, 0.0), (1.0, 1.0)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_linestring_overlaps(): + g1 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (1.0, 1.0)])]) + g2 = cuspatial.GeoSeries([LineString([(0.0, 0.0), (0.5, 0.5)])]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_two_linestrings_intersects_two_linestrings_parallel(): + g1 = cuspatial.GeoSeries( + [ + LineString([(1.0, 1.0), (1.0, 2.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(2.0, 0.0), (2.0, 2.0)]), + LineString([(1.0, 0.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_two_linestrings_intersects_two_linestrings_overlaps(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (1.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.5, 0.5)]), + LineString([(0.5, 0.5), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_two_linestrings_intersects_two_linestrings_touches(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (1.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(1.0, 1.0), (2.0, 2.0)]), + LineString([(1.0, 0.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_two_linestrings_intersects_two_linestrings_single(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 1.0), (1.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_two_linestrings_intersects_two_linestrings_single_reversed(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_three_linestrings_middle(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_three_linestrings_intersects_three_linestrings_not_middle(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 1.0), (1.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 0.0), (0.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_100_linestrings_intersects_100_linestrings(linestring_generator): + g1 = cuspatial.GeoSeries([*linestring_generator(100, 5)]) + g2 = cuspatial.GeoSeries([*linestring_generator(100, 5)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_100_linestrings_intersects_100_multilinestrings( + linestring_generator, multilinestring_generator +): + g1 = cuspatial.GeoSeries([*linestring_generator(100, 5)]) + g2 = cuspatial.GeoSeries([*multilinestring_generator(100, 5, 5)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_100_multilinestrings_intersects_100_linestrings( + linestring_generator, multilinestring_generator +): + g1 = cuspatial.GeoSeries([*multilinestring_generator(100, 5, 5)]) + g2 = cuspatial.GeoSeries([*linestring_generator(100, 5)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_100_multilinestrings_intersects_100_multilinestrings( + multilinestring_generator, +): + g1 = cuspatial.GeoSeries([*multilinestring_generator(100, 5, 5)]) + g2 = cuspatial.GeoSeries([*multilinestring_generator(100, 5, 5)]) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_intersects_multilinestring(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiLineString( + [ + [(0.0, 0.0), (0.0, 1.0)], + [(0.0, 1.0), (1.0, 1.0)], + ] + ), + MultiLineString( + [ + [(0.0, 0.0), (0.0, 1.0)], + [(0.0, 1.0), (1.0, 1.0)], + ] + ), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_multilinestring_intersects_linestring(): + g1 = cuspatial.GeoSeries( + [ + MultiLineString( + [ + [(0.0, 0.0), (0.0, 1.0)], + [(0.0, 1.0), (1.0, 1.0)], + ] + ), + MultiLineString( + [ + [(0.0, 0.0), (0.0, 1.0)], + [(0.0, 1.0), (1.0, 1.0)], + ] + ), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_linestring_intersects_polygon(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_polygon_intersects_linestring(): + g1 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_multipolygon_intersects_linestring(): + g1 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_linestring_intersects_multipolygon(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_polygon_intersects_multipolygon(): + g1 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_multipolygon_intersects_polygon(): + g1 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +@pytest.mark.skip( + reason="""NotImplemented. Depends on a a combination +of intersects and contains.""" +) +def test_multipolygon_intersects_multipolygon(): + g1 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + g2 = cuspatial.GeoSeries( + [ + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + MultiPolygon( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.intersects(g2) + expected = gpdg1.intersects(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_point_disjoint_linestring(): + g1 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.disjoint(g2) + expected = gpdg1.disjoint(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_disjoint_point(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.disjoint(g2) + expected = gpdg1.disjoint(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_disjoint_linestring(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.disjoint(g2) + expected = gpdg1.disjoint(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_contains_point(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.contains_properly(g2) + expected = gpdg1.contains(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_covers_point(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.covers(g2) + expected = gpdg1.covers(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_crosses_point(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.crosses(g2) + expected = gpdg1.crosses(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def linestring_crosses_linestring(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.crosses(g2) + expected = gpdg1.crosses(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def linestring_crosses_polygon(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.crosses(g2) + expected = gpdg1.crosses(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_overlaps_point(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Point(0.0, 0.0), + Point(0.0, 0.0), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.overlaps(g2) + expected = gpdg1.overlaps(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_overlaps_linestring(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + LineString([(0.0, 1.0), (1.0, 1.0)]), + LineString([(0.0, 1.0), (1.0, 1.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.overlaps(g2) + expected = gpdg1.overlaps(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas()) + + +def test_linestring_overlaps_polygon(): + g1 = cuspatial.GeoSeries( + [ + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + g2 = cuspatial.GeoSeries( + [ + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]), + ] + ) + gpdg1 = g1.to_geopandas() + gpdg2 = g2.to_geopandas() + got = g1.overlaps(g2) + expected = gpdg1.overlaps(gpdg2) + pd.testing.assert_series_equal(expected, got.to_pandas())