267 lines
8.2 KiB
Python
267 lines
8.2 KiB
Python
"""Line strings and related utilities
|
|
"""
|
|
|
|
import sys
|
|
|
|
if sys.version_info[0] < 3:
|
|
range = xrange
|
|
|
|
from ctypes import c_double
|
|
|
|
from shapely.geos import lgeos, TopologicalError
|
|
from shapely.geometry.base import (
|
|
BaseGeometry, geom_factory, JOIN_STYLE, geos_geom_from_py
|
|
)
|
|
from shapely.geometry.proxy import CachingGeometryProxy
|
|
from shapely.geometry.point import Point
|
|
|
|
__all__ = ['LineString', 'asLineString']
|
|
|
|
|
|
class LineString(BaseGeometry):
|
|
"""
|
|
A one-dimensional figure comprising one or more line segments
|
|
|
|
A LineString has non-zero length and zero area. It may approximate a curve
|
|
and need not be straight. Unlike a LinearRing, a LineString is not closed.
|
|
"""
|
|
|
|
def __init__(self, coordinates=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
coordinates : sequence
|
|
A sequence of (x, y [,z]) numeric coordinate pairs or triples or
|
|
an object that provides the numpy array interface, including
|
|
another instance of LineString.
|
|
|
|
Example
|
|
-------
|
|
Create a line with two segments
|
|
|
|
>>> a = LineString([[0, 0], [1, 0], [1, 1]])
|
|
>>> a.length
|
|
2.0
|
|
"""
|
|
BaseGeometry.__init__(self)
|
|
if coordinates is not None:
|
|
self._set_coords(coordinates)
|
|
|
|
@property
|
|
def __geo_interface__(self):
|
|
return {
|
|
'type': 'LineString',
|
|
'coordinates': tuple(self.coords)
|
|
}
|
|
|
|
def svg(self, scale_factor=1., stroke_color=None):
|
|
"""Returns SVG polyline element for the LineString geometry.
|
|
|
|
Parameters
|
|
==========
|
|
scale_factor : float
|
|
Multiplication factor for the SVG stroke-width. Default is 1.
|
|
stroke_color : str, optional
|
|
Hex string for stroke color. Default is to use "#66cc99" if
|
|
geometry is valid, and "#ff3333" if invalid.
|
|
"""
|
|
if self.is_empty:
|
|
return '<g />'
|
|
if stroke_color is None:
|
|
stroke_color = "#66cc99" if self.is_valid else "#ff3333"
|
|
pnt_format = " ".join(["{},{}".format(*c) for c in self.coords])
|
|
return (
|
|
'<polyline fill="none" stroke="{2}" stroke-width="{1}" '
|
|
'points="{0}" opacity="0.8" />'
|
|
).format(pnt_format, 2. * scale_factor, stroke_color)
|
|
|
|
@property
|
|
def ctypes(self):
|
|
if not self._ctypes_data:
|
|
self._ctypes_data = self.coords.ctypes
|
|
return self._ctypes_data
|
|
|
|
def array_interface(self):
|
|
"""Provide the Numpy array protocol."""
|
|
if self.is_empty:
|
|
ai = {'version': 3, 'typestr': '<f8', 'shape': (0,), 'data': (c_double * 0)()}
|
|
else:
|
|
ai = self.coords.array_interface()
|
|
return ai
|
|
|
|
__array_interface__ = property(array_interface)
|
|
|
|
# Coordinate access
|
|
def _set_coords(self, coordinates):
|
|
self.empty()
|
|
ret = geos_linestring_from_py(coordinates)
|
|
if ret is not None:
|
|
self._geom, self._ndim = ret
|
|
|
|
coords = property(BaseGeometry._get_coords, _set_coords)
|
|
|
|
@property
|
|
def xy(self):
|
|
"""Separate arrays of X and Y coordinate values
|
|
|
|
Example:
|
|
|
|
>>> x, y = LineString(((0, 0), (1, 1))).xy
|
|
>>> list(x)
|
|
[0.0, 1.0]
|
|
>>> list(y)
|
|
[0.0, 1.0]
|
|
"""
|
|
return self.coords.xy
|
|
|
|
def parallel_offset(
|
|
self, distance, side='right',
|
|
resolution=16, join_style=JOIN_STYLE.round, mitre_limit=5.0):
|
|
|
|
"""Returns a LineString or MultiLineString geometry at a distance from
|
|
the object on its right or its left side.
|
|
|
|
The side parameter may be 'left' or 'right' (default is 'right'). The
|
|
resolution of the buffer around each vertex of the object increases by
|
|
increasing the resolution keyword parameter or third positional
|
|
parameter. Vertices of right hand offset lines will be ordered in
|
|
reverse.
|
|
|
|
The join style is for outside corners between line segments. Accepted
|
|
values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and
|
|
JOIN_STYLE.bevel (3).
|
|
|
|
The mitre ratio limit is used for very sharp corners. It is the ratio
|
|
of the distance from the corner to the end of the mitred offset corner.
|
|
When two line segments meet at a sharp angle, a miter join will extend
|
|
far beyond the original geometry. To prevent unreasonable geometry, the
|
|
mitre limit allows controlling the maximum length of the join corner.
|
|
Corners with a ratio which exceed the limit will be beveled.
|
|
"""
|
|
if mitre_limit == 0.0:
|
|
raise ValueError(
|
|
'Cannot compute offset from zero-length line segment')
|
|
try:
|
|
return geom_factory(self.impl['parallel_offset'](
|
|
self, distance, resolution, join_style, mitre_limit, side))
|
|
except OSError:
|
|
raise TopologicalError()
|
|
|
|
|
|
class LineStringAdapter(CachingGeometryProxy, LineString):
|
|
|
|
def __init__(self, context):
|
|
self.context = context
|
|
self.factory = geos_linestring_from_py
|
|
|
|
@property
|
|
def _ndim(self):
|
|
try:
|
|
# From array protocol
|
|
array = self.context.__array_interface__
|
|
n = array['shape'][1]
|
|
assert n == 2 or n == 3
|
|
return n
|
|
except AttributeError:
|
|
# Fall back on list
|
|
return len(self.context[0])
|
|
|
|
@property
|
|
def __array_interface__(self):
|
|
"""Provide the Numpy array protocol."""
|
|
try:
|
|
return self.context.__array_interface__
|
|
except AttributeError:
|
|
return self.array_interface()
|
|
|
|
_get_coords = BaseGeometry._get_coords
|
|
|
|
def _set_coords(self, ob):
|
|
raise NotImplementedError(
|
|
"Adapters can not modify their coordinate sources")
|
|
|
|
coords = property(_get_coords)
|
|
|
|
|
|
def asLineString(context):
|
|
"""Adapt an object the LineString interface"""
|
|
return LineStringAdapter(context)
|
|
|
|
|
|
def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
|
|
# If a LineString is passed in, clone it and return
|
|
# If a LinearRing is passed in, clone the coord seq and return a
|
|
# LineString.
|
|
#
|
|
# NB: access to coordinates using the array protocol has been moved
|
|
# entirely to the speedups module.
|
|
|
|
if isinstance(ob, LineString):
|
|
if type(ob) == LineString:
|
|
return geos_geom_from_py(ob)
|
|
else:
|
|
return geos_geom_from_py(ob, lgeos.GEOSGeom_createLineString)
|
|
|
|
try:
|
|
m = len(ob)
|
|
except TypeError: # Iterators, e.g. Python 3 zip
|
|
ob = list(ob)
|
|
m = len(ob)
|
|
|
|
if m == 0:
|
|
return None
|
|
elif m == 1:
|
|
raise ValueError("LineStrings must have at least 2 coordinate tuples")
|
|
|
|
if m < 2:
|
|
raise ValueError(
|
|
"LineStrings must have at least 2 coordinate tuples")
|
|
|
|
def _coords(o):
|
|
if isinstance(o, Point):
|
|
return o.coords[0]
|
|
else:
|
|
return o
|
|
|
|
try:
|
|
n = len(_coords(ob[0]))
|
|
except TypeError:
|
|
raise ValueError(
|
|
"Input %s is the wrong shape for a LineString" % str(ob))
|
|
assert n == 2 or n == 3
|
|
|
|
# Create a coordinate sequence
|
|
if update_geom is not None:
|
|
cs = lgeos.GEOSGeom_getCoordSeq(update_geom)
|
|
if n != update_ndim:
|
|
raise ValueError(
|
|
"Wrong coordinate dimensions; this geometry has "
|
|
"dimensions: %d" % update_ndim)
|
|
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")
|
|
|
|
if update_geom is not None:
|
|
return None
|
|
else:
|
|
ptr = lgeos.GEOSGeom_createLineString(cs)
|
|
if not ptr:
|
|
raise ValueError("GEOSGeom_createLineString returned a NULL pointer")
|
|
return ptr, n
|
|
|
|
|
|
def update_linestring_from_py(geom, ob):
|
|
geos_linestring_from_py(ob, geom._geom, geom._ndim)
|