blob: f3c9903cb0e006b253a2388a708fa0b768c68f97 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import pytest
import shapely
from packaging.version import parse as parse_version
from shapely.geometry import (
GeometryCollection,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
)
from shapely.geometry.base import BaseGeometry
from shapely.wkt import loads as wkt_loads
from sedona.spark.utils import geometry_serde
from sedona.spark.utils import geometry_serde_general
class TestGeomSerdeSpeedup:
def test_speedup_enabled(self):
assert geometry_serde.speedup_enabled
def test_point(self):
points = [wkt_loads("POINT EMPTY"), Point(10, 20), Point(10, 20, 30)]
self._test_serde_roundtrip(points)
def test_linestring(self):
linestrings = [
wkt_loads("LINESTRING EMPTY"),
LineString([(10, 20), (30, 40)]),
LineString([(10, 20), (30, 40), (50, 60)]),
LineString([(10, 20, 30), (30, 40, 50), (50, 60, 70)]),
]
self._test_serde_roundtrip(linestrings)
def test_nan_first_z_serialization_keeps_dimension(self):
geometry = wkt_loads("LINESTRING Z (0 0 NaN, 1 1 2)")
buffer = geometry_serde.serialize(geometry)
coordinate_type = (buffer[0] & 0x0F) >> 1
assert coordinate_type == geometry_serde_general.CoordinateType.XYZ
def test_multi_point(self):
multi_points = [
wkt_loads("MULTIPOINT EMPTY"),
MultiPoint([(10, 20)]),
MultiPoint([(10, 20), (30, 40)]),
MultiPoint([(10, 20), (30, 40), (50, 60)]),
MultiPoint([(10, 20, 30), (30, 40, 50), (50, 60, 70)]),
]
self._test_serde_roundtrip(multi_points)
def test_multi_linestring(self):
multi_linestrings = [
wkt_loads("MULTILINESTRING EMPTY"),
MultiLineString([[(10, 20), (30, 40)]]),
MultiLineString([[(10, 20), (30, 40)], [(50, 60), (70, 80)]]),
MultiLineString(
[[(10, 20, 30), (30, 40, 50)], [(50, 60, 70), (70, 80, 90)]]
),
]
self._test_serde_roundtrip(multi_linestrings)
def test_polygon(self):
ext = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]
int0 = [(1, 1), (1, 1.5), (1.5, 1.5), (1.5, 1), (1, 1)]
int1 = [(2, 2), (2, 2.5), (2.5, 2.5), (2.5, 2), (2, 2)]
polygons = [
wkt_loads("POLYGON EMPTY"),
Polygon(ext),
Polygon(ext, [int0]),
Polygon(ext, [int0, int1]),
]
self._test_serde_roundtrip(polygons)
def test_multi_polygon(self):
ext = [(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)]
int0 = [(10, 10), (10, 15), (15, 15), (15, 10), (10, 10)]
int1 = [(2, 2), (2, 2.5), (2.5, 2.5), (2.5, 2), (2, 2)]
multi_polygons = [
wkt_loads("MULTIPOLYGON EMPTY"),
MultiPolygon([Polygon(ext)]),
MultiPolygon([Polygon(ext), Polygon(ext, [int0])]),
MultiPolygon([Polygon(ext), Polygon(ext, [int0, int1])]),
MultiPolygon(
[Polygon(ext, [int1]), Polygon(ext), Polygon(ext, [int0, int1])]
),
]
self._test_serde_roundtrip(multi_polygons)
def test_geometry_collection(self):
geometry_collections = [
wkt_loads("GEOMETRYCOLLECTION EMPTY"),
GeometryCollection(
[Point(10, 20), LineString([(10, 20), (30, 40)]), Point(30, 40)]
),
GeometryCollection(
[
MultiPoint([(10, 20), (30, 40)]),
MultiLineString([[(10, 20), (30, 40)], [(50, 60), (70, 80)]]),
MultiPolygon(
[
Polygon(
[(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)],
[[(10, 10), (10, 15), (15, 15), (15, 10), (10, 10)]],
)
]
),
Point(100, 200),
]
),
GeometryCollection(
[
GeometryCollection(
[Point(10, 20), LineString([(10, 20), (30, 40)]), Point(30, 40)]
),
GeometryCollection(
[
MultiPoint([(10, 20), (30, 40)]),
MultiLineString(
[[(10, 20), (30, 40)], [(50, 60), (70, 80)]]
),
Point(10, 20),
]
),
]
),
]
self._test_serde_roundtrip(geometry_collections)
@pytest.mark.skipif(
shapely.__version__ < "2", reason="SRID functions require Shapely >= 2.0"
)
def test_srid_roundtrip(self):
point = wkt_loads("POINT (1 2)")
point = shapely.set_srid(point, 1000)
point2 = TestGeomSerdeSpeedup.serde_roundtrip(point)
assert shapely.get_srid(point2) == 1000
@pytest.mark.skipif(
parse_version(shapely.__version__) < parse_version("2.1")
or getattr(shapely, "geos_version", (0, 0, 0)) < (3, 12, 0),
reason="M coordinates require Shapely 2.1 and GEOS 3.12 or newer",
)
@pytest.mark.parametrize(
"wkt",
[
"POINT M (1 2 3)",
"POINT ZM (1 2 3 4)",
"LINESTRING M (0 0 1, 2 3 4)",
"LINESTRING ZM (0 0 1 2, 3 4 5 6)",
"POLYGON M ((0 0 1, 2 0 2, 0 2 3, 0 0 1))",
"GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4), "
"LINESTRING ZM (0 0 1 2, 3 4 5 6))",
],
)
def test_m_roundtrip(self, wkt):
geometry = shapely.from_wkt(wkt)
actual = TestGeomSerdeSpeedup.serde_roundtrip(geometry)
assert shapely.to_wkt(actual) == shapely.to_wkt(geometry)
assert actual.has_z == geometry.has_z
assert actual.has_m == geometry.has_m
@pytest.mark.skipif(
parse_version(shapely.__version__) < parse_version("2.1")
or getattr(shapely, "geos_version", (0, 0, 0)) < (3, 12, 0),
reason="M coordinates require Shapely 2.1 and GEOS 3.12 or newer",
)
@pytest.mark.parametrize("wkt", ["POINT M (1 2 3)", "POINT ZM (1 2 3 4)"])
def test_general_serializer_rejects_m_instead_of_losing_it(self, wkt):
geometry = shapely.from_wkt(wkt)
with pytest.raises(ValueError, match="requires geomserde_speedup"):
geometry_serde_general.serialize(geometry)
@pytest.mark.parametrize(
"coord_type",
[
geometry_serde_general.CoordinateType.XYM,
geometry_serde_general.CoordinateType.XYZM,
],
)
def test_general_deserializer_rejects_m_instead_of_losing_it(self, coord_type):
buffer = geometry_serde_general.create_buffer_for_geom(
geometry_serde_general.GeometryTypeID.POINT,
coord_type,
8 + geometry_serde_general.CoordinateType.bytes_per_coord(coord_type),
1,
)
with pytest.raises(ValueError, match="requires geomserde_speedup"):
geometry_serde_general.deserialize(buffer)
def test_general_serializer_does_not_query_m_on_older_geos(self, monkeypatch):
def fail_if_queried(_geometry):
raise AssertionError("has_m should not be queried with GEOS < 3.12")
monkeypatch.setattr(shapely, "geos_version", (3, 11, 0), raising=False)
monkeypatch.setattr(
BaseGeometry, "has_m", property(fail_if_queried), raising=False
)
monkeypatch.setattr(
geometry_serde_general, "serialize_point", lambda _geometry: b"xy"
)
buffer = geometry_serde_general.serialize(Point(1, 2))
assert buffer == b"xy"
@staticmethod
def _test_serde_roundtrip(geoms):
for geom in geoms:
geom_actual = TestGeomSerdeSpeedup.serde_roundtrip(geom)
assert geom_actual.equals_exact(geom, 1e-6)
# GEOSGeom_createEmptyLineString in libgeos creates LineString with
# Z dimension, This bug has been fixed by
# https://github.com/libgeos/geos/pull/745
geom_actual_wkt = geom_actual.wkt.replace(
"LINESTRING Z EMPTY", "LINESTRING EMPTY"
)
assert geom.wkt == geom_actual_wkt
@staticmethod
def serde_roundtrip(geom: BaseGeometry) -> BaseGeometry:
buffer = geometry_serde.serialize(geom)
geom2, offset = geometry_serde.deserialize(buffer)
return geom2