def _split_line_with_line(line, splitter): """Split a LineString with another (Multi)LineString or (Multi)Polygon""" # if splitter is a polygon, pick it's boundary if splitter.type in ("Polygon", "MultiPolygon"): splitter = splitter.boundary if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, LineString) and not isinstance( splitter, MultiLineString): raise GeometryTypeError( "Second argument must be either a LineString or a MultiLineString" ) # | s\l | Interior | Boundary | Exterior | # |----------|----------|----------|----------| # | Interior | 0 or F | * | * | At least one of these two must be 0 # | Boundary | 0 or F | * | * | So either '0********' or '[0F]**0*****' # | Exterior | * | * | * | No overlapping interiors ('1********') relation = splitter.relate(line) if relation[0] == "1": # The lines overlap at some segment (linear intersection of interiors) raise ValueError( "Input geometry segment overlaps with the splitter.") elif relation[0] == "0" or relation[3] == "0": # The splitter crosses or touches the line's interior --> return multilinestring from the split return line.difference(splitter) else: # The splitter does not cross or touch the line's interior --> return collection with identity line return [line]
def _split_line_with_multipoint(line, splitter): """Split a LineString with a MultiPoint""" if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, MultiPoint): raise GeometryTypeError("Second argument must be a MultiPoint") chunks = [line] for pt in splitter.geoms: new_chunks = [] for chunk in filter(lambda x: not x.is_empty, chunks): # add the newly split 2 lines or the same line if not split new_chunks.extend(SplitOp._split_line_with_point(chunk, pt)) chunks = new_chunks return chunks
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.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("Unknown geometry type: %s" % geom_type)
def shared_paths(g1, g2): """Find paths shared between the two given lineal geometries Returns a GeometryCollection with two elements: - First element is a MultiLineString containing shared paths with the same direction for both inputs. - Second element is a MultiLineString containing shared paths with the opposite direction for the two inputs. Parameters ---------- g1 : geometry The first geometry g2 : geometry The second geometry """ if not isinstance(g1, LineString): raise GeometryTypeError("First geometry must be a LineString") if not isinstance(g2, LineString): raise GeometryTypeError("Second geometry must be a LineString") return shapely.shared_paths(g1, g2)
def _split_polygon_with_line(poly, splitter): """Split a Polygon with a LineString""" if not isinstance(poly, Polygon): raise GeometryTypeError("First argument must be a Polygon") if not isinstance(splitter, LineString): raise GeometryTypeError("Second argument must be a LineString") union = poly.boundary.union(splitter) # greatly improves split performance for big geometries with many # holes (the following contains checks) with minimal overhead # for common cases poly = prep(poly) # some polygonized geometries may be holes, we do not want them # that's why we test if the original polygon (poly) contains # an inner point of polygonized geometry (pg) return [ pg for pg in polygonize(union) if poly.contains(pg.representative_point()) ]
def _split_line_with_point(line, splitter): """Split a LineString with a Point""" if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, Point): raise GeometryTypeError("Second argument must be a Point") # check if point is in the interior of the line if not line.relate_pattern(splitter, "0********"): # point not on line interior --> return collection with single identity line # (REASONING: Returning a list with the input line reference and creating a # GeometryCollection at the general split function prevents unnecessary copying # of linestrings in multipoint splitting function) return [line] elif line.coords[0] == splitter.coords[0]: # if line is a closed ring the previous test doesn't behave as desired return [line] # point is on line, get the distance from the first point on line distance_on_line = line.project(splitter) coords = list(line.coords) # split the line at the point and create two new lines current_position = 0.0 for i in range(len(coords) - 1): point1 = coords[i] point2 = coords[i + 1] dx = point1[0] - point2[0] dy = point1[1] - point2[1] segment_length = (dx**2 + dy**2)**0.5 current_position += segment_length if distance_on_line == current_position: # splitter is exactly on a vertex return [LineString(coords[:i + 2]), LineString(coords[i + 1:])] elif distance_on_line < current_position: # splitter is between two vertices return [ LineString(coords[:i + 1] + [splitter.coords[0]]), LineString([splitter.coords[0]] + coords[i + 1:]), ] return [line]
def dump_coords(geom): """Dump coordinates of a geometry in the same order as data packing""" if not isinstance(geom, BaseGeometry): raise ValueError("Must be instance of a geometry class; found " + geom.__class__.__name__) elif geom.type in ("Point", "LineString", "LinearRing"): return geom.coords[:] elif geom.type == "Polygon": return geom.exterior.coords[:] + [i.coords[:] for i in geom.interiors] elif geom.type.startswith("Multi") or geom.type == "GeometryCollection": # Recursive call return [dump_coords(part) for part in geom.geoms] else: raise GeometryTypeError("Unhandled geometry type: " + repr(geom.type))
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("Unknown geometry type: %s" % geom_type)
def __getitem__(self, key): m = self.__len__() if isinstance(key, integer_types): 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_geom_item(i) elif isinstance(key, slice): if type(self) == HeterogeneousGeometrySequence: raise GeometryTypeError( "Heterogeneous geometry collections are not sliceable") res = [] start, stop, stride = key.indices(m) for i in range(start, stop, stride): res.append(self._get_geom_item(i)) return type(self.__p__)(res or None) else: raise TypeError("key must be an index or slice")
def asShape(context): """ Adapts the context to a geometry interface. The coordinates remain stored in the context, and changes to them will be reflected in the returned geometry object. .. deprecated:: 1.8 The proxy geometries (adapter classes) created by this function are deprecated, and this function will be removed in Shapely 2.0. Use the `shape` function instead to convert a GeoJSON-like dict to a Shapely geometry. 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 Notes ----- The Adapter classes returned by this function trade performance for reduced storage of coordinate values. In general, the shape() function should be used instead. Example ------- Create a Point and Polygon from GeoJSON, change the coordinates of the Point's context and show that the corresponding geometry is changed, as well. >>> point_context = {'type': 'Point', 'coordinates': [0.5, 0.5]} >>> poly_context = {'type': 'Polygon', 'coordinates': [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]} >>> point, poly = asShape(point_context), asShape(poly_context) >>> poly.intersects(point) True >>> point_context['coordinates'][0] = 1.5 >>> poly.intersects(point) False """ if hasattr(context, "__geo_interface__"): ob = context.__geo_interface__ else: ob = context try: geom_type = ob.get("type").lower() except AttributeError: raise ValueError("Context does not provide geo interface") if geom_type == "point": return asPoint(ob["coordinates"]) elif geom_type == "linestring": return asLineString(ob["coordinates"]) elif geom_type == "polygon": return asPolygon(ob["coordinates"][0], ob["coordinates"][1:]) elif geom_type == "multipoint": return asMultiPoint(ob["coordinates"]) elif geom_type == "multilinestring": return asMultiLineString(ob["coordinates"]) elif geom_type == "multipolygon": return MultiPolygonAdapter(ob["coordinates"], context_type='geojson') elif geom_type == "geometrycollection": geoms = [asShape(g) for g in ob.get("geometries", [])] if len(geoms) == 0: # in this case no asShape call already raised the warning warnings.warn( "The proxy geometries (through the 'asShape()' constructor) " "are deprecated and will be removed in Shapely 2.0. Use the " "'shape()' function instead.", ShapelyDeprecationWarning, stacklevel=2) return GeometryCollection(geoms) else: raise GeometryTypeError("Unknown geometry type: %s" % geom_type)
def substring(geom, start_dist, end_dist, normalized=False): """Return a line segment between specified distances along a LineString Negative distance values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values. If the start distance equals the end distance, a Point is returned. If the start distance is actually beyond the end distance, then the reversed substring is returned such that the start distance is at the first coordinate. Parameters ---------- geom : LineString The geometry to get a substring of. start_dist : float The distance along `geom` of the start of the substring. end_dist : float The distance along `geom` of the end of the substring. normalized : bool, False Whether the distance parameters are interpreted as a fraction of the geometry's length. Returns ------- Union[Point, LineString] The substring between `start_dist` and `end_dist` or a Point if they are at the same location. Raises ------ TypeError If `geom` is not a LineString. Examples -------- >>> from shapely.geometry import LineString >>> from shapely.ops import substring >>> ls = LineString((i, 0) for i in range(6)) >>> ls.wkt 'LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)' >>> substring(ls, start_dist=1, end_dist=3).wkt 'LINESTRING (1 0, 2 0, 3 0)' >>> substring(ls, start_dist=3, end_dist=1).wkt 'LINESTRING (3 0, 2 0, 1 0)' >>> substring(ls, start_dist=1, end_dist=-3).wkt 'LINESTRING (1 0, 2 0)' >>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True).wkt 'LINESTRING (1 0, 2 0)' Returning a `Point` when `start_dist` and `end_dist` are at the same location. >>> substring(ls, 2.5, -2.5).wkt 'POINT (2.5 0)' """ if not isinstance(geom, LineString): raise GeometryTypeError( "Can only calculate a substring of LineString geometries. A %s was provided." % geom.type) # Filter out cases in which to return a point if start_dist == end_dist: return geom.interpolate(start_dist, normalized) elif not normalized and start_dist >= geom.length and end_dist >= geom.length: return geom.interpolate(geom.length, normalized) elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length: return geom.interpolate(0, normalized) elif normalized and start_dist >= 1 and end_dist >= 1: return geom.interpolate(1, normalized) elif normalized and -start_dist >= 1 and -end_dist >= 1: return geom.interpolate(0, normalized) if normalized: start_dist *= geom.length end_dist *= geom.length # Filter out cases where distances meet at a middle point from opposite ends. if start_dist < 0 < end_dist and abs(start_dist) + end_dist == geom.length: return geom.interpolate(end_dist) elif end_dist < 0 < start_dist and abs( end_dist) + start_dist == geom.length: return geom.interpolate(start_dist) start_point = geom.interpolate(start_dist) end_point = geom.interpolate(end_dist) if start_dist < 0: start_dist = geom.length + start_dist # Values may still be negative, if end_dist < 0: # but only in the out-of-range end_dist = geom.length + end_dist # sense, not the wrap-around sense. reverse = start_dist > end_dist if reverse: start_dist, end_dist = end_dist, start_dist if start_dist < 0: start_dist = 0 # to avoid duplicating the first vertex if reverse: vertex_list = [(end_point.x, end_point.y)] else: vertex_list = [(start_point.x, start_point.y)] coords = list(geom.coords) current_distance = 0 for p1, p2 in zip(coords, coords[1:]): if start_dist < current_distance < end_dist: vertex_list.append(p1) elif current_distance >= end_dist: break current_distance += ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5 if reverse: vertex_list.append((start_point.x, start_point.y)) # reverse direction result vertex_list = reversed(vertex_list) else: vertex_list.append((end_point.x, end_point.y)) return LineString(vertex_list)
def split(geom, splitter): """ Splits a geometry by another geometry and returns a collection of geometries. This function is the theoretical opposite of the union of the split geometry parts. If the splitter does not split the geometry, a collection with a single geometry equal to the input geometry is returned. The function supports: - Splitting a (Multi)LineString by a (Multi)Point or (Multi)LineString or (Multi)Polygon - Splitting a (Multi)Polygon by a LineString It may be convenient to snap the splitter with low tolerance to the geometry. For example in the case of splitting a line by a point, the point must be exactly on the line, for the line to be correctly split. When splitting a line by a polygon, the boundary of the polygon is used for the operation. When splitting a line by another line, a ValueError is raised if the two overlap at some segment. Parameters ---------- geom : geometry The geometry to be split splitter : geometry The geometry that will split the input geom Example ------- >>> pt = Point((1, 1)) >>> line = LineString([(0,0), (2,2)]) >>> result = split(line, pt) >>> result.wkt 'GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), LINESTRING (1 1, 2 2))' """ if geom.type in ("MultiLineString", "MultiPolygon"): return GeometryCollection([ i for part in geom.geoms for i in SplitOp.split(part, splitter).geoms ]) elif geom.type == "LineString": if splitter.type in ( "LineString", "MultiLineString", "Polygon", "MultiPolygon", ): split_func = SplitOp._split_line_with_line elif splitter.type in ("Point"): split_func = SplitOp._split_line_with_point elif splitter.type in ("MultiPoint"): split_func = SplitOp._split_line_with_multipoint else: raise GeometryTypeError( "Splitting a LineString with a %s is not supported" % splitter.type) elif geom.type == "Polygon": if splitter.type == "LineString": split_func = SplitOp._split_polygon_with_line else: raise GeometryTypeError( "Splitting a Polygon with a %s is not supported" % splitter.type) else: raise GeometryTypeError("Splitting %s geometry is not supported" % geom.type) return GeometryCollection(split_func(geom, splitter))
def transform(func, geom): """Applies `func` to all coordinates of `geom` and returns a new geometry of the same type from the transformed coordinates. `func` maps x, y, and optionally z to output xp, yp, zp. The input parameters may iterable types like lists or arrays or single values. The output shall be of the same type. Scalars in, scalars out. Lists in, lists out. For example, here is an identity function applicable to both types of input. def id_func(x, y, z=None): return tuple(filter(None, [x, y, z])) g2 = transform(id_func, g1) Using pyproj >= 2.1, this example will accurately project Shapely geometries: import pyproj wgs84 = pyproj.CRS('EPSG:4326') utm = pyproj.CRS('EPSG:32618') project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform g2 = transform(project, g1) Note that the always_xy kwarg is required here as Shapely geometries only support X,Y coordinate ordering. Lambda expressions such as the one in g2 = transform(lambda x, y, z=None: (x+1.0, y+1.0), g1) also satisfy the requirements for `func`. """ if geom.is_empty: return geom if geom.type in ("Point", "LineString", "LinearRing", "Polygon"): # First we try to apply func to x, y, z sequences. When func is # optimized for sequences, this is the fastest, though zipping # the results up to go back into the geometry constructors adds # extra cost. try: if geom.type in ("Point", "LineString", "LinearRing"): return type(geom)(zip(*func(*zip(*geom.coords)))) elif geom.type == "Polygon": shell = type(geom.exterior)( zip(*func(*zip(*geom.exterior.coords)))) holes = list( type(ring)(zip(*func(*zip(*ring.coords)))) for ring in geom.interiors) return type(geom)(shell, holes) # A func that assumes x, y, z are single values will likely raise a # TypeError, in which case we'll try again. except TypeError: if geom.type in ("Point", "LineString", "LinearRing"): return type(geom)([func(*c) for c in geom.coords]) elif geom.type == "Polygon": shell = type( geom.exterior)([func(*c) for c in geom.exterior.coords]) holes = list( type(ring)([func(*c) for c in ring.coords]) for ring in geom.interiors) return type(geom)(shell, holes) elif geom.type.startswith("Multi") or geom.type == "GeometryCollection": return type(geom)([transform(func, part) for part in geom.geoms]) else: raise GeometryTypeError("Type %r not recognized" % geom.type)
def affine_transform(geom, matrix): r"""Returns a transformed geometry using an affine transformation matrix. The coefficient matrix is provided as a list or tuple with 6 or 12 items for 2D or 3D transformations, respectively. For 2D affine transformations, the 6 parameter matrix is:: [a, b, d, e, xoff, yoff] which represents the augmented matrix:: [x'] / a b xoff \ [x] [y'] = | d e yoff | [y] [1 ] \ 0 0 1 / [1] or the equations for the transformed coordinates:: x' = a * x + b * y + xoff y' = d * x + e * y + yoff For 3D affine transformations, the 12 parameter matrix is:: [a, b, c, d, e, f, g, h, i, xoff, yoff, zoff] which represents the augmented matrix:: [x'] / a b c xoff \ [x] [y'] = | d e f yoff | [y] [z'] | g h i zoff | [z] [1 ] \ 0 0 0 1 / [1] or the equations for the transformed coordinates:: x' = a * x + b * y + c * z + xoff y' = d * x + e * y + f * z + yoff z' = g * x + h * y + i * z + zoff """ if geom.is_empty: return geom if len(matrix) == 6: ndim = 2 a, b, d, e, xoff, yoff = matrix if geom.has_z: ndim = 3 i = 1.0 c = f = g = h = zoff = 0.0 matrix = a, b, c, d, e, f, g, h, i, xoff, yoff, zoff elif len(matrix) == 12: ndim = 3 a, b, c, d, e, f, g, h, i, xoff, yoff, zoff = matrix if not geom.has_z: ndim = 2 matrix = a, b, d, e, xoff, yoff else: raise ValueError("'matrix' expects either 6 or 12 coefficients") def affine_pts(pts): """Internal function to yield affine transform of coordinate tuples""" if ndim == 2: for x, y in pts: xp = a * x + b * y + xoff yp = d * x + e * y + yoff yield (xp, yp) elif ndim == 3: for x, y, z in pts: xp = a * x + b * y + c * z + xoff yp = d * x + e * y + f * z + yoff zp = g * x + h * y + i * z + zoff yield (xp, yp, zp) # Process coordinates from each supported geometry type if geom.type in ('Point', 'LineString', 'LinearRing'): return type(geom)(list(affine_pts(geom.coords))) elif geom.type == 'Polygon': ring = geom.exterior shell = type(ring)(list(affine_pts(ring.coords))) holes = list(geom.interiors) for pos, ring in enumerate(holes): holes[pos] = type(ring)(list(affine_pts(ring.coords))) return type(geom)(shell, holes) elif geom.type.startswith('Multi') or geom.type == 'GeometryCollection': # Recursive call # TODO: fix GeometryCollection constructor return type(geom)( [affine_transform(part, matrix) for part in geom.geoms]) else: raise GeometryTypeError('Type %r not recognized' % geom.type)
def _validate_line(self, ob): super()._validate(ob) if not ob.geom_type in ['LinearRing', 'LineString', 'MultiLineString']: raise GeometryTypeError("Only linear types support this operation")