"""Polygons and their linear ring components
"""
import sys
if sys.version_info[0] < 3:
range = xrange
from ctypes import c_void_p, cast, POINTER
import weakref
from shapely.algorithms.cga import signed_area
from shapely.geos import lgeos
from shapely.geometry.base import BaseGeometry, geos_geom_from_py
from shapely.geometry.linestring import LineString, LineStringAdapter
from shapely.geometry.point import Point
from shapely.geometry.proxy import PolygonProxy
from shapely.errors import TopologicalError
__all__ = ['Polygon', 'asPolygon', 'LinearRing', 'asLinearRing']
class LinearRing(LineString):
"""
A closed one-dimensional feature comprising one or more line segments
A LinearRing that crosses itself or touches itself at a single point is
invalid and operations on it may fail.
"""
def __init__(self, coordinates=None):
"""
Parameters
----------
coordinates : sequence
A sequence of (x, y [,z]) numeric coordinate pairs or triples.
Also can be a sequence of Point objects.
Rings are implicitly closed. There is no need to specific a final
coordinate pair identical to the first.
Example
-------
Construct a square ring.
>>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) )
>>> ring.is_closed
True
>>> ring.length
4.0
"""
BaseGeometry.__init__(self)
if coordinates is not None:
self._set_coords(coordinates)
@property
def __geo_interface__(self):
return {
'type': 'LinearRing',
'coordinates': tuple(self.coords)
}
# Coordinate access
_get_coords = BaseGeometry._get_coords
def _set_coords(self, coordinates):
self.empty()
ret = geos_linearring_from_py(coordinates)
if ret is not None:
self._geom, self._ndim = ret
coords = property(_get_coords, _set_coords)
def __setstate__(self, state):
"""WKB doesn't differentiate between LineString and LinearRing so we
need to move the coordinate sequence into the correct geometry type"""
super(LinearRing, self).__setstate__(state)
cs = lgeos.GEOSGeom_getCoordSeq(self.__geom__)
cs_clone = lgeos.GEOSCoordSeq_clone(cs)
lgeos.GEOSGeom_destroy(self.__geom__)
self.__geom__ = lgeos.GEOSGeom_createLinearRing(cs_clone)
@property
def is_ccw(self):
"""True is the ring is oriented counter clock-wise"""
return bool(self.impl['is_ccw'](self))
@property
def is_simple(self):
"""True if the geometry is simple, meaning that any self-intersections
are only at boundary points, else False"""
return LineString(self).is_simple
class LinearRingAdapter(LineStringAdapter):
__p__ = None
def __init__(self, context):
self.context = context
self.factory = geos_linearring_from_py
@property
def __geo_interface__(self):
return {
'type': 'LinearRing',
'coordinates': tuple(self.coords)
}
coords = property(BaseGeometry._get_coords)
def asLinearRing(context):
"""Adapt an object to the LinearRing interface"""
return LinearRingAdapter(context)
class InteriorRingSequence(object):
_factory = None
_geom = None
__p__ = None
_ndim = None
_index = 0
_length = 0
__rings__ = None
_gtag = None
def __init__(self, parent):
self.__p__ = parent
self._geom = parent._geom
self._ndim = parent._ndim
def __iter__(self):
self._index = 0
self._length = self.__len__()
return self
def __next__(self):
if self._index < self._length:
ring = self._get_ring(self._index)
self._index += 1
return ring
else:
raise StopIteration
if sys.version_info[0] < 3:
next = __next__
def __len__(self):
return lgeos.GEOSGetNumInteriorRings(self._geom)
def __getitem__(self, key):
m = self.__len__()
if isinstance(key, int):
if key + m < 0 or key >= m:
raise IndexError("index out of range")
if key < 0:
i = m + key
else:
i = key
return self._get_ring(i)
elif isinstance(key, slice):
res = []
start, stop, stride = key.indices(m)
for i in range(start, stop, stride):
res.append(self._get_ring(i))
return res
else:
raise TypeError("key must be an index or slice")
@property
def _longest(self):
max = 0
for g in iter(self):
l = len(g.coords)
if l > max:
max = l
def gtag(self):
return hash(repr(self.__p__))
def _get_ring(self, i):
gtag = self.gtag()
if gtag != self._gtag:
self.__rings__ = {}
if i not in self.__rings__:
g = lgeos.GEOSGetInteriorRingN(self._geom, i)
ring = LinearRing()
ring._geom = g
ring.__p__ = self
ring._other_owned = True
ring._ndim = self._ndim
self.__rings__[i] = weakref.ref(ring)
return self.__rings__[i]()
class Polygon(BaseGeometry):
"""
A two-dimensional figure bounded by a linear ring
A polygon has a non-zero area. It may have one or more negative-space
"holes" which are also bounded by linear rings. If any rings cross each
other, the feature is invalid and operations on it may fail.
Attributes
----------
exterior : LinearRing
The ring which bounds the positive space of the polygon.
interiors : sequence
A sequence of rings which bound all existing holes.
"""
_exterior = None
_interiors = []
_ndim = 2
def __init__(self, shell=None, holes=None):
"""
Parameters
----------
shell : sequence
A sequence of (x, y [,z]) numeric coordinate pairs or triples.
Also can be a sequence of Point objects.
holes : sequence
A sequence of objects which satisfy the same requirements as the
shell parameters above
Example
-------
Create a square polygon with no holes
>>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.))
>>> polygon = Polygon(coords)
>>> polygon.area
1.0
"""
BaseGeometry.__init__(self)
if shell is not None:
ret = geos_polygon_from_py(shell, holes)
if ret is not None:
self._geom, self._ndim = ret
else:
self.empty()
@property
def exterior(self):
if self.is_empty:
return LinearRing()
elif self._exterior is None or self._exterior() is None:
g = lgeos.GEOSGetExteriorRing(self._geom)
ring = LinearRing()
ring._geom = g
ring.__p__ = self
ring._other_owned = True
ring._ndim = self._ndim
self._exterior = weakref.ref(ring)
return self._exterior()
@property
def interiors(self):
if self.is_empty:
return []
return InteriorRingSequence(self)
def __eq__(self, other):
if not isinstance(other, Polygon):
return False
check_empty = (self.is_empty, other.is_empty)
if all(check_empty):
return True
elif any(check_empty):
return False
my_coords = [
tuple(self.exterior.coords),
[tuple(interior.coords) for interior in self.interiors]
]
other_coords = [
tuple(other.exterior.coords),
[tuple(interior.coords) for interior in other.interiors]
]
return my_coords == other_coords
def __ne__(self, other):
return not self.__eq__(other)
__hash__ = None
@property
def ctypes(self):
if not self._ctypes_data:
self._ctypes_data = self.exterior.ctypes
return self._ctypes_data
@property
def __array_interface__(self):
raise NotImplementedError(
"A polygon does not itself provide the array interface. Its rings do.")
def _get_coords(self):
raise NotImplementedError(
"Component rings have coordinate sequences, but the polygon does not")
def _set_coords(self, ob):
raise NotImplementedError(
"Component rings have coordinate sequences, but the polygon does not")
@property
def coords(self):
raise NotImplementedError(
"Component rings have coordinate sequences, but the polygon does not")
@property
def __geo_interface__(self):
if self.exterior == LinearRing():
coords = []
else:
coords = [tuple(self.exterior.coords)]
for hole in self.interiors:
coords.append(tuple(hole.coords))
return {
'type': 'Polygon',
'coordinates': tuple(coords)}
def svg(self, scale_factor=1., fill_color=None):
"""Returns SVG path element for the Polygon geometry.
Parameters
==========
scale_factor : float
Multiplication factor for the SVG stroke-width. Default is 1.
fill_color : str, optional
Hex string for fill color. Default is to use "#66cc99" if
geometry is valid, and "#ff3333" if invalid.
"""
if self.is_empty:
return ''
if fill_color is None:
fill_color = "#66cc99" if self.is_valid else "#ff3333"
exterior_coords = [
["{},{}".format(*c) for c in self.exterior.coords]]
interior_coords = [
["{},{}".format(*c) for c in interior.coords]
for interior in self.interiors]
path = " ".join([
"M {} L {} z".format(coords[0], " L ".join(coords[1:]))
for coords in exterior_coords + interior_coords])
return (
''
).format(2. * scale_factor, path, fill_color)
@classmethod
def from_bounds(cls, xmin, ymin, xmax, ymax):
"""Construct a `Polygon()` from spatial bounds."""
return cls([
(xmin, ymin),
(xmin, ymax),
(xmax, ymax),
(xmax, ymin)])
class PolygonAdapter(PolygonProxy, Polygon):
def __init__(self, shell, holes=None):
self.shell = shell
self.holes = holes
self.context = (shell, holes)
self.factory = geos_polygon_from_py
@property
def _ndim(self):
try:
# From array protocol
array = self.shell.__array_interface__
n = array['shape'][1]
assert n == 2 or n == 3
return n
except AttributeError:
# Fall back on list
return len(self.shell[0])
def asPolygon(shell, holes=None):
"""Adapt objects to the Polygon interface"""
return PolygonAdapter(shell, holes)
def orient(polygon, sign=1.0):
s = float(sign)
rings = []
ring = polygon.exterior
if signed_area(ring)/s >= 0.0:
rings.append(ring)
else:
rings.append(list(ring.coords)[::-1])
for ring in polygon.interiors:
if signed_area(ring)/s <= 0.0:
rings.append(ring)
else:
rings.append(list(ring.coords)[::-1])
return Polygon(rings[0], rings[1:])
def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
# If a LinearRing is passed in, clone it and return
# If a valid LineString is passed in, clone the coord seq and return a
# LinearRing.
#
# NB: access to coordinates using the array protocol has been moved
# entirely to the speedups module.
if isinstance(ob, LineString):
if type(ob) == LinearRing:
return geos_geom_from_py(ob)
elif not ob.is_valid:
raise TopologicalError("An input LineString must be valid.")
elif ob.is_closed and len(ob.coords) >= 4:
return geos_geom_from_py(ob, lgeos.GEOSGeom_createLinearRing)
else:
ob = list(ob.coords)
try:
m = len(ob)
except TypeError: # Iterators, e.g. Python 3 zip
ob = list(ob)
m = len(ob)
if m == 0:
return None
def _coords(o):
if isinstance(o, Point):
return o.coords[0]
else:
return o
n = len(_coords(ob[0]))
if m < 3:
raise ValueError(
"A LinearRing must have at least 3 coordinate tuples")
assert (n == 2 or n == 3)
# Add closing coordinates if not provided
if (
m == 3
or _coords(ob[0])[0] != _coords(ob[-1])[0]
or _coords(ob[0])[1] != _coords(ob[-1])[1]
):
M = m + 1
else:
M = m
# Create a coordinate sequence
if update_geom is not None:
if n != update_ndim:
raise ValueError(
"Coordinate dimensions mismatch: target geom has {} dims, "
"update geom has {} dims".format(n, update_ndim))
cs = lgeos.GEOSGeom_getCoordSeq(update_geom)
else:
cs = lgeos.GEOSCoordSeq_create(M, n)
# add to coordinate sequence
for i in range(m):
coords = _coords(ob[i])
# Because of a bug in the GEOS C API,
# always set X before Y
lgeos.GEOSCoordSeq_setX(cs, i, coords[0])
lgeos.GEOSCoordSeq_setY(cs, i, coords[1])
if n == 3:
try:
lgeos.GEOSCoordSeq_setZ(cs, i, coords[2])
except IndexError:
raise ValueError("Inconsistent coordinate dimensionality")
# Add closing coordinates to sequence?
if M > m:
coords = _coords(ob[0])
# Because of a bug in the GEOS C API,
# always set X before Y
lgeos.GEOSCoordSeq_setX(cs, M-1, coords[0])
lgeos.GEOSCoordSeq_setY(cs, M-1, coords[1])
if n == 3:
lgeos.GEOSCoordSeq_setZ(cs, M-1, coords[2])
if update_geom is not None:
return None
else:
return lgeos.GEOSGeom_createLinearRing(cs), n
def update_linearring_from_py(geom, ob):
geos_linearring_from_py(ob, geom._geom, geom._ndim)
def geos_polygon_from_py(shell, holes=None):
if shell is None:
return None
if isinstance(shell, Polygon):
return geos_geom_from_py(shell)
if shell is not None:
ret = geos_linearring_from_py(shell)
if ret is None:
return None
geos_shell, ndim = ret
if holes is not None and len(holes) > 0:
ob = holes
L = len(ob)
exemplar = ob[0]
try:
N = len(exemplar[0])
except TypeError:
N = exemplar._ndim
if not L >= 1:
raise ValueError("number of holes must be non zero")
if N not in (2, 3):
raise ValueError("insufficiant coordinate dimension")
# Array of pointers to ring geometries
geos_holes = (c_void_p * L)()
# add to coordinate sequence
for l in range(L):
geom, ndim = geos_linearring_from_py(ob[l])
geos_holes[l] = cast(geom, c_void_p)
else:
geos_holes = POINTER(c_void_p)()
L = 0
return (
lgeos.GEOSGeom_createPolygon(
c_void_p(geos_shell), geos_holes, L), ndim)