def remove_identical_sindex(geosrs: gpd.GeoSeries, snap_threshold: float) -> gpd.GeoSeries: """ Remove stacked nodes by using a search buffer the size of snap_threshold. """ geosrs_reset = geosrs.reset_index(inplace=False, drop=True) assert isinstance(geosrs_reset, gpd.GeoSeries) geosrs = geosrs_reset spatial_index = geosrs.sindex identical_idxs = [] point: Point for idx, point in enumerate(geosrs.geometry.values): if idx in identical_idxs: continue # point = point.buffer(snap_threshold) if snap_threshold != 0 else point p_candidate_idxs = ( # list(spatial_index.intersection(point.buffer(snap_threshold).bounds)) spatial_index_intersection( spatial_index=spatial_index, coordinates=geom_bounds( safe_buffer(geom=point, radius=snap_threshold)), ) if snap_threshold != 0 else list( spatial_index.intersection(point.coords[0]))) p_candidate_idxs.remove(idx) p_candidates = geosrs.iloc[p_candidate_idxs] inter = p_candidates.distance(point) < snap_threshold colliding = inter.loc[inter] if len(colliding) > 0: index_to_list = colliding.index.to_list() assert len(index_to_list) > 0 assert all(isinstance(i, int) for i in index_to_list) identical_idxs.extend(index_to_list) geosrs_dropped = geosrs.drop(identical_idxs) assert isinstance(geosrs_dropped, gpd.GeoSeries) return geosrs_dropped
def estimate_censoring(self, ) -> float: """ Estimate the amount of censoring as area float value. Censoring is caused by e.g. vegetation. Returns np.nan if no ``censoring_area`` is passed by the user into ``Network`` creation or if the passed GeoDataFrame is empty. """ # Either censoring_area is passed directly or its passed in Network # creation. Otherwise raise ValueError. if self.censoring_area is None: raise ValueError( "Expected censoring_area as an argument or initialized" f" as Network (name:{self.name}) attribute.") if not isinstance(self.censoring_area, gpd.GeoDataFrame): raise TypeError( "Expected censoring_area to be of type gpd.GeoDataFrame." f" Got type={type(self.censoring_area)}") if self.censoring_area.empty: return np.nan # Determine bounds of Network.area_gdf network_area_bounds = total_bounds(self.area_gdf) # Use spatial index to filter censoring polygons that are not near the # network sindex = pygeos_spatial_index(self.censoring_area) index_intersection = spatial_index_intersection( spatial_index=sindex, coordinates=network_area_bounds) candidate_idxs = list( index_intersection if index_intersection is not None else []) if len(candidate_idxs) == 0: return 0.0 candidates = self.censoring_area.iloc[candidate_idxs] # Clip the censoring areas with the network area and calculate the area # sum of leftover area. clipped = gpd.clip(candidates, self.area_gdf) if not isinstance(clipped, (gpd.GeoDataFrame, gpd.GeoSeries)): vals = type(clipped), clipped raise TypeError( f"Expected that clipped is of geopandas data type. Got: {vals}." ) censoring_value = clipped.area.sum() unpacked_value = numpy_to_python_type(censoring_value) assert isinstance(unpacked_value, float) assert unpacked_value >= 0.0 return unpacked_value
def determine_trace_candidates( geom: LineString, idx: int, traces: gpd.GeoDataFrame, spatial_index: Optional[PyGEOSSTRTreeIndex], ) -> gpd.GeoSeries: """ Determine potentially intersecting traces with spatial index. """ if spatial_index is None: logging.error("Expected spatial_index not be None.") return gpd.GeoSeries() assert isinstance(traces, (gpd.GeoSeries, gpd.GeoDataFrame)) assert isinstance(spatial_index, PyGEOSSTRTreeIndex) candidate_idxs = spatial_index_intersection(spatial_index, geom_bounds(geom)) candidate_idxs.remove(idx) candidate_traces: gpd.GeoSeries = traces.geometry.iloc[candidate_idxs] candidate_traces = candidate_traces.loc[ # type: ignore [isinstance(geom, LineString) for geom in candidate_traces.geometry.values] ] return candidate_traces
def populate_sample_cell( sample_cell: Polygon, sample_cell_area: float, traces_sindex: PyGEOSSTRTreeIndex, traces: gpd.GeoDataFrame, nodes: gpd.GeoDataFrame, snap_threshold: float, resolve_branches_and_nodes: bool, ) -> Dict[str, float]: """ Take a single grid polygon and populate it with parameters. Mauldon determination requires that E-nodes are defined for every single sample circle. If correct Mauldon values are wanted `resolve_branches_and_nodes` must be passed as True. This will result in much longer analysis time. """ _centroid = sample_cell.centroid if not isinstance(_centroid, Point): raise TypeError("Expected Point centroid.") centroid = _centroid sample_circle = safe_buffer(centroid, np.sqrt(sample_cell_area) * 1.5) sample_circle_area = sample_circle.area assert sample_circle_area > 0 # Choose geometries that are either within the sample_circle or # intersect it # Use spatial indexing to filter to only spatially relevant traces, # traces and nodes trace_candidates_idx = spatial_index_intersection( traces_sindex, geom_bounds(sample_circle)) trace_candidates = traces.iloc[trace_candidates_idx] assert isinstance(trace_candidates, gpd.GeoDataFrame) if len(trace_candidates) == 0: return determine_topology_parameters( trace_length_array=np.array([]), node_counts=determine_node_type_counts(np.array([]), branches_defined=True), area=sample_circle_area, ) if resolve_branches_and_nodes: # Solve branches and nodes for each cell if wanted # Only way to make sure Mauldon parameters are correct _, nodes = branches_and_nodes( traces=trace_candidates, areas=gpd.GeoSeries([sample_circle], crs=traces.crs), snap_threshold=snap_threshold, ) # node_candidates_idx = list(nodes_sindex.intersection(sample_circle.bounds)) node_candidates_idx = spatial_index_intersection( spatial_index=pygeos_spatial_index(nodes), coordinates=geom_bounds(sample_circle), ) node_candidates = nodes.iloc[node_candidates_idx] # Crop traces to sample circle # First check if any geometries intersect # If not: sample_features is an empty GeoDataFrame if any( trace_candidate.intersects(sample_circle) for trace_candidate in trace_candidates.geometry.values): sample_traces = crop_to_target_areas( traces=trace_candidates, areas=gpd.GeoSeries([sample_circle]), is_filtered=True, keep_column_data=False, ) else: sample_traces = traces.iloc[0:0] if any(node.intersects(sample_circle) for node in nodes.geometry.values): # if any(nodes.intersects(sample_circle)): # TODO: Is node clipping stable? sample_nodes = gpd.clip(node_candidates, sample_circle) assert sample_nodes is not None assert all( isinstance(val, Point) for val in sample_nodes.geometry.values) else: sample_nodes = nodes.iloc[0:0] assert isinstance(sample_nodes, gpd.GeoDataFrame) assert isinstance(sample_traces, gpd.GeoDataFrame) sample_node_type_values = sample_nodes[CLASS_COLUMN].values assert isinstance(sample_node_type_values, np.ndarray) node_counts = determine_node_type_counts(sample_node_type_values, branches_defined=True) topology_parameters = determine_topology_parameters( trace_length_array=sample_traces.geometry.length.values, node_counts=node_counts, area=sample_circle_area, correct_mauldon=resolve_branches_and_nodes, ) return topology_parameters
def choose_geometries(sindex, sample_circle, geometries): candidates_idx = spatial_index_intersection(sindex, geom_bounds(sample_circle)) candidates = geometries.iloc[candidates_idx] assert isinstance(candidates, gpd.GeoDataFrame) return candidates
def get_branch_identities( branches: gpd.GeoSeries, nodes: gpd.GeoSeries, node_identities: list, snap_threshold: float, ) -> List[str]: """ Determine the types of branches for a GeoSeries of branches. i.e. C-C, C-I or I-I, + (C-E, E-E, I-E) >>> branches = gpd.GeoSeries( ... [ ... LineString([(1, 1), (2, 2)]), ... LineString([(2, 2), (3, 3)]), ... LineString([(3, 0), (2, 2)]), ... LineString([(2, 2), (-2, 5)]), ... ] ... ) >>> nodes = gpd.GeoSeries( ... [ ... Point(2, 2), ... Point(1, 1), ... Point(3, 3), ... Point(3, 0), ... Point(-2, 5), ... ] ... ) >>> node_identities = ["X", "I", "I", "I", "E"] >>> snap_threshold = 0.001 >>> get_branch_identities(branches, nodes, node_identities, snap_threshold) ['C - I', 'C - I', 'C - I', 'C - E'] """ assert len(nodes) == len(node_identities) node_spatial_index = pygeos_spatial_index(nodes) branch_identities = [] for branch in branches.geometry.values: assert isinstance(branch, LineString) node_candidate_idxs = spatial_index_intersection( spatial_index=node_spatial_index, coordinates=geom_bounds(branch)) # node_candidate_idxs = list(node_spatial_index.intersection(branch.bounds)) node_candidates = nodes.iloc[node_candidate_idxs] node_candidate_types = [ node_identities[i] for i in node_candidate_idxs ] # Use distance instead of two polygon buffers inter = [ dist < snap_threshold for dist in node_candidates.distance( MultiPoint(list(get_trace_endpoints(branch)))).values ] assert len(inter) == len(node_candidates) # nodes_that_intersect = node_candidates.loc[inter] nodes_that_intersect_types = list(compress(node_candidate_types, inter)) number_of_E_nodes = sum( [inter_id == E_node for inter_id in nodes_that_intersect_types]) number_of_I_nodes = sum( [inter_id == I_node for inter_id in nodes_that_intersect_types]) number_of_XY_nodes = sum([ inter_id in [X_node, Y_node] for inter_id in nodes_that_intersect_types ]) branch_identities.append( determine_branch_identity(number_of_I_nodes, number_of_XY_nodes, number_of_E_nodes)) return branch_identities
def determine_proximal_traces( traces: Union[gpd.GeoSeries, gpd.GeoDataFrame], buffer_value: float, azimuth_tolerance: float, ) -> gpd.GeoDataFrame: """ Determine proximal traces. Takes an input of GeoSeries or GeoDataFrame of LineString geometries and returns a GeoDataFrame with a new column `Merge` which has values of True or False depending on if nearby proximal traces were found. E.g. >>> lines = [ ... LineString([(0, 0), (0, 3)]), ... LineString([(1, 0), (1, 3)]), ... LineString([(5, 0), (5, 3)]), ... LineString([(0, 0), (-3, -3)]), ... ] >>> traces = gpd.GeoDataFrame({"geometry": lines}) >>> buffer_value = 1.1 >>> azimuth_tolerance = 10 >>> determine_proximal_traces(traces, buffer_value, azimuth_tolerance) geometry Merge 0 LINESTRING (0.00000 0.00000, 0.00000 3.00000) True 1 LINESTRING (1.00000 0.00000, 1.00000 3.00000) True 2 LINESTRING (5.00000 0.00000, 5.00000 3.00000) False 3 LINESTRING (0.00000 0.00000, -3.00000 -3.00000) False """ assert isinstance(traces, (gpd.GeoSeries, gpd.GeoDataFrame)) if isinstance(traces, gpd.GeoSeries): traces_as_gdf: gpd.GeoDataFrame = gpd.GeoDataFrame(geometry=traces) else: traces_as_gdf = traces traces_as_gdf.reset_index(inplace=True, drop=True) spatial_index = pygeos_spatial_index(traces_as_gdf) trace: LineString proximal_traces: List[int] = [] for idx, trace in enumerate(traces_as_gdf.geometry.values): candidate_idxs = spatial_index_intersection( spatial_index, geom_bounds(safe_buffer(trace, buffer_value * 5))) candidate_idxs.remove(idx) candidate_traces: Union[ gpd.GeoSeries, gpd.GeoDataFrame] = traces_as_gdf.iloc[candidate_idxs] candidate_traces = candidate_traces.loc[ # type: ignore [ is_within_buffer_distance(trace, other, buffer_value) and is_similar_azimuth(trace, other, tolerance=azimuth_tolerance) for other in candidate_traces.geometry.values ]] if len(candidate_traces) > 0: proximal_traces.extend([ i for i in list(candidate_traces.index) + [idx] # type: ignore if i not in proximal_traces ]) traces_as_gdf[MERGE_COLUMN] = [ i in proximal_traces for i in traces_as_gdf.index ] return traces_as_gdf