1"""
2Testing functionality for geopandas objects.
3"""
4import warnings
5
6import pandas as pd
7
8from geopandas import GeoDataFrame, GeoSeries
9from geopandas.array import GeometryDtype
10from geopandas import _vectorized
11
12
13def _isna(this):
14    """isna version that works for both scalars and (Geo)Series"""
15    with warnings.catch_warnings():
16        # GeoSeries.isna will raise a warning about no longer returning True
17        # for empty geometries. This helper is used below always in combination
18        # with an is_empty check to preserve behaviour, and thus we ignore the
19        # warning here to avoid it bubbling up to the user
20        warnings.filterwarnings(
21            "ignore", r"GeoSeries.isna\(\) previously returned", UserWarning
22        )
23        if hasattr(this, "isna"):
24            return this.isna()
25        elif hasattr(this, "isnull"):
26            return this.isnull()
27        else:
28            return pd.isnull(this)
29
30
31def _geom_equals_mask(this, that):
32    """
33    Test for geometric equality. Empty or missing geometries are considered
34    equal.
35
36    Parameters
37    ----------
38    this, that : arrays of Geo objects (or anything that has an `is_empty`
39                 attribute)
40
41    Returns
42    -------
43    Series
44        boolean Series, True if geometries in left equal geometries in right
45    """
46
47    return (
48        this.geom_equals(that)
49        | (this.is_empty & that.is_empty)
50        | (_isna(this) & _isna(that))
51    )
52
53
54def geom_equals(this, that):
55    """
56    Test for geometric equality. Empty or missing geometries are considered
57    equal.
58
59    Parameters
60    ----------
61    this, that : arrays of Geo objects (or anything that has an `is_empty`
62                 attribute)
63
64    Returns
65    -------
66    bool
67        True if all geometries in left equal geometries in right
68    """
69
70    return _geom_equals_mask(this, that).all()
71
72
73def _geom_almost_equals_mask(this, that):
74    """
75    Test for 'almost' geometric equality. Empty or missing geometries
76    considered equal.
77
78    This method allows small difference in the coordinates, but this
79    requires coordinates be in the same order for all components of a geometry.
80
81    Parameters
82    ----------
83    this, that : arrays of Geo objects (or anything that has an `is_empty`
84                 property)
85
86    Returns
87    -------
88    Series
89        boolean Series, True if geometries in left almost equal geometries in right
90    """
91
92    return (
93        this.geom_almost_equals(that)
94        | (this.is_empty & that.is_empty)
95        | (_isna(this) & _isna(that))
96    )
97
98
99def geom_almost_equals(this, that):
100    """
101    Test for 'almost' geometric equality. Empty or missing geometries
102    considered equal.
103
104    This method allows small difference in the coordinates, but this
105    requires coordinates be in the same order for all components of a geometry.
106
107    Parameters
108    ----------
109    this, that : arrays of Geo objects (or anything that has an `is_empty`
110                 property)
111
112    Returns
113    -------
114    bool
115        True if all geometries in left almost equal geometries in right
116    """
117
118    return _geom_almost_equals_mask(this, that).all()
119
120
121def assert_geoseries_equal(
122    left,
123    right,
124    check_dtype=True,
125    check_index_type=False,
126    check_series_type=True,
127    check_less_precise=False,
128    check_geom_type=False,
129    check_crs=True,
130    normalize=False,
131):
132    """
133    Test util for checking that two GeoSeries are equal.
134
135    Parameters
136    ----------
137    left, right : two GeoSeries
138    check_dtype : bool, default False
139        If True, check geo dtype [only included so it's a drop-in replacement
140        for assert_series_equal].
141    check_index_type : bool, default False
142        Check that index types are equal.
143    check_series_type : bool, default True
144        Check that both are same type (*and* are GeoSeries). If False,
145        will attempt to convert both into GeoSeries.
146    check_less_precise : bool, default False
147        If True, use geom_almost_equals. if False, use geom_equals.
148    check_geom_type : bool, default False
149        If True, check that all the geom types are equal.
150    check_crs: bool, default True
151        If `check_series_type` is True, then also check that the
152        crs matches.
153    normalize: bool, default False
154        If True, normalize the geometries before comparing equality.
155        Typically useful with ``check_less_precise=True``, which uses
156        ``geom_almost_equals`` and requires exact coordinate order.
157    """
158    assert len(left) == len(right), "%d != %d" % (len(left), len(right))
159
160    if check_dtype:
161        msg = "dtype should be a GeometryDtype, got {0}"
162        assert isinstance(left.dtype, GeometryDtype), msg.format(left.dtype)
163        assert isinstance(right.dtype, GeometryDtype), msg.format(left.dtype)
164
165    if check_index_type:
166        assert isinstance(left.index, type(right.index))
167
168    if check_series_type:
169        assert isinstance(left, GeoSeries)
170        assert isinstance(left, type(right))
171
172        if check_crs:
173            assert left.crs == right.crs
174    else:
175        if not isinstance(left, GeoSeries):
176            left = GeoSeries(left)
177        if not isinstance(right, GeoSeries):
178            right = GeoSeries(right, index=left.index)
179
180    assert left.index.equals(right.index), "index: %s != %s" % (left.index, right.index)
181
182    if check_geom_type:
183        assert (left.type == right.type).all(), "type: %s != %s" % (
184            left.type,
185            right.type,
186        )
187
188    if normalize:
189        left = GeoSeries(_vectorized.normalize(left.array.data))
190        right = GeoSeries(_vectorized.normalize(right.array.data))
191
192    if not check_crs:
193        with warnings.catch_warnings():
194            warnings.filterwarnings("ignore", "CRS mismatch", UserWarning)
195            _check_equality(left, right, check_less_precise)
196    else:
197        _check_equality(left, right, check_less_precise)
198
199
200def _truncated_string(geom):
201    """Truncated WKT repr of geom"""
202    s = str(geom)
203    if len(s) > 100:
204        return s[:100] + "..."
205    else:
206        return s
207
208
209def _check_equality(left, right, check_less_precise):
210    assert_error_message = (
211        "{0} out of {1} geometries are not {3}equal.\n"
212        "Indices where geometries are not {3}equal: {2} \n"
213        "The first not {3}equal geometry:\n"
214        "Left: {4}\n"
215        "Right: {5}\n"
216    )
217    if check_less_precise:
218        precise = "almost "
219        equal = _geom_almost_equals_mask(left, right)
220    else:
221        precise = ""
222        equal = _geom_equals_mask(left, right)
223
224    if not equal.all():
225        unequal_left_geoms = left[~equal]
226        unequal_right_geoms = right[~equal]
227        raise AssertionError(
228            assert_error_message.format(
229                len(unequal_left_geoms),
230                len(left),
231                unequal_left_geoms.index.to_list(),
232                precise,
233                _truncated_string(unequal_left_geoms.iloc[0]),
234                _truncated_string(unequal_right_geoms.iloc[0]),
235            )
236        )
237
238
239def assert_geodataframe_equal(
240    left,
241    right,
242    check_dtype=True,
243    check_index_type="equiv",
244    check_column_type="equiv",
245    check_frame_type=True,
246    check_like=False,
247    check_less_precise=False,
248    check_geom_type=False,
249    check_crs=True,
250    normalize=False,
251):
252    """
253    Check that two GeoDataFrames are equal/
254
255    Parameters
256    ----------
257    left, right : two GeoDataFrames
258    check_dtype : bool, default True
259        Whether to check the DataFrame dtype is identical.
260    check_index_type, check_column_type : bool, default 'equiv'
261        Check that index types are equal.
262    check_frame_type : bool, default True
263        Check that both are same type (*and* are GeoDataFrames). If False,
264        will attempt to convert both into GeoDataFrame.
265    check_like : bool, default False
266        If true, ignore the order of rows & columns
267    check_less_precise : bool, default False
268        If True, use geom_almost_equals. if False, use geom_equals.
269    check_geom_type : bool, default False
270        If True, check that all the geom types are equal.
271    check_crs: bool, default True
272        If `check_frame_type` is True, then also check that the
273        crs matches.
274    normalize: bool, default False
275        If True, normalize the geometries before comparing equality.
276        Typically useful with ``check_less_precise=True``, which uses
277        ``geom_almost_equals`` and requires exact coordinate order.
278    """
279    try:
280        # added from pandas 0.20
281        from pandas.testing import assert_frame_equal, assert_index_equal
282    except ImportError:
283        from pandas.util.testing import assert_frame_equal, assert_index_equal
284
285    # instance validation
286    if check_frame_type:
287        assert isinstance(left, GeoDataFrame)
288        assert isinstance(left, type(right))
289
290        if check_crs:
291            # no crs can be either None or {}
292            if not left.crs and not right.crs:
293                pass
294            else:
295                assert left.crs == right.crs
296    else:
297        if not isinstance(left, GeoDataFrame):
298            left = GeoDataFrame(left)
299        if not isinstance(right, GeoDataFrame):
300            right = GeoDataFrame(right)
301
302    # shape comparison
303    assert left.shape == right.shape, (
304        "GeoDataFrame shape mismatch, left: {lshape!r}, right: {rshape!r}.\n"
305        "Left columns: {lcols!r}, right columns: {rcols!r}"
306    ).format(
307        lshape=left.shape, rshape=right.shape, lcols=left.columns, rcols=right.columns
308    )
309
310    if check_like:
311        left, right = left.reindex_like(right), right
312
313    # column comparison
314    assert_index_equal(
315        left.columns, right.columns, exact=check_column_type, obj="GeoDataFrame.columns"
316    )
317
318    # geometry comparison
319    for col, dtype in left.dtypes.iteritems():
320        if isinstance(dtype, GeometryDtype):
321            assert_geoseries_equal(
322                left[col],
323                right[col],
324                normalize=normalize,
325                check_dtype=check_dtype,
326                check_less_precise=check_less_precise,
327                check_geom_type=check_geom_type,
328                check_crs=check_crs,
329            )
330
331    # drop geometries and check remaining columns
332    left2 = left.drop([left._geometry_column_name], axis=1)
333    right2 = right.drop([right._geometry_column_name], axis=1)
334    assert_frame_equal(
335        left2,
336        right2,
337        check_dtype=check_dtype,
338        check_index_type=check_index_type,
339        check_column_type=check_column_type,
340        obj="GeoDataFrame",
341    )
342