"""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 ''
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 (
''
).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': '>> 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)