refactor: excel parse
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""Geometry classes and factories
|
||||
"""
|
||||
|
||||
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
|
||||
from shapely.geometry.collection import GeometryCollection
|
||||
from shapely.geometry.geo import box, mapping, shape
|
||||
from shapely.geometry.linestring import LineString
|
||||
from shapely.geometry.multilinestring import MultiLineString
|
||||
from shapely.geometry.multipoint import MultiPoint
|
||||
from shapely.geometry.multipolygon import MultiPolygon
|
||||
from shapely.geometry.point import Point
|
||||
from shapely.geometry.polygon import LinearRing, Polygon
|
||||
|
||||
__all__ = [
|
||||
"box",
|
||||
"shape",
|
||||
"mapping",
|
||||
"Point",
|
||||
"LineString",
|
||||
"Polygon",
|
||||
"MultiPoint",
|
||||
"MultiLineString",
|
||||
"MultiPolygon",
|
||||
"GeometryCollection",
|
||||
"LinearRing",
|
||||
"CAP_STYLE",
|
||||
"JOIN_STYLE",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
"""Multi-part collections of geometries
|
||||
"""
|
||||
|
||||
import shapely
|
||||
from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
|
||||
|
||||
|
||||
class GeometryCollection(BaseMultipartGeometry):
|
||||
"""
|
||||
A collection of one or more geometries that may contain more than one type
|
||||
of geometry.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
geoms : list
|
||||
A list of shapely geometry instances, which may be of varying
|
||||
geometry types.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
geoms : sequence
|
||||
A sequence of Shapely geometry instances
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create a GeometryCollection with a Point and a LineString
|
||||
|
||||
>>> from shapely import LineString, Point
|
||||
>>> p = Point(51, -1)
|
||||
>>> l = LineString([(52, -1), (49, 2)])
|
||||
>>> gc = GeometryCollection([p, l])
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, geoms=None):
|
||||
if not geoms:
|
||||
# TODO better empty constructor
|
||||
return shapely.from_wkt("GEOMETRYCOLLECTION EMPTY")
|
||||
if isinstance(geoms, BaseGeometry):
|
||||
# TODO(shapely-2.0) do we actually want to split Multi-part geometries?
|
||||
# this is needed for the split() tests
|
||||
if hasattr(geoms, "geoms"):
|
||||
geoms = geoms.geoms
|
||||
else:
|
||||
geoms = [geoms]
|
||||
|
||||
return shapely.geometrycollections(geoms)
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
geometries = []
|
||||
for geom in self.geoms:
|
||||
geometries.append(geom.__geo_interface__)
|
||||
return dict(type="GeometryCollection", geometries=geometries)
|
||||
|
||||
|
||||
shapely.lib.registry[7] = GeometryCollection
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Autouse fixtures for doctests."""
|
||||
|
||||
import pytest
|
||||
|
||||
from shapely.geometry.linestring import LineString
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def add_linestring(doctest_namespace):
|
||||
doctest_namespace["LineString"] = LineString
|
||||
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Geometry factories based on the geo interface
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from shapely.errors import GeometryTypeError
|
||||
from shapely.geometry.collection import GeometryCollection
|
||||
from shapely.geometry.linestring import LineString
|
||||
from shapely.geometry.multilinestring import MultiLineString
|
||||
from shapely.geometry.multipoint import MultiPoint
|
||||
from shapely.geometry.multipolygon import MultiPolygon
|
||||
from shapely.geometry.point import Point
|
||||
from shapely.geometry.polygon import LinearRing, Polygon
|
||||
|
||||
|
||||
def _is_coordinates_empty(coordinates):
|
||||
"""Helper to identify if coordinates or subset of coordinates are empty"""
|
||||
|
||||
if coordinates is None:
|
||||
return True
|
||||
|
||||
if isinstance(coordinates, (list, tuple, np.ndarray)):
|
||||
if len(coordinates) == 0:
|
||||
return True
|
||||
return all(map(_is_coordinates_empty, coordinates))
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _empty_shape_for_no_coordinates(geom_type):
|
||||
"""Return empty counterpart for geom_type"""
|
||||
if geom_type == "point":
|
||||
return Point()
|
||||
elif geom_type == "multipoint":
|
||||
return MultiPoint()
|
||||
elif geom_type == "linestring":
|
||||
return LineString()
|
||||
elif geom_type == "multilinestring":
|
||||
return MultiLineString()
|
||||
elif geom_type == "polygon":
|
||||
return Polygon()
|
||||
elif geom_type == "multipolygon":
|
||||
return MultiPolygon()
|
||||
else:
|
||||
raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}")
|
||||
|
||||
|
||||
def box(minx, miny, maxx, maxy, ccw=True):
|
||||
"""Returns a rectangular polygon with configurable normal vector"""
|
||||
coords = [(maxx, miny), (maxx, maxy), (minx, maxy), (minx, miny)]
|
||||
if not ccw:
|
||||
coords = coords[::-1]
|
||||
return Polygon(coords)
|
||||
|
||||
|
||||
def shape(context):
|
||||
"""
|
||||
Returns a new, independent geometry with coordinates *copied* from the
|
||||
context. Changes to the original context will not be reflected in the
|
||||
geometry object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context :
|
||||
a GeoJSON-like dict, which provides a "type" member describing the type
|
||||
of the geometry and "coordinates" member providing a list of coordinates,
|
||||
or an object which implements __geo_interface__.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Geometry object
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create a Point from GeoJSON, and then create a copy using __geo_interface__.
|
||||
|
||||
>>> context = {'type': 'Point', 'coordinates': [0, 1]}
|
||||
>>> geom = shape(context)
|
||||
>>> geom.geom_type == 'Point'
|
||||
True
|
||||
>>> geom.wkt
|
||||
'POINT (0 1)'
|
||||
>>> geom2 = shape(geom)
|
||||
>>> geom == geom2
|
||||
True
|
||||
"""
|
||||
if hasattr(context, "__geo_interface__"):
|
||||
ob = context.__geo_interface__
|
||||
else:
|
||||
ob = context
|
||||
geom_type = ob.get("type").lower()
|
||||
if "coordinates" in ob and _is_coordinates_empty(ob["coordinates"]):
|
||||
return _empty_shape_for_no_coordinates(geom_type)
|
||||
elif geom_type == "point":
|
||||
return Point(ob["coordinates"])
|
||||
elif geom_type == "linestring":
|
||||
return LineString(ob["coordinates"])
|
||||
elif geom_type == "linearring":
|
||||
return LinearRing(ob["coordinates"])
|
||||
elif geom_type == "polygon":
|
||||
return Polygon(ob["coordinates"][0], ob["coordinates"][1:])
|
||||
elif geom_type == "multipoint":
|
||||
return MultiPoint(ob["coordinates"])
|
||||
elif geom_type == "multilinestring":
|
||||
return MultiLineString(ob["coordinates"])
|
||||
elif geom_type == "multipolygon":
|
||||
return MultiPolygon([[c[0], c[1:]] for c in ob["coordinates"]])
|
||||
elif geom_type == "geometrycollection":
|
||||
geoms = [shape(g) for g in ob.get("geometries", [])]
|
||||
return GeometryCollection(geoms)
|
||||
else:
|
||||
raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}")
|
||||
|
||||
|
||||
def mapping(ob):
|
||||
"""
|
||||
Returns a GeoJSON-like mapping from a Geometry or any
|
||||
object which implements __geo_interface__
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ob :
|
||||
An object which implements __geo_interface__.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> pt = Point(0, 0)
|
||||
>>> mapping(pt)
|
||||
{'type': 'Point', 'coordinates': (0.0, 0.0)}
|
||||
"""
|
||||
return ob.__geo_interface__
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Line strings and related utilities
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
import shapely
|
||||
from shapely.geometry.base import BaseGeometry, JOIN_STYLE
|
||||
from shapely.geometry.point import Point
|
||||
|
||||
__all__ = ["LineString"]
|
||||
|
||||
|
||||
class LineString(BaseGeometry):
|
||||
"""
|
||||
A geometry type composed of one or more line segments.
|
||||
|
||||
A LineString is a one-dimensional feature and has a non-zero length but
|
||||
zero area. It may approximate a curve and need not be straight. Unlike a
|
||||
LinearRing, a LineString is not closed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : sequence
|
||||
A sequence of (x, y, [,z]) numeric coordinate pairs or triples, or
|
||||
an array-like with shape (N, 2) or (N, 3).
|
||||
Also can be a sequence of Point objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create a LineString with two segments
|
||||
|
||||
>>> a = LineString([[0, 0], [1, 0], [1, 1]])
|
||||
>>> a.length
|
||||
2.0
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, coordinates=None):
|
||||
if coordinates is None:
|
||||
# empty geometry
|
||||
# TODO better constructor
|
||||
return shapely.from_wkt("LINESTRING EMPTY")
|
||||
elif isinstance(coordinates, LineString):
|
||||
if type(coordinates) == LineString:
|
||||
# return original objects since geometries are immutable
|
||||
return coordinates
|
||||
else:
|
||||
# LinearRing
|
||||
# TODO convert LinearRing to LineString more directly
|
||||
coordinates = coordinates.coords
|
||||
else:
|
||||
if hasattr(coordinates, "__array__"):
|
||||
coordinates = np.asarray(coordinates)
|
||||
if isinstance(coordinates, np.ndarray) and np.issubdtype(
|
||||
coordinates.dtype, np.number
|
||||
):
|
||||
pass
|
||||
else:
|
||||
# check coordinates on points
|
||||
def _coords(o):
|
||||
if isinstance(o, Point):
|
||||
return o.coords[0]
|
||||
else:
|
||||
return [float(c) for c in o]
|
||||
|
||||
coordinates = [_coords(o) for o in coordinates]
|
||||
|
||||
if len(coordinates) == 0:
|
||||
# empty geometry
|
||||
# TODO better constructor + should shapely.linestrings handle this?
|
||||
return shapely.from_wkt("LINESTRING EMPTY")
|
||||
|
||||
geom = shapely.linestrings(coordinates)
|
||||
if not isinstance(geom, LineString):
|
||||
raise ValueError("Invalid values passed to LineString constructor")
|
||||
return geom
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
return {"type": "LineString", "coordinates": tuple(self.coords)}
|
||||
|
||||
def svg(self, scale_factor=1.0, stroke_color=None, opacity=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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.8
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if stroke_color is None:
|
||||
stroke_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
if opacity is None:
|
||||
opacity = 0.8
|
||||
pnt_format = " ".join(["{},{}".format(*c) for c in self.coords])
|
||||
return (
|
||||
'<polyline fill="none" stroke="{2}" stroke-width="{1}" '
|
||||
'points="{0}" opacity="{3}" />'
|
||||
).format(pnt_format, 2.0 * scale_factor, stroke_color, opacity)
|
||||
|
||||
@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 offset_curve(
|
||||
self,
|
||||
distance,
|
||||
quad_segs=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 is determined by the sign of the `distance` parameter
|
||||
(negative for right side offset, positive for left side offset). The
|
||||
resolution of the buffer around each vertex of the object increases
|
||||
by increasing the `quad_segs` keyword parameter.
|
||||
|
||||
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.
|
||||
|
||||
Note: the behaviour regarding orientation of the resulting line
|
||||
depends on the GEOS version. With GEOS < 3.11, the line retains the
|
||||
same direction for a left offset (positive distance) or has reverse
|
||||
direction for a right offset (negative distance), and this behaviour
|
||||
was documented as such in previous Shapely versions. Starting with
|
||||
GEOS 3.11, the function tries to preserve the orientation of the
|
||||
original line.
|
||||
"""
|
||||
if mitre_limit == 0.0:
|
||||
raise ValueError("Cannot compute offset from zero-length line segment")
|
||||
elif not np.isfinite(distance):
|
||||
raise ValueError("offset_curve distance must be finite")
|
||||
return shapely.offset_curve(self, distance, quad_segs, join_style, mitre_limit)
|
||||
|
||||
def parallel_offset(
|
||||
self,
|
||||
distance,
|
||||
side="right",
|
||||
resolution=16,
|
||||
join_style=JOIN_STYLE.round,
|
||||
mitre_limit=5.0,
|
||||
):
|
||||
"""
|
||||
Alternative method to :meth:`offset_curve` method.
|
||||
|
||||
Older alternative method to the :meth:`offset_curve` method, but uses
|
||||
``resolution`` instead of ``quad_segs`` and a ``side`` keyword
|
||||
('left' or 'right') instead of sign of the distance. This method is
|
||||
kept for backwards compatibility for now, but is is recommended to
|
||||
use :meth:`offset_curve` instead.
|
||||
"""
|
||||
if side == "right":
|
||||
distance *= -1
|
||||
return self.offset_curve(
|
||||
distance,
|
||||
quad_segs=resolution,
|
||||
join_style=join_style,
|
||||
mitre_limit=mitre_limit,
|
||||
)
|
||||
|
||||
|
||||
shapely.lib.registry[1] = LineString
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Collections of linestrings and related utilities
|
||||
"""
|
||||
|
||||
import shapely
|
||||
from shapely.errors import EmptyPartError
|
||||
from shapely.geometry import linestring
|
||||
from shapely.geometry.base import BaseMultipartGeometry
|
||||
|
||||
__all__ = ["MultiLineString"]
|
||||
|
||||
|
||||
class MultiLineString(BaseMultipartGeometry):
|
||||
"""
|
||||
A collection of one or more LineStrings.
|
||||
|
||||
A MultiLineString has non-zero length and zero area.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : sequence
|
||||
A sequence LineStrings, or a sequence of line-like coordinate
|
||||
sequences or array-likes (see accepted input for LineString).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
geoms : sequence
|
||||
A sequence of LineStrings
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a MultiLineString containing two LineStrings.
|
||||
|
||||
>>> lines = MultiLineString([[[0, 0], [1, 2]], [[4, 4], [5, 6]]])
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, lines=None):
|
||||
if not lines:
|
||||
# allow creation of empty multilinestrings, to support unpickling
|
||||
# TODO better empty constructor
|
||||
return shapely.from_wkt("MULTILINESTRING EMPTY")
|
||||
elif isinstance(lines, MultiLineString):
|
||||
return lines
|
||||
|
||||
lines = getattr(lines, "geoms", lines)
|
||||
m = len(lines)
|
||||
subs = []
|
||||
for i in range(m):
|
||||
line = linestring.LineString(lines[i])
|
||||
if line.is_empty:
|
||||
raise EmptyPartError(
|
||||
"Can't create MultiLineString with empty component"
|
||||
)
|
||||
subs.append(line)
|
||||
|
||||
if len(lines) == 0:
|
||||
return shapely.from_wkt("MULTILINESTRING EMPTY")
|
||||
|
||||
return shapely.multilinestrings(subs)
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
return {
|
||||
"type": "MultiLineString",
|
||||
"coordinates": tuple(tuple(c for c in g.coords) for g in self.geoms),
|
||||
}
|
||||
|
||||
def svg(self, scale_factor=1.0, stroke_color=None, opacity=None):
|
||||
"""Returns a group of SVG polyline elements 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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.8
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if stroke_color is None:
|
||||
stroke_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
return (
|
||||
"<g>"
|
||||
+ "".join(p.svg(scale_factor, stroke_color, opacity) for p in self.geoms)
|
||||
+ "</g>"
|
||||
)
|
||||
|
||||
|
||||
shapely.lib.registry[5] = MultiLineString
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Collections of points and related utilities
|
||||
"""
|
||||
|
||||
import shapely
|
||||
from shapely.errors import EmptyPartError
|
||||
from shapely.geometry import point
|
||||
from shapely.geometry.base import BaseMultipartGeometry
|
||||
|
||||
__all__ = ["MultiPoint"]
|
||||
|
||||
|
||||
class MultiPoint(BaseMultipartGeometry):
|
||||
"""
|
||||
A collection of one or more Points.
|
||||
|
||||
A MultiPoint has zero area and zero length.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points : sequence
|
||||
A sequence of Points, or a sequence of (x, y [,z]) numeric coordinate
|
||||
pairs or triples, or an array-like of shape (N, 2) or (N, 3).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
geoms : sequence
|
||||
A sequence of Points
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a MultiPoint containing two Points
|
||||
|
||||
>>> from shapely import Point
|
||||
>>> ob = MultiPoint([[0.0, 0.0], [1.0, 2.0]])
|
||||
>>> len(ob.geoms)
|
||||
2
|
||||
>>> type(ob.geoms[0]) == Point
|
||||
True
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, points=None):
|
||||
if points is None:
|
||||
# allow creation of empty multipoints, to support unpickling
|
||||
# TODO better empty constructor
|
||||
return shapely.from_wkt("MULTIPOINT EMPTY")
|
||||
elif isinstance(points, MultiPoint):
|
||||
return points
|
||||
|
||||
m = len(points)
|
||||
subs = []
|
||||
for i in range(m):
|
||||
p = point.Point(points[i])
|
||||
if p.is_empty:
|
||||
raise EmptyPartError("Can't create MultiPoint with empty component")
|
||||
subs.append(p)
|
||||
|
||||
if len(points) == 0:
|
||||
return shapely.from_wkt("MULTIPOINT EMPTY")
|
||||
|
||||
return shapely.multipoints(subs)
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
return {
|
||||
"type": "MultiPoint",
|
||||
"coordinates": tuple(g.coords[0] for g in self.geoms),
|
||||
}
|
||||
|
||||
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
|
||||
"""Returns a group of SVG circle elements for the MultiPoint geometry.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
scale_factor : float
|
||||
Multiplication factor for the SVG circle diameters. 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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.6
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if fill_color is None:
|
||||
fill_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
return (
|
||||
"<g>"
|
||||
+ "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms)
|
||||
+ "</g>"
|
||||
)
|
||||
|
||||
|
||||
shapely.lib.registry[4] = MultiPoint
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Collections of polygons and related utilities
|
||||
"""
|
||||
|
||||
import shapely
|
||||
from shapely.geometry import polygon
|
||||
from shapely.geometry.base import BaseMultipartGeometry
|
||||
|
||||
__all__ = ["MultiPolygon"]
|
||||
|
||||
|
||||
class MultiPolygon(BaseMultipartGeometry):
|
||||
"""
|
||||
A collection of one or more Polygons.
|
||||
|
||||
If component polygons overlap the collection is invalid and some
|
||||
operations on it may fail.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
polygons : sequence
|
||||
A sequence of Polygons, or a sequence of (shell, holes) tuples
|
||||
where shell is the sequence representation of a linear ring
|
||||
(see LinearRing) and holes is a sequence of such linear rings.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
geoms : sequence
|
||||
A sequence of `Polygon` instances
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a MultiPolygon from a sequence of coordinate tuples
|
||||
|
||||
>>> from shapely import Polygon
|
||||
>>> ob = MultiPolygon([
|
||||
... (
|
||||
... ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
|
||||
... [((0.1,0.1), (0.1,0.2), (0.2,0.2), (0.2,0.1))]
|
||||
... )
|
||||
... ])
|
||||
>>> len(ob.geoms)
|
||||
1
|
||||
>>> type(ob.geoms[0]) == Polygon
|
||||
True
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, polygons=None):
|
||||
if not polygons:
|
||||
# allow creation of empty multipolygons, to support unpickling
|
||||
# TODO better empty constructor
|
||||
return shapely.from_wkt("MULTIPOLYGON EMPTY")
|
||||
elif isinstance(polygons, MultiPolygon):
|
||||
return polygons
|
||||
|
||||
polygons = getattr(polygons, "geoms", polygons)
|
||||
polygons = [
|
||||
p
|
||||
for p in polygons
|
||||
if p and not (isinstance(p, polygon.Polygon) and p.is_empty)
|
||||
]
|
||||
|
||||
L = len(polygons)
|
||||
|
||||
# Bail immediately if we have no input points.
|
||||
if L == 0:
|
||||
return shapely.from_wkt("MULTIPOLYGON EMPTY")
|
||||
|
||||
# This function does not accept sequences of MultiPolygons: there is
|
||||
# no implicit flattening.
|
||||
if isinstance(polygons[0], MultiPolygon):
|
||||
raise ValueError("Sequences of multi-polygons are not valid arguments")
|
||||
|
||||
subs = []
|
||||
for i in range(L):
|
||||
ob = polygons[i]
|
||||
if not isinstance(ob, polygon.Polygon):
|
||||
shell = ob[0]
|
||||
if len(ob) > 1:
|
||||
holes = ob[1]
|
||||
else:
|
||||
holes = None
|
||||
p = polygon.Polygon(shell, holes)
|
||||
else:
|
||||
p = polygon.Polygon(ob)
|
||||
subs.append(p)
|
||||
|
||||
return shapely.multipolygons(subs)
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
allcoords = []
|
||||
for geom in self.geoms:
|
||||
coords = []
|
||||
coords.append(tuple(geom.exterior.coords))
|
||||
for hole in geom.interiors:
|
||||
coords.append(tuple(hole.coords))
|
||||
allcoords.append(tuple(coords))
|
||||
return {"type": "MultiPolygon", "coordinates": allcoords}
|
||||
|
||||
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
|
||||
"""Returns group of SVG path elements for the MultiPolygon 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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.6
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if fill_color is None:
|
||||
fill_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
return (
|
||||
"<g>"
|
||||
+ "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms)
|
||||
+ "</g>"
|
||||
)
|
||||
|
||||
|
||||
shapely.lib.registry[6] = MultiPolygon
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Points and related utilities
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
import shapely
|
||||
from shapely.errors import DimensionError
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
|
||||
__all__ = ["Point"]
|
||||
|
||||
|
||||
class Point(BaseGeometry):
|
||||
"""
|
||||
A geometry type that represents a single coordinate with
|
||||
x,y and possibly z values.
|
||||
|
||||
A point is a zero-dimensional feature and has zero length and zero area.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args : float, or sequence of floats
|
||||
The coordinates can either be passed as a single parameter, or as
|
||||
individual float values using multiple parameters:
|
||||
|
||||
1) 1 parameter: a sequence or array-like of with 2 or 3 values.
|
||||
2) 2 or 3 parameters (float): x, y, and possibly z.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
x, y, z : float
|
||||
Coordinate values
|
||||
|
||||
Examples
|
||||
--------
|
||||
Constructing the Point using separate parameters for x and y:
|
||||
|
||||
>>> p = Point(1.0, -1.0)
|
||||
|
||||
Constructing the Point using a list of x, y coordinates:
|
||||
|
||||
>>> p = Point([1.0, -1.0])
|
||||
>>> print(p)
|
||||
POINT (1 -1)
|
||||
>>> p.y
|
||||
-1.0
|
||||
>>> p.x
|
||||
1.0
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, *args):
|
||||
if len(args) == 0:
|
||||
# empty geometry
|
||||
# TODO better constructor
|
||||
return shapely.from_wkt("POINT EMPTY")
|
||||
elif len(args) > 3:
|
||||
raise TypeError(f"Point() takes at most 3 arguments ({len(args)} given)")
|
||||
elif len(args) == 1:
|
||||
coords = args[0]
|
||||
if isinstance(coords, Point):
|
||||
return coords
|
||||
|
||||
# Accept either (x, y) or [(x, y)]
|
||||
if not hasattr(coords, "__getitem__"): # generators
|
||||
coords = list(coords)
|
||||
coords = np.asarray(coords).squeeze()
|
||||
else:
|
||||
# 2 or 3 args
|
||||
coords = np.array(args).squeeze()
|
||||
|
||||
if coords.ndim > 1:
|
||||
raise ValueError(
|
||||
f"Point() takes only scalar or 1-size vector arguments, got {args}"
|
||||
)
|
||||
if not np.issubdtype(coords.dtype, np.number):
|
||||
coords = [float(c) for c in coords]
|
||||
geom = shapely.points(coords)
|
||||
if not isinstance(geom, Point):
|
||||
raise ValueError("Invalid values passed to Point constructor")
|
||||
return geom
|
||||
|
||||
# Coordinate getters and setters
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"""Return x coordinate."""
|
||||
return float(shapely.get_x(self))
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""Return y coordinate."""
|
||||
return float(shapely.get_y(self))
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"""Return z coordinate."""
|
||||
if not shapely.has_z(self):
|
||||
raise DimensionError("This point has no z coordinate.")
|
||||
# return shapely.get_z(self) -> get_z only supported for GEOS 3.7+
|
||||
return self.coords[0][2]
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
return {"type": "Point", "coordinates": self.coords[0]}
|
||||
|
||||
def svg(self, scale_factor=1.0, fill_color=None, opacity=None):
|
||||
"""Returns SVG circle element for the Point geometry.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
scale_factor : float
|
||||
Multiplication factor for the SVG circle diameter. 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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.6
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if fill_color is None:
|
||||
fill_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
if opacity is None:
|
||||
opacity = 0.6
|
||||
return (
|
||||
'<circle cx="{0.x}" cy="{0.y}" r="{1}" '
|
||||
'stroke="#555555" stroke-width="{2}" fill="{3}" opacity="{4}" />'
|
||||
).format(self, 3.0 * scale_factor, 1.0 * scale_factor, fill_color, opacity)
|
||||
|
||||
@property
|
||||
def xy(self):
|
||||
"""Separate arrays of X and Y coordinate values
|
||||
|
||||
Example:
|
||||
>>> x, y = Point(0, 0).xy
|
||||
>>> list(x)
|
||||
[0.0]
|
||||
>>> list(y)
|
||||
[0.0]
|
||||
"""
|
||||
return self.coords.xy
|
||||
|
||||
|
||||
shapely.lib.registry[0] = Point
|
||||
@@ -0,0 +1,355 @@
|
||||
"""Polygons and their linear ring components
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import shapely
|
||||
from shapely.algorithms.cga import is_ccw_impl, signed_area
|
||||
from shapely.errors import TopologicalError
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.geometry.linestring import LineString
|
||||
from shapely.geometry.point import Point
|
||||
|
||||
__all__ = ["orient", "Polygon", "LinearRing"]
|
||||
|
||||
|
||||
def _unpickle_linearring(wkb):
|
||||
linestring = shapely.from_wkb(wkb)
|
||||
srid = shapely.get_srid(linestring)
|
||||
linearring = shapely.linearrings(shapely.get_coordinates(linestring))
|
||||
if srid:
|
||||
linearring = shapely.set_srid(linearring, srid)
|
||||
return linearring
|
||||
|
||||
|
||||
class LinearRing(LineString):
|
||||
"""
|
||||
A geometry type composed of one or more line segments
|
||||
that forms a closed loop.
|
||||
|
||||
A LinearRing is a closed, one-dimensional feature.
|
||||
A LinearRing that crosses itself or touches itself at a single point is
|
||||
invalid and operations on it may fail.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : sequence
|
||||
A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
|
||||
an array-like with shape (N, 2) or (N, 3).
|
||||
Also can be a sequence of Point objects.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Rings are automatically closed. There is no need to specify a final
|
||||
coordinate pair identical to the first.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a square ring.
|
||||
|
||||
>>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) )
|
||||
>>> ring.is_closed
|
||||
True
|
||||
>>> list(ring.coords)
|
||||
[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
|
||||
>>> ring.length
|
||||
4.0
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, coordinates=None):
|
||||
if coordinates is None:
|
||||
# empty geometry
|
||||
# TODO better way?
|
||||
return shapely.from_wkt("LINEARRING EMPTY")
|
||||
elif isinstance(coordinates, LineString):
|
||||
if type(coordinates) == LinearRing:
|
||||
# return original objects since geometries are immutable
|
||||
return coordinates
|
||||
elif not coordinates.is_valid:
|
||||
raise TopologicalError("An input LineString must be valid.")
|
||||
else:
|
||||
# LineString
|
||||
# TODO convert LineString to LinearRing more directly?
|
||||
coordinates = coordinates.coords
|
||||
|
||||
else:
|
||||
if hasattr(coordinates, "__array__"):
|
||||
coordinates = np.asarray(coordinates)
|
||||
if isinstance(coordinates, np.ndarray) and np.issubdtype(
|
||||
coordinates.dtype, np.number
|
||||
):
|
||||
pass
|
||||
else:
|
||||
# check coordinates on points
|
||||
def _coords(o):
|
||||
if isinstance(o, Point):
|
||||
return o.coords[0]
|
||||
else:
|
||||
return [float(c) for c in o]
|
||||
|
||||
coordinates = np.array([_coords(o) for o in coordinates])
|
||||
if not np.issubdtype(coordinates.dtype, np.number):
|
||||
# conversion of coords to 2D array failed, this might be due
|
||||
# to inconsistent coordinate dimensionality
|
||||
raise ValueError("Inconsistent coordinate dimensionality")
|
||||
|
||||
if len(coordinates) == 0:
|
||||
# empty geometry
|
||||
# TODO better constructor + should shapely.linearrings handle this?
|
||||
return shapely.from_wkt("LINEARRING EMPTY")
|
||||
|
||||
geom = shapely.linearrings(coordinates)
|
||||
if not isinstance(geom, LinearRing):
|
||||
raise ValueError("Invalid values passed to LinearRing constructor")
|
||||
return geom
|
||||
|
||||
@property
|
||||
def __geo_interface__(self):
|
||||
return {"type": "LinearRing", "coordinates": tuple(self.coords)}
|
||||
|
||||
def __reduce__(self):
|
||||
"""WKB doesn't differentiate between LineString and LinearRing so we
|
||||
need to move the coordinate sequence into the correct geometry type"""
|
||||
return (_unpickle_linearring, (shapely.to_wkb(self, include_srid=True),))
|
||||
|
||||
@property
|
||||
def is_ccw(self):
|
||||
"""True is the ring is oriented counter clock-wise"""
|
||||
return bool(is_ccw_impl()(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 bool(shapely.is_simple(self))
|
||||
|
||||
|
||||
shapely.lib.registry[2] = LinearRing
|
||||
|
||||
|
||||
class InteriorRingSequence:
|
||||
|
||||
_parent = None
|
||||
_ndim = None
|
||||
_index = 0
|
||||
_length = 0
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
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
|
||||
|
||||
def __len__(self):
|
||||
return shapely.get_num_interior_rings(self._parent)
|
||||
|
||||
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")
|
||||
|
||||
def _get_ring(self, i):
|
||||
return shapely.get_interior_ring(self._parent, i)
|
||||
|
||||
|
||||
class Polygon(BaseGeometry):
|
||||
"""
|
||||
A geometry type representing an area that is enclosed by a linear ring.
|
||||
|
||||
A polygon is a two-dimensional feature and 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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shell : sequence
|
||||
A sequence of (x, y [,z]) numeric coordinate pairs or triples, or
|
||||
an array-like with shape (N, 2) or (N, 3).
|
||||
Also can be a sequence of Point objects.
|
||||
holes : sequence
|
||||
A sequence of objects which satisfy the same requirements as the
|
||||
shell parameters above
|
||||
|
||||
Attributes
|
||||
----------
|
||||
exterior : LinearRing
|
||||
The ring which bounds the positive space of the polygon.
|
||||
interiors : sequence
|
||||
A sequence of rings which bound all existing holes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
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
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
def __new__(self, shell=None, holes=None):
|
||||
if shell is None:
|
||||
# empty geometry
|
||||
# TODO better way?
|
||||
return shapely.from_wkt("POLYGON EMPTY")
|
||||
elif isinstance(shell, Polygon):
|
||||
# return original objects since geometries are immutable
|
||||
return shell
|
||||
else:
|
||||
shell = LinearRing(shell)
|
||||
|
||||
if holes is not None:
|
||||
if len(holes) == 0:
|
||||
# shapely constructor cannot handle holes=[]
|
||||
holes = None
|
||||
else:
|
||||
holes = [LinearRing(ring) for ring in holes]
|
||||
|
||||
geom = shapely.polygons(shell, holes=holes)
|
||||
if not isinstance(geom, Polygon):
|
||||
raise ValueError("Invalid values passed to Polygon constructor")
|
||||
return geom
|
||||
|
||||
@property
|
||||
def exterior(self):
|
||||
return shapely.get_exterior_ring(self)
|
||||
|
||||
@property
|
||||
def interiors(self):
|
||||
if self.is_empty:
|
||||
return []
|
||||
return InteriorRingSequence(self)
|
||||
|
||||
@property
|
||||
def coords(self):
|
||||
raise NotImplementedError(
|
||||
"Component rings have coordinate sequences, but the polygon does not"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, BaseGeometry):
|
||||
return NotImplemented
|
||||
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 = [self.exterior.coords] + [
|
||||
interior.coords for interior in self.interiors
|
||||
]
|
||||
other_coords = [other.exterior.coords] + [
|
||||
interior.coords for interior in other.interiors
|
||||
]
|
||||
if not len(my_coords) == len(other_coords):
|
||||
return False
|
||||
# equal_nan=False is the default, but not yet available for older numpy
|
||||
return np.all(
|
||||
[
|
||||
np.array_equal(left, right) # , equal_nan=False)
|
||||
for left, right in zip(my_coords, other_coords)
|
||||
]
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
@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.0, fill_color=None, opacity=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.
|
||||
opacity : float
|
||||
Float number between 0 and 1 for color opacity. Default value is 0.6
|
||||
"""
|
||||
if self.is_empty:
|
||||
return "<g />"
|
||||
if fill_color is None:
|
||||
fill_color = "#66cc99" if self.is_valid else "#ff3333"
|
||||
if opacity is None:
|
||||
opacity = 0.6
|
||||
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 (
|
||||
'<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
|
||||
'stroke-width="{0}" opacity="{3}" d="{1}" />'
|
||||
).format(2.0 * scale_factor, path, fill_color, opacity)
|
||||
|
||||
@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)])
|
||||
|
||||
|
||||
shapely.lib.registry[3] = Polygon
|
||||
|
||||
|
||||
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:])
|
||||
Reference in New Issue
Block a user