def _rings_to_multi_polygon(self, rings): exterior_rings = [] interior_rings = [] for ring in rings: if ring.is_ccw: interior_rings.append(ring) else: exterior_rings.append(ring) polygon_bits = [] # Turn all the exterior rings into polygon definitions, # "slurping up" and interior rings they contain. for exterior_ring in exterior_rings: polygon = sgeom.Polygon(exterior_ring) holes = [] for interior_ring in interior_rings[:]: if polygon.contains(interior_ring): holes.append(interior_ring) interior_rings.remove(interior_ring) polygon_bits.append( (exterior_ring.coords, [ring.coords for ring in holes])) # Any left over "interior" rings need "inverting" with respect # to the boundary. if interior_rings: boundary_poly = sgeom.Polygon(self.boundary) x3, y3, x4, y4 = boundary_poly.bounds bx = (x4 - x3) * 0.1 by = (y4 - y3) * 0.1 x3 -= bx y3 -= by x4 += bx y4 += by for ring in interior_rings: polygon = sgeom.Polygon(ring) x1, y1, x2, y2 = polygon.bounds bx = (x2 - x1) * 0.1 by = (y2 - y1) * 0.1 x1 -= bx y1 -= by x2 += bx y2 += by box = sgeom.box(min(x1, x3), min(y1, y3), max(x2, x4), max(y2, y4)) # Invert the polygon polygon = box.difference(polygon) # Intersect the inverted polygon with the boundary polygon = boundary_poly.intersection(polygon) if not polygon.is_empty: polygon_bits.append(polygon) if polygon_bits: multi_poly = sgeom.MultiPolygon(polygon_bits) else: multi_poly = sgeom.MultiPolygon() return multi_poly
def shape(self): # shapely's idea of "holes" are to subtract everything in the second set # from the first. So let's at least make sure the "first" thing is the # biggest path. paths = self.paths paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) # Very small holes will cause a shape to be rendered as an outline only # they are too small to be rendered and only confuse the auto_fill algorithm. # So let's ignore them if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon( paths[-1]).area < 5: paths = [path for path in paths if shgeo.Polygon(path).area > 3] polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) # There is a great number of "crossing border" errors on fill shapes # If the polygon fails, we can try to run buffer(0) on the polygon in the # hope it will fix at least some of them if not self.shape_is_valid(polygon): why = explain_validity(polygon) message = re.match(r".+?(?=\[)", why) if message.group(0) == "Self-intersection": buffered = polygon.buffer(0) # if we receive a multipolygon, only use the first one of it if type(buffered) == shgeo.MultiPolygon: buffered = buffered[0] # we do not want to break apart into multiple objects (possibly in the future?!) # best way to distinguish the resulting polygon is to compare the area size of the two # and make sure users will not experience significantly altered shapes without a warning if type(buffered) == shgeo.Polygon and math.isclose( polygon.area, buffered.area, abs_tol=0.5): polygon = shgeo.MultiPolygon([buffered]) return polygon
def __mask_to_polygons(mask): """[summary] Args: mask ([type]): [description] Returns: [type]: [description] """ # XXX: maybe this should be merged with __mask_to_polys() defined above import numpy as np from rasterio import features, Affine from shapely import geometry all_polygons = [] for shape, value in features.shapes(mask.astype(np.int16), mask=(mask > 0), transform=Affine(1.0, 0, 0, 0, 1.0, 0)): all_polygons.append(geometry.shape(shape)) all_polygons = geometry.MultiPolygon(all_polygons) if not all_polygons.is_valid: all_polygons = all_polygons.buffer(0) # Sometimes buffer() converts a simple Multipolygon to just a Polygon, # need to keep it a Multi throughout if all_polygons.type == 'Polygon': all_polygons = geometry.MultiPolygon([all_polygons]) return all_polygons
def _create_polygon(shape): if not shape.points: return sgeom.MultiPolygon() # Partition the shapefile rings into outer rings/polygons (clockwise) and # inner rings/holes (anti-clockwise). parts = list(shape.parts) + [None] bounds = zip(parts[:-1], parts[1:]) outer_polygons_and_holes = [] inner_polygons = [] for lower, upper in bounds: polygon = sgeom.Polygon(shape.points[slice(lower, upper)]) if polygon.exterior.is_ccw: inner_polygons.append(polygon) else: outer_polygons_and_holes.append((polygon, [])) # Find the appropriate outer ring for each inner ring. # aka. Group the holes with their containing polygons. for inner_polygon in inner_polygons: for outer_polygon, holes in outer_polygons_and_holes: if outer_polygon.contains(inner_polygon): holes.append(inner_polygon.exterior.coords) break polygon_defns = [(outer_polygon.exterior.coords, holes) for outer_polygon, holes in outer_polygons_and_holes] return sgeom.MultiPolygon(polygon_defns)
def buffer_shpfile(source, target, env): buffer_dist = env['buffer_dist'] with fiona.open(str(source[1])) as coasts: lands = [c for c in coasts if c['properties']['area'] > 10000] land = [sgeom.shape(record['geometry']) for record in lands] land = sgeom.MultiPolygon(land) with fiona.open(str(source[0])) as inshp: driver = inshp.driver crs = inshp.crs schema = inshp.schema polys = [sgeom.shape(record['geometry']) for record in inshp] multi = sgeom.MultiPolygon(polys) dissolved = sops.unary_union(multi) clipped = dissolved.intersection(land) buffered = clipped.buffer(buffer_dist) # deg lat/lon record = { 'properties': { 'BASIN': 'merged' }, 'geometry': sgeom.mapping(buffered) } with fiona.open(str(target[0]), 'w', driver=driver, crs=crs, schema=schema) as outshp: outshp.write(record) return 0
def getMultiPolygon(self, hasZ=False): if hasZ: return geometry.MultiPolygon([ geometry.Polygon([(0, 0, 5), (1, 1, 5), (2, 1, 5), (0, 0, 5)]), geometry.Polygon([(3, 3, 5), (4, 4, 5), (5, 3, 5), (3, 3, 5)]) ]) return geometry.MultiPolygon([ geometry.Polygon([(0, 0), (1, 1), (2, 1), (0, 0)]), geometry.Polygon([(3, 3), (4, 4), (5, 3), (3, 3)]) ])
def shapelyToMultipolygon(anydata): if anydata.type == 'MultiPolygon': return anydata elif anydata.type == 'Polygon': if not anydata.is_empty: return shapely.geometry.MultiPolygon([anydata]) else: return sgeometry.MultiPolygon() else: print(anydata.type, 'shapely conversion aborted') return sgeometry.MultiPolygon()
def _project_multipolygon(self, geometry, src_crs): geoms = [] for geom in geometry.geoms: r = self._project_polygon(geom, src_crs) if r: geoms.extend(r.geoms) if geoms: result = sgeom.MultiPolygon(geoms) else: result = sgeom.MultiPolygon() return result
def set_terrain_type(api_key, hexagons): """ Function that updates terrain in Tygron. Mainly, it updates terrain from land to water and visa versa. In case of water to land, first changes the hexagon terrain to water and then adds a building to it which is subsequently updated to a specific land use. In case of land to water, first removes any building (the land use) from the hexagon and then changes the terrain to water. """ print("Updating terrain in Tygron.") water = [] land = [] new_land_hexagons = [] for feature in hexagons.features: if feature.properties["ghost_hexagon"]: continue if (feature.properties["water"] and feature.properties["z_changed"]): shape = geometry.asShape(feature.geometry) water.append(shape) if feature.properties["tygron_id"] is not None: remove_polygon(api_key, feature.properties["tygron_id"], shape) elif (feature.properties["land"] and feature.properties["z_changed"]): shape = geometry.asShape(feature.geometry) land.append(shape) new_land_hexagons.append(feature) else: continue water_multipolygon = geometry.MultiPolygon(water) land_multipolygon = geometry.MultiPolygon(land) becomes_water = geometry.mapping(water_multipolygon) becomes_land = geometry.mapping(land_multipolygon) r = update_terrain(api_key, becomes_water, terrain_type="water") try: pastebin_url = r.json() print(pastebin_url) except ValueError: print("Water terrain updated in Tygron.") r = update_terrain(api_key, becomes_land, terrain_type="land") new_land_hexagons = FeatureCollection(new_land_hexagons) for feature in new_land_hexagons.features: if feature.properties["tygron_id"] is None: tygron_id = add_standard(api_key) feature.properties["tygron_id"] = tygron_id set_name(api_key, tygron_id, feature.id) shape = geometry.asShape(feature.geometry) add_polygon(api_key, feature.properties["tygron_id"], shape) set_function(api_key, feature.properties["tygron_id"], 0) try: pastebin_url = r.json() print(pastebin_url) except ValueError: print("Land terrain updated in Tygron.") return
def ensure_MultiPolygon(input_geometry): if input_geometry.is_empty: return geometry.MultiPolygon() elif input_geometry.geom_type == 'MultiPolygon': return input_geometry elif input_geometry.geom_type == 'Polygon': return geometry.MultiPolygon([input_geometry]) elif 'Collection' in input_geometry.geom_type: return geometry.MultiPolygon((pol for pol in input_geometry.geoms if pol.geom_type == 'Polygon')) else: return geometry.MultiPolygon()
def descretize_voronoi_to_polys(vor, bbox): points = vor.points lines = [ geometry.LineString(vor.vertices[line]) for line in vor.ridge_vertices if -1 not in line ] result = geometry.MultiPolygon( [poly.intersection(bbox) for poly in ops.polygonize(lines)]) result = geometry.MultiPolygon( [p for p in result] + [p for p in bbox.difference(ops.unary_union(result))]) return result
def voronoi_polygons(X, margin=0): ''' Returns a set of Voronoi polygons corresponding to a set of points X. :param X: Array of points (optional). Numpy array, shape = [n, 2]. :param margin: Minimum margin to extend the outer polygons of the tessellation. Non-negative float. :return: Geopandas data frame. ''' assert isinstance(X, np.ndarray), 'Expecting a numpy array.' assert X.ndim == 2, 'Expecting a two-dimensional array.' assert X.shape[1] == 2, 'Number of columns is different from expected.' n_points = X.shape[0] c1, c2 = np.sort(X[:, 0]), np.sort(X[:, 1]) _diffs = np.array( [max(margin, np.diff(c1).mean()), max(margin, np.diff(c2).mean())]) min_c1, min_c2 = X.min(0) - _diffs max_c1, max_c2 = X.max(0) + _diffs extra_points = np.vstack([ np.vstack([np.repeat(min_c1, n_points), c2]).T, np.vstack([np.repeat(max_c1, n_points), c2]).T, np.vstack([c1, np.repeat(min_c2, n_points)]).T, np.vstack([c1, np.repeat(max_c2, n_points)]).T ]) _X = np.vstack([X, extra_points]) # Define polygons geometry based on tessellation vor = Voronoi(_X) lines = [ geometry.LineString(vor.vertices[li]) for li in vor.ridge_vertices if -1 not in li ] disord = geometry.MultiPolygon(list(polygonize(lines))) ix_order = np.array( [[i for i, di in enumerate(disord) if di.contains(geometry.Point(pi))] for pi in X]).ravel() return geop.GeoDataFrame( {'geometry': geometry.MultiPolygon([disord[i] for i in ix_order])})
def make_cone(self): polygons = [self.makeErrCircle(lat, lon, radius) \ for lat, lon, radius in zip(self.interp_lats, self.interp_lons, \ self.interp_errs)] convex_hulls = [sgeometry.MultiPolygon([polygons[i], polygons[i+1]]).convex_hull \ for i in range(len(polygons)-1)] self.cone = sops.cascaded_union(convex_hulls)
def __mask_to_polys(mask): """[summary] Args: mask ([type]): [description] Returns: [type]: [description] """ import numpy as np import pandas as pd from rasterio import features from shapely import ops, geometry shapes = features.shapes(mask.astype(np.int16), mask > 0) mp = ops.cascaded_union( geometry.MultiPolygon( [geometry.shape(shape) for shape, value in shapes])) if isinstance(mp, geometry.Polygon): polygon_gdf = pd.DataFrame({ 'geometry': [mp], }) else: polygon_gdf = pd.DataFrame({ 'geometry': [p for p in mp], }) return polygon_gdf
def project(coords, p, prj_cen, i): #plot_slice(gg) #plot_slice(list(l.coords)) pp = sg.MultiPolygon([sg.Polygon(a) for a in coords]) minx, miny, maxx, maxy = pp.bounds minx1, miny1, maxx1, maxy1 = p.bounds cen = (maxx + minx) / 2 pp = sa.translate(pp, -cen + prj_cen[0], -(miny - maxy1 - 20)) minx, miny, maxx, maxy = pp.bounds if maxy > i + miny: l = sg.LineString([(int(minx - 3), i + miny), (int(maxx + 3), i + miny)]) intrsct = l.intersection(pp) if type(intrsct) == sg.multilinestring.MultiLineString: for b in intrsct: points = list(b.coords) lines = [ sg.LineString(ln) for ln in list(zip(points, [prj_cen] * 2)) ] ln1, ln2 = [a.intersection(p) for a in lines] p1 = list(lines[0].coords)[0] p2 = list(ln1.interpolate(1).coords)[0] p3 = list(ln2.interpolate(1).coords)[0] p4 = list(lines[1].coords)[0] p = p.difference(sg.Polygon([p1, p2, p3, p4])) return p
def setup_shape(key: str): """ This method returns the shape (or shapes) as 'Polygon' or 'MultyPoligon' type :param key: country ISO2 code/ISO3 code/name/FIPS code :return: the shape as Polygon or Multipolygon """ # importing geojson file country_ids = eucountries_py.CountryList(constants_py.Constants.EU_PATH) uid = country_ids.get_by_key(key) shape_dict = country_ids.get_by_key(uid) # list of poligons (only used for MultiPolygon) poligons = [] if shape_dict['type'] == "MultiPolygon": for polygon in shape_dict['coordinates']: for sub_polygon in polygon: pol = geom_shapely.Polygon(sub_polygon) poligons.append(pol) shape = geom_shapely.MultiPolygon(poligons) else: if shape_dict['type'] == "Polygon": shape = geom_shapely.Polygon(shape_dict['coordinates'][0]) else: raise Exception('Error occurred during setting up the shape') return shape
def run(self): dataset = DataSet.query.filter_by(name=self.args['dataset']).first() volume = Volume.query.filter_by(name=self.args['volume'], dataset=dataset).first() coll_type = self.args['synapse_collection_type'] coll_name = self.args['synapse_collection_name'] collection = SynapseCollection(name=coll_name, volume=volume, synapse_collection_type=coll_type) synapse_file = self.args['synapse_file'] with open(synapse_file, 'r') as fp: syn_anns = json.load(fp) syn_anns = syn_anns['area_lists'] for ann in syn_anns: rings = [] for area in ann['areas']: path = np.array(area['global_path']) path = np.concatenate((path, area['z'] * np.ones( (path.shape[0], 1))), axis=1) rings.append(geometry.Polygon(path)) mpoly = geometry.MultiPolygon(rings) syn = Synapse(oid=ann['oid'], areas=from_shape(mpoly, srid=SYNAPSE_SRID), collection=collection) db.session.add(syn) db.session.add(collection) db.session.commit()
def shape(self): poly_ary = [] for sub_path in self.paths: point_ary = [] last_pt = None for pt in sub_path: if (last_pt is not None): vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) # dbg.write("dp %s\n" % dp) if (dp > 0.01): # I think too-close points confuse shapely. point_ary.append(pt) last_pt = pt else: last_pt = pt if point_ary: poly_ary.append(point_ary) # shapely's idea of "holes" are to subtract everything in the second set # from the first. So let's at least make sure the "first" thing is the # biggest path. # TODO: actually figure out which things are holes and which are shells poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) # print >> sys.stderr, "polygon valid:", polygon.is_valid return polygon
def geodjango_to_shapely(geos_obj): """ Convert geodjango geometry to shapely for plotting etc inputs: x is a sequence of geodjango geometry objects """ assert HAS_GEODJANGO, "Requires Geodjango" geodjango_poly_to_shapely = lambda t: geometry.Polygon(shell=t.coords[0], holes=t.coords[1:]) converters = { geos.Point: lambda t: geometry.Point(t.coords), geos.LineString: lambda t: geometry.LineString(t.coords), geos.Polygon: lambda t: geodjango_poly_to_shapely(t), geos.MultiPolygon: lambda t: geometry.MultiPolygon( [geodjango_poly_to_shapely(x) for x in t]) } if not issubclass(geos_obj.__class__, geos.GEOSGeometry): raise TypeError("Require object that inherits from geos.GEOSGeometry") return converters[type(geos_obj)]( geos_obj) # FIXME: why is PyCharm complaining about this line?!
def add(self, points: List[aecPoint], restart: bool = False) -> bool: """ If restart is True, constructs a new boundary from the delivered list of points. If restart is False, combines the current boundary with boundaries defined by the delivered points. Returns True if successful. Returns False if the delivered points do not resolve to a single non-crossing polygon and leaves the current boundary unchanged. """ try: if restart: boundaries = [] else: boundaries = [self.__boundary] if self.__setBoundary(points): boundaries.append(self.__boundary) boundaries = shapely.MultiPolygon(boundaries) boundary = shapelyOps.unary_union(boundaries) if type(boundary) != shapely.polygon.Polygon: return False points = [ aecPoint(pnt[0], pnt[1]) for pnt in list(boundary.exterior.coords)[:-1] ] return self.__setBoundary(points) return False except Exception: traceback.print_exc() return False
def from_shapely(cls, shape, orient=True): """ Build a spatialpandas MultiPolygon object from a shapely shape Args: shape: A shapely Polygon or MultiPolygon shape orient: If True (default), reorder polygon vertices so that outer shells are stored in counter clockwise order and holes are stored in clockwise order. If False, accept vertices as given. Note that while there is a performance cost associated with this operation some algorithms will not behave properly if the above ordering convention is not followed, so only set orient=False if it is known that this convention is followed in the input data. Returns: spatialpandas MultiPolygon """ import shapely.geometry as sg if orient: if isinstance(shape, sg.Polygon): shape = sg.polygon.orient(shape) elif isinstance(shape, sg.MultiPolygon): shape = sg.MultiPolygon( [sg.polygon.orient(poly) for poly in shape]) shape_parts = cls._shapely_to_coordinates(shape) return cls(shape_parts)
def add_boundary_from_gml(self, gml, hucname): """ Creates Shapely polygon objects from GML returned via ArcServer """ polys = [] namespaces = {'gml': 'http://www.opengis.net/gml/3.2'} for polygon in gml: vertex_list = polygon.text.split(' ') vertex_list = [float(v) for v in vertex_list] coords = list(zip(vertex_list[0::2], vertex_list[1::2])) polys.append(geometry.Polygon(coords)) if len(polys) > 1: # create multipolygon plist = list(itertools.chain.from_iterable(self.polygons.values())) shape = geometry.MultiPolygon(plist) else: shape = polys[0] if hucname not in self.polygons.keys(): self.polygons[hucname] = shape else: raise Exception( f'Huc already exists in WatershedBoundary: {hucname}')
def _intersect_multipolygon(shape, tile_bounds, clip_bounds): """ Return the parts of the MultiPolygon shape which overlap the tile_bounds, each clipped to the clip_bounds. This can be used to extract only the parts of a multipolygon which are actually visible in the tile, while keeping those parts which extend beyond the tile clipped to avoid huge polygons. """ polys = [] for poly in shape.geoms: if tile_bounds.intersects(poly): if not clip_bounds.contains(poly): poly = clip_bounds.intersection(poly) # the intersection operation can make the resulting polygon # invalid. including it in a MultiPolygon would make that # invalid too. instead, we skip it, and hope it wasn't too # important. if not poly.is_valid: continue if poly.type == 'Polygon': polys.append(poly) elif poly.type == 'MultiPolygon': polys.extend(poly.geoms) return geometry.MultiPolygon(polys)
def sparsify(union, subset_paths): """Helper function to discard polygons from a superset that do not intersect at least one geometry from each subset geojson Arguments: union {MultiPolygon} -- superset Multipolygon that overlaps the subset GeoJSONs subset_paths {List of Paths} -- List of Locations of the small disparate GeoJSONs Returns: MultiPolygon -- thinned out version of union """ # Function to delete features that don't intersect another file if server_config.VERBOSE: print("Discarding extraneous polygons") remove_polys = [] for path in subset_paths: sub_multi = geojson_to_shapely_multi(path) for i, super_poly in enumerate(union): if not super_poly.intersects(sub_multi): remove_polys += [i] thinned_polys = [] for i, super_poly in enumerate(union): if not i in remove_polys: thinned_polys += [super_poly] sparse = unary_union([sh.shape(p) for p in thinned_polys]) if isinstance(sparse, sh.Polygon): sparse = sh.MultiPolygon([sparse]) return sparse
def regular_polygons(X, radius, n_angles=8): assert isinstance(X, np.ndarray), 'Expecting a numpy array.' assert X.ndim == 2, 'Expecting a two-dimensional array.' assert X.shape[1] == 2, 'Number of columns is different from expected.' assert isinstance(n_angles, int), 'n_angles must be an integer.' assert n_angles >= 3, 'Angles must be greater than two.' vertex = np.pi * np.linspace(0, 2, n_angles + 1) if isinstance(radius, float): assert radius > 0, 'Radius must be positive.' polys = [ np.vstack([ xi + radius * np.array([np.cos(t), np.sin(t)]) for t in vertex ]) for xi in X ] else: assert isinstance(radius, np.ndarray), 'Expecting a numpy array.' assert radius.ndim == 1, 'Expecting a one-dimensional array.' assert radius.size == X.shape[ 0], 'Array size is different from expected.' polys = [ np.vstack( [xi + ri * np.array([np.cos(t), np.sin(t)]) for t in vertex]) for xi, ri in zip(X, radius) ] return geop.GeoDataFrame({ 'geometry': geometry.MultiPolygon([geometry.Polygon(pi) for pi in polys]) })
def shape(self): poly_ary = [] for sub_path in self.paths: point_ary = [] last_pt = None for pt in sub_path: if (last_pt is not None): vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) # dbg.write("dp %s\n" % dp) if (dp > 0.01): # I think too-close points confuse shapely. point_ary.append(pt) last_pt = pt else: last_pt = pt if len(point_ary) > 2: poly_ary.append(point_ary) if not poly_ary: self.fatal(_("shape %s is so small that it cannot be filled with stitches. Please make it bigger or delete it.") % self.node.get('id')) # shapely's idea of "holes" are to subtract everything in the second set # from the first. So let's at least make sure the "first" thing is the # biggest path. # TODO: actually figure out which things are holes and which are shells poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) if not polygon.is_valid: self.fatal(_("shape is not valid. This can happen if the border crosses over itself.")) return polygon
def unify(json_paths): """Merge multiple GeoJsons with overlapping polygons into a single GeoJSON with "union" applied to all overlapping features Arguments: json_paths {list of Paths} -- Multi-featured GeoJSONs to be merged Returns: MultiPolygon -- shapely multipolygon containing all the polygons from all the geojsons dissolved together """ if server_config.VERBOSE: print("Combining all model outputs") polys = [] for path in json_paths: with open(path) as f: features = json.load(f)["features"] polys += [ sh.shape(feat["geometry"]).buffer(0) for feat in features if sh.shape(feat["geometry"]).geom_type in ["Polygon", "Multipolygon"] ] # Ignore points and lines uni = unary_union(polys) if isinstance(uni, sh.Polygon): uni = sh.MultiPolygon([uni]) return uni
def fix_rings(multipolygon, strict=False): """ This resolves a multipolygon with invalid exterior/interior ring pairing. It does so by first sorting the exteriors by z-order (so that the exteriors contained by the most other exteriors are "higher" & get priority). Then, interiors are assigned to their highest containing exterior ring. This ensures that the "most interior" interior rings are paired with the "most interior" external rings. Requires shapely, may take a bit for shapes with many exteriors/interiors. Parameters --------- multipolygon: a shapely polygon. (should be invalid due to ring ordering) Returns ------- multipolygon: a shapely polygon that should have valid ring ordering. May be invalid due to other reasons. NOTE: This function has undefined behavior for invalid multipolygons. """ from shapely import geometry as geom from shapely.ops import cascaded_union from shapely.validation import explain_validity vexplain = explain_validity(multipolygon) if "hole lies outside shell" not in vexplain.lower(): if strict: from shapely.geos import TopologicalError def tell_user(x): raise TopologicalError(x) else: from warnings import warn as tell_user tell_user("Shape is invalid: \n{}".format(vexplain)) exteriors = [geom.Polygon(part.exterior) for part in multipolygon.geoms] interiors = [ geom.Polygon(interior) for part in multipolygon.geoms for interior in part.interiors ] zorder = [ sum([exterior.contains(other_exterior) for other_exterior in exteriors]) - 1 for exterior in exteriors ] sort_zorder = np.argsort(zorder) zordered_exteriors = np.asarray(exteriors)[sort_zorder] polygons = [[exterior] for exterior in exteriors] for i, exterior in enumerate(zordered_exteriors): owns = [exterior.contains(interior) for interior in interiors] owned_interiors = [ interior for owned, interior in zip(owns, interiors) if owned ] polygons[i] = exterior.difference(cascaded_union(owned_interiors)) interiors = [interior for owned, interior in zip(owns, interiors) if not owned] return geom.MultiPolygon(polygons)
def st_multipolygon_array(draw, min_size=0, max_size=5, geoseries=False): n = draw(st.integers(min_value=min_size, max_value=max_size)) sg_multipolygons = [] for _ in range(n): xmid = draw(st.floats(-50, 50)) ymid = draw(st.floats(-50, 50)) polygon = draw(st_polygon(xmid=xmid, ymid=ymid)) m = draw(st.integers(min_value=1, max_value=4)) # populate polygons with m copies of polygon polygons = [polygon] * m # translate polygons so they don't overlap _, _, last_x1, last_y1 = polygons[0].bounds for j in range(1, m): polygon = scale(polygons[j], 0.8, 0.5) poly_x0, poly_y0, poly_x1, poly_y1 = polygon.bounds new_polygon = translate(polygon, yoff=last_y1 - poly_y0 + 1) _, _, last_x1, last_y1 = new_polygon.bounds polygons[j] = new_polygon sg_multipolygons.append(sg.MultiPolygon(polygons)) result = from_shapely(sg_multipolygons) if geoseries: result = GeoSeries(result) return result
def test_remove_inner_rings(): # Apply to single Polygon, with area tolerance smaller than holes polygon_removerings_withholes = sh_geom.Polygon( shell=[(0, 0), (0, 10), (1, 10), (10, 10), (10, 0), (0,0)], holes=[[(2,2), (2,4), (4,4), (4,2), (2,2)], [(5,5), (5,6), (7,6), (7,5), (5,5)]]) poly_result = geometry_util.remove_inner_rings(polygon_removerings_withholes, min_area_to_keep=1, crs=None) assert isinstance(poly_result, sh_geom.Polygon) assert len(poly_result.interiors) == 2 # Apply to single Polygon, with area tolerance between # smallest hole (= 2m²) and largest (= 4m²) poly_result = geometry_util.remove_inner_rings(polygon_removerings_withholes, min_area_to_keep=3, crs=None) assert isinstance(poly_result, sh_geom.Polygon) assert len(poly_result.interiors) == 1 # Apply to single polygon and remove all holes poly_result = geometry_util.remove_inner_rings(polygon_removerings_withholes, min_area_to_keep=0, crs=None) assert isinstance(poly_result, sh_geom.Polygon) assert len(poly_result.interiors) == 0 polygon_removerings_noholes = sh_geom.Polygon(shell=[(100, 100), (100, 110), (110, 110), (110, 100), (100,100)]) poly_result = geometry_util.remove_inner_rings(polygon_removerings_noholes, min_area_to_keep=0, crs=None) assert isinstance(poly_result, sh_geom.Polygon) assert len(poly_result.interiors) == 0 # Apply to MultiPolygon, with area tolerance between # smallest hole (= 2m²) and largest (= 4m²) multipoly_removerings = sh_geom.MultiPolygon([polygon_removerings_withholes, polygon_removerings_noholes]) poly_result = geometry_util.remove_inner_rings(multipoly_removerings, min_area_to_keep=3, crs=None) assert isinstance(poly_result, sh_geom.MultiPolygon) assert len(poly_result.geoms[0].interiors) == 1