def convert_to_points( data: gpd.GeoDataFrame, crs: dict, ) -> gpd.GeoDataFrame: data.geometry = data.geometry.centroid data = data.to_crs(crs) return data
def reduce_precision(ingeo: GDF, precision: int = 3) -> GDF: """ Reduces the number of after comma decimals of a shapely Polygon or geodataframe geometries. GeoJSON specification recommends 6 decimal places for latitude and longitude which equates to roughly 10cm of precision (https://github.com/perrygeo/geojson-precision). :param ingeo: input geodataframe. :param precision: number of after comma values that should remain. :return: Result polygon or geodataframe, same type as input. """ @typechecked def _reduce_precision(poly: Polygon, precision: int) -> Polygon: geojson = shapely.geometry.mapping(poly) try: geojson["coordinates"] = np.round(np.array(geojson["coordinates"]), precision).tolist() except TypeError as e: # There is a strange polygon for which this does not work TODO! figure it out! pass poly = shapely.geometry.shape(geojson) if ( not poly.is_valid ): # Too low precision can potentially lead to invalid polygons due to line overlap effects. poly = poly.buffer(0) return poly ingeo.geometry = ingeo.geometry.apply( lambda _p: _reduce_precision(poly=_p, precision=precision)) return ingeo
def get_pair_footprints(pair_ids: List, plot=True, save_path=None): """ Plot or save overlapping areas between pairs of NAC images :param save_path: Path to output a vector file of the geometries, or None for no output. File based on extension. :param plot: Create a figure :param pair_ids: Pair ids, a list given like ['M106761561LExxM1101080055RE', 'M1096364254RExxM1142334242LE'] :return: GeoDataFrame containing the pair footprints """ pairs = [pair_id.split('xx') for pair_id in pair_ids] df = DataFrame(pairs, index=pair_ids, columns=['prod_id_0', 'prod_id_1']) df = df.applymap(get_geometry_from_ODE).applymap(wkt.loads) df['intersection'] = df.apply(lambda a: a[0].intersection(a[1]), axis='columns') gdf = GeoDataFrame(df, geometry='intersection') gdf['pair_ids'] = gdf.index.values if save_path: if save_path.endswith('.json'): save_driver = 'GeoJSON' gdf.drop(['prod_id_0', 'prod_id_1'], axis='columns').to_file(save_path, driver=save_driver) if plot: gdf.geometry = gdf.geometry.boundary intersection_plot = gdf.plot(column='pair_ids', legend=True, legend_kwds={ 'loc': 'center left', 'bbox_to_anchor': (1, 0.5) }) pyplot.xlabel('Longitude, degrees E') pyplot.ylabel('Latitude, degrees N') return gdf
def drop_z(map_: gpd.GeoDataFrame): """Drops z attribute""" if pygeos.has_z(map_.geometry).any(): warnings.warn( "Geometry contains Z co-ordinates. Removed from Map3D (height attribute)" ) map_.geometry = pygeos.apply(map_.geometry, lambda x: x, include_z=False) return map_
def to_pixelcoords( ingeo: GDF, reference_bounds: Union[rasterio.coords.BoundingBox, tuple], scale: bool = False, nrows: int = None, ncols: int = None, ) -> GDF: """ Converts projected polygon coordinates to pixel coordinates of an image array. Subtracts point of origin, scales to pixelcoordinates. :param ingeo: input geodataframe or shapely Polygon. :param reference_bounds: Bounding box object or tuple of reference (e.g. image chip) in format (left, bottom, right, top) :param scale: Scale the polygons to the image size/resolution. Requires image array nrows and ncols parameters. :param nrows: image array nrows, required for scale. :param ncols: image array ncols, required for scale. :return:Result polygon or geodataframe, same type as input. """ # TODO - Maxime this function should be just to polygon def _to_pixelcoords(poly: Polygon, reference_bounds, scale, nrows, ncols): try: minx, miny, maxx, maxy = reference_bounds w_poly, h_poly = (maxx - minx, maxy - miny) except (TypeError, ValueError): raise Exception( f"reference_bounds argument is of type {type(reference_bounds)}, needs to be a tuple or rasterio bounding box " f"instance. Can be delineated from transform, nrows, ncols via rasterio.transform.reference_bounds" ) # Subtract point of origin of image bbox. x_coords, y_coords = poly.exterior.coords.xy p_origin = shapely.geometry.Polygon( [[x - minx, y - miny] for x, y in zip(x_coords, y_coords)]) if scale is False: return p_origin elif scale is True: if ncols is None or nrows is None: raise ValueError("ncols and nrows required for scale") x_scaler = ncols / w_poly y_scaler = nrows / h_poly return shapely.affinity.scale(p_origin, xfact=x_scaler, yfact=y_scaler, origin=(0, 0, 0)) ingeo.geometry = ingeo.geometry.apply(lambda _p: _to_pixelcoords( poly=_p, reference_bounds=reference_bounds, scale=scale, nrows=nrows, ncols=ncols, )) return ingeo
def split_edge_at_points(edge, points, tolerance=1e-9): """Split edge at point/multipoint""" try: segments = split_line(edge.geometry, points, tolerance) except ValueError: # if splitting fails, e.g. becuase points is empty GeometryCollection segments = [edge.geometry] edges = GeoDataFrame([edge] * len(segments)) edges.geometry = segments return edges
def _vectorize_single_raster(self, raster, affine_transform, crs, timestamp=None): """Vectorizes a data slice of a single time component :param raster: Numpy array or shape (height, width, channels) :type raster: numpy.ndarray :param affine_transform: Object holding a transform vector (i.e. geographical location vector) of the raster :type affine_transform: affine.Affine :param crs: Coordinate reference system :type crs: sentinelhub.CRS :param timestamp: Time of the data slice :type timestamp: datetime.datetime :return: Vectorized data :rtype: geopandas.GeoDataFrame """ mask = None if self.values: mask = np.zeros(raster.shape, dtype=bool) for value in self.values: mask[raster == value] = True geo_list = [] value_list = [] for idx in range(raster.shape[-1]): for geojson, value in rasterio.features.shapes( raster[..., idx], mask=None if mask is None else mask[..., idx], transform=affine_transform, **self.rasterio_params, ): geo_list.append(shapely.geometry.shape(geojson)) value_list.append(value) series_dict = { self.values_column: pd.Series(value_list, dtype=self.raster_dtype) } if timestamp is not None: series_dict["TIMESTAMP"] = pd.to_datetime([timestamp] * len(geo_list)) vector_data = GeoDataFrame(series_dict, geometry=geo_list, crs=crs.pyproj_crs()) if not vector_data.geometry.is_valid.all(): vector_data.geometry = vector_data.geometry.buffer(0) return vector_data
def clip( df: GDF, clip_poly: Polygon, explode_mp_: bool = False, keep_biggest_poly_: bool = False, ) -> GDF: """ Filter and clip geodataframe to clipping geometry. The clipping geometry needs to be in the same projection as the geodataframe. :param df: input geodataframe :param clip_poly: Clipping polygon geometry, needs to be in the same crs as the input geodataframe. :param explode_mp_: Applies explode_mp function. Append dataframe rows for each polygon in potential multipolygons that were created by the intersection. Resets the dataframe index! :param keep_biggest_poly_: Applies keep_biggest_poly function. Replaces MultiPolygons with the biggest polygon contained in the MultiPolygon. :return: Result geodataframe. """ df = df[df.geometry.intersects(clip_poly)].copy() df.geometry = df.geometry.apply(lambda _p: _p.intersection(clip_poly)) # !TODO: Maxime's comments - return df # df = gpd.overlay(df, clip_poly, how='intersection') # Slower. # !TODO: Maxime's comments - this shoud return just the clipped, should not have explosions and biggest polly row_idxs_mp = df.index[df.geometry.geom_type == "MultiPolygon"].tolist() if not row_idxs_mp: return df elif not explode_mp_ and (not keep_biggest_poly_): warnings.warn( f"Warning, intersection resulted in {len(row_idxs_mp)} split multipolygons. Use " f"explode_mp_=True or keep_biggest_poly_=True.") return df elif explode_mp_ and keep_biggest_poly_: raise ValueError( 'You can only use one of "explode_mp_" or "keep_biggest_poly_"!') elif explode_mp_: return explode_mp(df) elif keep_biggest_poly_: return keep_biggest_poly(df)
def read_vector_file( self, filename: str = "aoi.geojson", as_dataframe: bool = False ) -> Union[Dict, GeoDataFrame]: """ Reads vector files (geojson, shapefile, kml, wkt) to a feature collection, for use as the aoi geometry in the workflow input parameters (see get_input_parameters). Example aoi fiels are provided, e.g. example/data/aoi_Berlin.geojson Args: filename: File path of the vector file. as_dataframe: Return type, default FeatureCollection, GeoDataFrame if True. Returns: Feature Collection """ suffix = Path(filename).suffix if suffix == ".kml": gpd.io.file.fiona.drvsupport.supported_drivers["KML"] = "rw" df = gpd.read_file(filename, driver="KML") elif suffix == ".wkt": with open(filename) as wkt_file: wkt = wkt_file.read() df = pd.DataFrame({"geometry": [wkt]}) df["geometry"] = df["geometry"].apply(shapely.wkt.loads) df = GeoDataFrame(df, geometry="geometry", crs=4326) else: df = gpd.read_file(filename) if df.crs.to_string() != "EPSG:4326": df = df.to_crs(epsg=4326) df.geometry = df.geometry.buffer(0) # TODO: Explode multipolygons (if neccessary as union in aoi anyway most often). # TODO: Have both bboxes for each feature and overall? if as_dataframe: return df else: return df.__geo_interface__
def invert_y_axis(ingeo: GDF, reference_height: int) -> GDF: """ Invert y-axis of polygon or geodataframe geometries in reference to a bounding box e.g. of an image chip. Usage e.g. for COCOJson format. :param ingeo: Input Polygon or geodataframe. :param reference_height: Height (in coordinates or rows) of reference object (polygon or image, e.g. image chip. :return: Result polygon or geodataframe, same type as input. """ def _invert_y_axis(poly: Polygon = ingeo, reference_height=reference_height): x_coords, y_coords = poly.exterior.coords.xy p_inverted_y_axis = shapely.geometry.Polygon( [[x, reference_height - y] for x, y in zip(x_coords, y_coords)]) return p_inverted_y_axis ingeo.geometry = ingeo.geometry.apply( lambda _p: _invert_y_axis(poly=_p, reference_height=reference_height)) return ingeo
def get_footprints(product_ids: List, plot=True, save_path=None): """ Plot footprints of images, given PDS (Planetary Data System) product IDs :param product_ids: Product id, for example M106761561LE :return: GeoDataFrame containing the plot footprints """ df = DataFrame({'index': product_ids, 'footprint': product_ids}) df['footprint'] = df['footprint'].apply(get_geometry_from_ODE).apply( wkt.loads) gdf = GeoDataFrame(df, geometry='footprint') gdf.geometry = gdf.geometry.boundary footprint_plot = gdf.plot(column='index', legend=True, legend_kwds={ 'loc': 'center left', 'bbox_to_anchor': (1, 0.5) }) pyplot.xlabel('Longitude, degrees E') pyplot.ylabel('Latitude, degrees N') return gdf
def clip( df: GDF, clip_poly: Polygon, explode_mp_: bool = False, keep_biggest_poly_: bool = False, ) -> GDF: """Filter and clip geodataframe to clipping geometry. The clipping geometry needs to be in the same projection as the geodataframe. Args: df: input geodataframe clip_poly: Clipping polygon geometry, needs to be in the same crs as the input geodataframe. explode_mp_: Applies explode_mp function. Append dataframe rows for each polygon in potential multipolygons that were created by the intersection. Resets the dataframe index! keep_biggest_poly_: Applies keep_biggest_poly function. Drops Multipolygons by only keeping the Polygon with the biggest area. Returns: Result geodataframe. """ df = df[df.geometry.intersects(clip_poly)].copy() df.geometry = df.geometry.apply(lambda _p: _p.intersection(clip_poly)) # df = gpd.overlay(df, clip_poly, how='intersection') # Slower. row_idxs_mp = df.index[df.geometry.geom_type == 'MultiPolygon'].tolist() if not row_idxs_mp: return df elif not explode_mp_ and (not keep_biggest_poly_): warnings.warn( f"Warning, intersection resulted in {len(row_idxs_mp)} split multipolygons. Use " f"explode_mp_=True or keep_biggest_poly_=True.") return df elif explode_mp_ and keep_biggest_poly_: raise ValueError( 'You can only use only "explode_mp" or "keep_biggest"!') elif explode_mp_: return explode_mp(df) elif keep_biggest_poly_: return keep_biggest_poly(df)
def append_raw_overhanging_PV_installations_to_intersected_installations( self, raw_overhanging_PV_installations: gpd.GeoDataFrame = None, raw_PV_installations_on_rooftop: gpd.GeoDataFrame = None, ) -> gpd.GeoDataFrame: """ PV polygons which do not intersect with a rooftop polygon, although they do border to a rooftop, are matched to their nearest rooftop geometry and appended to the GeoDataFrame listing all rooftop PV polygons Parameters ---------- raw_overhanging_PV_installations: GeoPandas.GeoDataFrame GeoDataFrame which specifies all the PV polygons which border to a rooftop, but are not intersected with a rooftop geometry raw_PV_installations_on_rooftop: GeoPandas.GeoDataFrame GeoDataFrame which specifies all the PV polygons which are intersected with a rooftop geometry Returns ------- GeoPandas.GeoDataFrame GeoDataFrame where overhanging PV installations have been enriched with the attributes of the closest rooftop and appended to raw_PV_installations_on_rooftop """ # IMPORTANT: if ckdnearest is used always reset_index before raw_overhanging_PV_installations = raw_overhanging_PV_installations.reset_index( drop=True) raw_overhanging_PV_installations.rename( columns={"identifier": "identifier_diff"}, inplace=True) # Extract centroid from intersected PV polygons while preserving their polygon geometry raw_PV_installations_on_rooftop[ "geometry_intersected_polygon"] = raw_PV_installations_on_rooftop[ "geometry"] raw_PV_installations_on_rooftop[ "geometry"] = raw_PV_installations_on_rooftop["geometry"].centroid raw_PV_installations_on_rooftop[ "centroid_intersect"] = raw_PV_installations_on_rooftop["geometry"] raw_overhanging_pv_installations_enriched_with_closest_rooftop_data = self.enrich_raw_overhanging_pv_installations_with_closest_rooftop_attributes( raw_overhanging_PV_installations, raw_PV_installations_on_rooftop) raw_PV_installations_on_rooftop.geometry = ( raw_PV_installations_on_rooftop.geometry_intersected_polygon) raw_PV_installations_on_rooftop = raw_PV_installations_on_rooftop[[ "raw_area", "identifier", "Area", "Azimuth", "Building_I", "City", "PostalCode", "RoofTopID", "RooftopTyp", "Street", "StreetNumb", "Tilt", "area_inter", "geometry", ]] # Append the dataframe of all raw overhanging PV installations, enriched with the # rooftop attributes of their nearest rooftop, to the dataframe of all intersected PV installations # Note 1: Attributes starting with capital letters specify rooftop attributes. # Note 2: The geometry of the overhanging PV installations is not yet dissolved with the geometry of the # intersected PV installations raw_PV_installations_on_rooftop = gpd.GeoDataFrame( raw_PV_installations_on_rooftop.append( raw_overhanging_pv_installations_enriched_with_closest_rooftop_data )).reset_index(drop=True) return raw_PV_installations_on_rooftop
def transform_islands(islands: GeoDataFrame) -> GeoDataFrame: islands.geometry = islands.buffer(0) islands = islands[['Id', 'geometry']] islands['FID'] = islands.Id - 1 islands = islands.set_index('FID', drop=False, verify_integrity=True) return islands
def asGdf(geoms, crs=None): gdf = GeoDataFrame(crs=crs) gdf.geometry = geoms return gdf