461 lines
15 KiB
Python
461 lines
15 KiB
Python
"""Polygons and Linear Rings"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from shapely import LinearRing, LineString, Point, Polygon
|
|
from shapely.coords import CoordinateSequence
|
|
from shapely.errors import TopologicalError
|
|
from shapely.wkb import loads as load_wkb
|
|
|
|
|
|
def test_empty_linearring_coords():
|
|
assert LinearRing().coords[:] == []
|
|
|
|
|
|
def test_linearring_from_coordinate_sequence():
|
|
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
assert ring.coords[:] == expected_coords
|
|
|
|
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
assert ring.coords[:] == expected_coords
|
|
|
|
|
|
def test_linearring_from_points():
|
|
# From Points
|
|
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
ring = LinearRing([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)])
|
|
assert ring.coords[:] == expected_coords
|
|
|
|
|
|
def test_linearring_from_closed_linestring():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
line = LineString(coords)
|
|
ring = LinearRing(line)
|
|
assert len(ring.coords) == 4
|
|
assert ring.coords[:] == coords
|
|
assert ring.geom_type == "LinearRing"
|
|
|
|
|
|
def test_linearring_from_unclosed_linestring():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
line = LineString(coords[:-1]) # Pass in unclosed line
|
|
ring = LinearRing(line)
|
|
assert len(ring.coords) == 4
|
|
assert ring.coords[:] == coords
|
|
assert ring.geom_type == "LinearRing"
|
|
|
|
|
|
def test_linearring_from_invalid():
|
|
coords = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
|
|
line = LineString(coords)
|
|
assert not line.is_valid
|
|
with pytest.raises(TopologicalError):
|
|
LinearRing(line)
|
|
|
|
|
|
def test_linearring_from_too_short_linestring():
|
|
# Creation of LinearRing request at least 3 coordinates (unclosed) or
|
|
# 4 coordinates (closed)
|
|
coords = [(0.0, 0.0), (1.0, 1.0)]
|
|
line = LineString(coords)
|
|
with pytest.raises(ValueError, match="requires at least 4 coordinates"):
|
|
LinearRing(line)
|
|
|
|
|
|
def test_linearring_from_linearring():
|
|
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
ring = LinearRing(coords)
|
|
assert ring.coords[:] == coords
|
|
|
|
|
|
def test_linearring_from_generator():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
gen = (coord for coord in coords)
|
|
ring = LinearRing(gen)
|
|
assert ring.coords[:] == coords
|
|
|
|
|
|
def test_linearring_from_empty():
|
|
ring = LinearRing()
|
|
assert ring.is_empty
|
|
assert isinstance(ring.coords, CoordinateSequence)
|
|
assert ring.coords[:] == []
|
|
|
|
ring = LinearRing([])
|
|
assert ring.is_empty
|
|
assert isinstance(ring.coords, CoordinateSequence)
|
|
assert ring.coords[:] == []
|
|
|
|
|
|
def test_linearring_from_numpy():
|
|
# Construct from a numpy array
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
ring = LinearRing(np.array(coords))
|
|
assert ring.coords[:] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
|
|
def test_numpy_linearring_coords():
|
|
from numpy.testing import assert_array_equal
|
|
|
|
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
ra = np.asarray(ring.coords)
|
|
expected = np.asarray([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)])
|
|
assert_array_equal(ra, expected)
|
|
|
|
|
|
def test_numpy_empty_linearring_coords():
|
|
ring = LinearRing()
|
|
assert np.asarray(ring.coords).shape == (0, 2)
|
|
|
|
|
|
def test_numpy_object_array():
|
|
geom = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
ar = np.empty(1, object)
|
|
ar[:] = [geom]
|
|
assert ar[0] == geom
|
|
|
|
|
|
def test_polygon_from_coordinate_sequence():
|
|
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
# Construct a polygon, exterior ring only
|
|
polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
assert polygon.exterior.coords[:] == coords
|
|
assert len(polygon.interiors) == 0
|
|
|
|
polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
|
|
assert polygon.exterior.coords[:] == coords
|
|
assert len(polygon.interiors) == 0
|
|
|
|
|
|
def test_polygon_from_coordinate_sequence_with_holes():
|
|
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
|
|
# Interior rings (holes)
|
|
polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]])
|
|
assert polygon.exterior.coords[:] == coords
|
|
assert len(polygon.interiors) == 1
|
|
assert len(polygon.interiors[0].coords) == 5
|
|
|
|
# Multiple interior rings with different length
|
|
coords = [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]
|
|
holes = [
|
|
[(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)],
|
|
[(3, 3), (3, 4), (4, 5), (5, 4), (5, 3), (3, 3)],
|
|
]
|
|
polygon = Polygon(coords, holes)
|
|
assert polygon.exterior.coords[:] == coords
|
|
assert len(polygon.interiors) == 2
|
|
assert len(polygon.interiors[0].coords) == 5
|
|
assert len(polygon.interiors[1].coords) == 6
|
|
|
|
|
|
def test_polygon_from_linearring():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
ring = LinearRing(coords)
|
|
|
|
polygon = Polygon(ring)
|
|
assert polygon.exterior.coords[:] == coords
|
|
assert len(polygon.interiors) == 0
|
|
|
|
# from shell and holes linearrings
|
|
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
|
|
holes = [
|
|
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
|
|
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
|
|
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
|
|
]
|
|
polygon = Polygon(shell, holes)
|
|
assert polygon.exterior.coords[:] == shell.coords[:]
|
|
assert len(polygon.interiors) == 3
|
|
for i in range(3):
|
|
assert polygon.interiors[i].coords[:] == holes[i].coords[:]
|
|
|
|
|
|
def test_polygon_from_linestring():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
line = LineString(coords)
|
|
polygon = Polygon(line)
|
|
assert polygon.exterior.coords[:] == coords
|
|
|
|
# from unclosed linestring
|
|
line = LineString(coords[:-1])
|
|
polygon = Polygon(line)
|
|
assert polygon.exterior.coords[:] == coords
|
|
|
|
|
|
def test_polygon_from_points():
|
|
polygon = Polygon([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)])
|
|
expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]
|
|
assert polygon.exterior.coords[:] == expected_coords
|
|
|
|
|
|
def test_polygon_from_polygon():
|
|
coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]
|
|
polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]])
|
|
|
|
# Test from another Polygon
|
|
copy = Polygon(polygon)
|
|
assert len(copy.exterior.coords) == 5
|
|
assert len(copy.interiors) == 1
|
|
assert len(copy.interiors[0].coords) == 5
|
|
|
|
|
|
def test_polygon_from_invalid():
|
|
# Error handling
|
|
with pytest.raises(ValueError):
|
|
# A LinearRing must have at least 3 coordinate tuples
|
|
Polygon([[1, 2], [2, 3]])
|
|
|
|
|
|
def test_polygon_from_empty():
|
|
polygon = Polygon()
|
|
assert polygon.is_empty
|
|
assert polygon.exterior.coords[:] == []
|
|
|
|
polygon = Polygon([])
|
|
assert polygon.is_empty
|
|
assert polygon.exterior.coords[:] == []
|
|
|
|
|
|
def test_polygon_from_numpy():
|
|
a = np.array(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)))
|
|
polygon = Polygon(a)
|
|
assert len(polygon.exterior.coords) == 5
|
|
assert polygon.exterior.coords[:] == [
|
|
(0.0, 0.0),
|
|
(0.0, 1.0),
|
|
(1.0, 1.0),
|
|
(1.0, 0.0),
|
|
(0.0, 0.0),
|
|
]
|
|
assert len(polygon.interiors) == 0
|
|
|
|
|
|
def test_polygon_from_generator():
|
|
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
|
|
gen = (coord for coord in coords)
|
|
polygon = Polygon(gen)
|
|
assert polygon.exterior.coords[:] == coords
|
|
|
|
|
|
class TestPolygon:
|
|
def test_linearring(self):
|
|
# Initialization
|
|
# Linear rings won't usually be created by users, but by polygons
|
|
coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
|
|
ring = LinearRing(coords)
|
|
assert len(ring.coords) == 5
|
|
assert ring.coords[0] == ring.coords[4]
|
|
assert ring.coords[0] == ring.coords[-1]
|
|
assert ring.is_ring is True
|
|
|
|
def test_polygon(self):
|
|
coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
|
|
|
|
# Construct a polygon, exterior ring only
|
|
polygon = Polygon(coords)
|
|
assert len(polygon.exterior.coords) == 5
|
|
|
|
# Ring Access
|
|
assert isinstance(polygon.exterior, LinearRing)
|
|
ring = polygon.exterior
|
|
assert len(ring.coords) == 5
|
|
assert ring.coords[0] == ring.coords[4]
|
|
assert ring.coords[0] == (0.0, 0.0)
|
|
assert ring.is_ring is True
|
|
assert len(polygon.interiors) == 0
|
|
|
|
# Create a new polygon from WKB
|
|
data = polygon.wkb
|
|
polygon = None
|
|
ring = None
|
|
polygon = load_wkb(data)
|
|
ring = polygon.exterior
|
|
assert len(ring.coords) == 5
|
|
assert ring.coords[0] == ring.coords[4]
|
|
assert ring.coords[0] == (0.0, 0.0)
|
|
assert ring.is_ring is True
|
|
polygon = None
|
|
|
|
# Interior rings (holes)
|
|
polygon = Polygon(
|
|
coords, [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))]
|
|
)
|
|
assert len(polygon.exterior.coords) == 5
|
|
assert len(polygon.interiors[0].coords) == 5
|
|
with pytest.raises(IndexError): # index out of range
|
|
polygon.interiors[1]
|
|
|
|
# Coordinate getter raises exceptions
|
|
with pytest.raises(NotImplementedError):
|
|
polygon.coords
|
|
|
|
# Geo interface
|
|
assert polygon.__geo_interface__ == {
|
|
"type": "Polygon",
|
|
"coordinates": (
|
|
((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),
|
|
((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)),
|
|
),
|
|
}
|
|
|
|
def test_linearring_empty(self):
|
|
# Test Non-operability of Null rings
|
|
r_null = LinearRing()
|
|
assert r_null.wkt == "LINEARRING EMPTY"
|
|
assert r_null.length == 0.0
|
|
|
|
def test_dimensions(self):
|
|
# Background: see http://trac.gispython.org/lab/ticket/168
|
|
# http://lists.gispython.org/pipermail/community/2008-August/001859.html
|
|
|
|
coords = ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0))
|
|
polygon = Polygon(coords)
|
|
assert polygon._ndim == 3
|
|
gi = polygon.__geo_interface__
|
|
assert gi["coordinates"] == (
|
|
(
|
|
(0.0, 0.0, 0.0),
|
|
(0.0, 1.0, 0.0),
|
|
(1.0, 1.0, 0.0),
|
|
(1.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
),
|
|
)
|
|
|
|
e = polygon.exterior
|
|
assert e._ndim == 3
|
|
gi = e.__geo_interface__
|
|
assert gi["coordinates"] == (
|
|
(0.0, 0.0, 0.0),
|
|
(0.0, 1.0, 0.0),
|
|
(1.0, 1.0, 0.0),
|
|
(1.0, 0.0, 0.0),
|
|
(0.0, 0.0, 0.0),
|
|
)
|
|
|
|
def test_attribute_chains(self):
|
|
# Attribute Chaining
|
|
# See also ticket #151.
|
|
p = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
|
|
assert list(p.boundary.coords) == [
|
|
(0.0, 0.0),
|
|
(0.0, 1.0),
|
|
(-1.0, 1.0),
|
|
(-1.0, 0.0),
|
|
(0.0, 0.0),
|
|
]
|
|
|
|
ec = list(Point(0.0, 0.0).buffer(1.0, quad_segs=1).exterior.coords)
|
|
assert isinstance(ec, list) # TODO: this is a poor test
|
|
|
|
# Test chained access to interiors
|
|
p = Polygon(
|
|
[(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)],
|
|
[[(-0.25, 0.25), (-0.25, 0.75), (-0.75, 0.75), (-0.75, 0.25)]],
|
|
)
|
|
assert p.area == 0.75
|
|
|
|
"""Not so much testing the exact values here, which are the
|
|
responsibility of the geometry engine (GEOS), but that we can get
|
|
chain functions and properties using anonymous references.
|
|
"""
|
|
assert list(p.interiors[0].coords) == [
|
|
(-0.25, 0.25),
|
|
(-0.25, 0.75),
|
|
(-0.75, 0.75),
|
|
(-0.75, 0.25),
|
|
(-0.25, 0.25),
|
|
]
|
|
xy = next(iter(p.interiors[0].buffer(1).exterior.coords))
|
|
assert len(xy) == 2
|
|
|
|
# Test multiple operators, boundary of a buffer
|
|
ec = list(p.buffer(1).boundary.coords)
|
|
assert isinstance(ec, list) # TODO: this is a poor test
|
|
|
|
def test_empty_equality(self):
|
|
# Test equals operator, including empty geometries
|
|
# see issue #338
|
|
|
|
point1 = Point(0, 0)
|
|
polygon1 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
|
|
polygon2 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)])
|
|
polygon_empty1 = Polygon()
|
|
polygon_empty2 = Polygon()
|
|
|
|
assert point1 != polygon1
|
|
assert polygon_empty1 == polygon_empty2
|
|
assert polygon1 != polygon_empty1
|
|
assert polygon1 == polygon2
|
|
assert polygon_empty1 is not None
|
|
|
|
def test_from_bounds(self):
|
|
xmin, ymin, xmax, ymax = -180, -90, 180, 90
|
|
coords = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
|
|
assert Polygon(coords) == Polygon.from_bounds(xmin, ymin, xmax, ymax)
|
|
|
|
def test_empty_polygon_exterior(self):
|
|
p = Polygon()
|
|
assert p.exterior == LinearRing()
|
|
|
|
|
|
def test_linearring_immutable():
|
|
ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
|
|
|
|
with pytest.raises(AttributeError):
|
|
ring.coords = [(1.0, 1.0), (2.0, 2.0), (1.0, 2.0)]
|
|
|
|
with pytest.raises(TypeError):
|
|
ring.coords[0] = (1.0, 1.0)
|
|
|
|
|
|
class TestLinearRingGetItem:
|
|
def test_index_linearring(self):
|
|
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
|
|
holes = [
|
|
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
|
|
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
|
|
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
|
|
]
|
|
g = Polygon(shell, holes)
|
|
for i in range(-3, 3):
|
|
assert g.interiors[i].equals(holes[i])
|
|
with pytest.raises(IndexError):
|
|
g.interiors[3]
|
|
with pytest.raises(IndexError):
|
|
g.interiors[-4]
|
|
|
|
def test_index_linearring_misc(self):
|
|
g = Polygon() # empty
|
|
with pytest.raises(IndexError):
|
|
g.interiors[0]
|
|
with pytest.raises(TypeError):
|
|
g.interiors[0.0]
|
|
|
|
def test_slice_linearring(self):
|
|
shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)])
|
|
holes = [
|
|
LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]),
|
|
LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]),
|
|
LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]),
|
|
]
|
|
g = Polygon(shell, holes)
|
|
t = [a.equals(b) for (a, b) in zip(g.interiors[1:], holes[1:])]
|
|
assert all(t)
|
|
t = [a.equals(b) for (a, b) in zip(g.interiors[:-1], holes[:-1])]
|
|
assert all(t)
|
|
t = [a.equals(b) for (a, b) in zip(g.interiors[::-1], holes[::-1])]
|
|
assert all(t)
|
|
t = [a.equals(b) for (a, b) in zip(g.interiors[::2], holes[::2])]
|
|
assert all(t)
|
|
t = [a.equals(b) for (a, b) in zip(g.interiors[:3], holes[:3])]
|
|
assert all(t)
|
|
assert g.interiors[3:] == holes[3:] == []
|