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 random_target_circle(self) -> Tuple[Polygon, Point, float]: """ Get random target area and its centroid and radius. The target area is always within the original target area. """ radius = ( self.random_radius() if self.random_choice == RandomChoice.radius else calc_circle_radius(self.random_area()) ) possible_buffer: Polygon = safe_buffer( self.target_area_centroid, self.max_radius - radius ) random_target_centroid = random_points_within(possible_buffer, 1)[0] random_target_circle: Polygon = safe_buffer(random_target_centroid, radius) return random_target_circle, random_target_centroid, radius
def conditional_linemerge_collection( traces: Union[gpd.GeoDataFrame, gpd.GeoSeries], tolerance: float, buffer_value: float, ) -> Tuple[List[LineString], List[int]]: """ Conditionally linemerge within a collection of LineStrings. Returns the linemerged traces and the idxs of traces that were linemerged. E.g. >>> first = LineString([(0, 0), (0, 2)]) >>> second = LineString([(0, 2.001), (0, 4)]) >>> traces = gpd.GeoSeries([first, second]) >>> tolerance = 5 >>> buffer_value = 0.01 >>> new_traces, idx = LineMerge.conditional_linemerge_collection( ... traces, tolerance, buffer_value ... ) >>> [trace.wkt for trace in new_traces], idx (['LINESTRING (0 0, 0 2, 0 4)'], [0, 1]) """ spatial_index = pygeos_spatial_index(traces) new_traces = [] modified_idx = [] for i, trace in enumerate(traces.geometry): assert isinstance(trace, LineString) trace_candidates_idx: List[int] = list( spatial_index.intersection( geom_bounds(safe_buffer(trace, buffer_value * 2)))) trace_candidates_idx.remove(i) if len(trace_candidates_idx) == 0 or i in modified_idx: continue for idx in trace_candidates_idx: if idx in modified_idx or i in modified_idx: continue trace_candidate = traces.geometry.iloc[idx] merged = LineMerge.conditional_linemerge( trace, trace_candidate, tolerance=tolerance, buffer_value=buffer_value, ) if merged is not None: new_traces.append(merged) modified_idx.append(i) modified_idx.append(idx) break return new_traces, modified_idx
def assess_coverage(target_centroid: Point, radius: float, coverage_gdf: gpd.GeoDataFrame) -> float: """ Determine the coverage within a circle. Based on manually digitized gpkg of coverage. """ circle = safe_buffer(target_centroid, radius=radius) index_intersection = pygeos_spatial_index(coverage_gdf).intersection( circle.bounds) candidate_idxs = list( index_intersection if index_intersection is not None else []) if len(candidate_idxs) == 0: return 0.0 candidates = coverage_gdf.iloc[candidate_idxs] coverage_area = gpd.clip(candidates, circle).area.sum() assert isinstance(coverage_area, float) return coverage_area
def segment_within_buffer( linestring: LineString, multilinestring: MultiLineString, snap_threshold: float, snap_threshold_error_multiplier: float, overlap_detection_multiplier: float, ) -> bool: """ Check if segment is within buffer of multilinestring. First check if given linestring completely overlaps any part of multilinestring and if it does, returns True. Next it starts to segmentize the multilinestring to smaller linestrings and consequently checks if these segments are completely within a buffer made of the given linestring. It also checks that the segment size is reasonable. TODO: segmentize_linestring is very inefficient. """ # Test for a single segment overlap if linestring.overlaps(multilinestring): return True buffered_linestring = safe_buffer( linestring, snap_threshold * snap_threshold_error_multiplier ) assert isinstance(linestring, LineString) assert isinstance(buffered_linestring, Polygon) assert isinstance(multilinestring, MultiLineString) assert buffered_linestring.area > 0 min_x, min_y, max_x, max_y = geom_bounds(buffered_linestring) # Crop MultiLineString near to the buffered_linestring cropped_mls = buffered_linestring.intersection(multilinestring) # Check for cases with no chance of stacking if cropped_mls.is_empty or ( isinstance(cropped_mls, LineString) and cropped_mls.length < snap_threshold * overlap_detection_multiplier ): return False assert isinstance(cropped_mls, (MultiLineString, LineString)) all_segments: List[Tuple[Tuple[float, float], Tuple[float, float]]] = [] ls: LineString # Create list of LineStrings from within the crop mls_geoms: List[LineString] = ( list(cropped_mls.geoms) if isinstance(cropped_mls, MultiLineString) else [cropped_mls] ) # Iterate over list of LineStrings for ls in mls_geoms: all_segments.extend( segmentize_linestring(ls, snap_threshold * overlap_detection_multiplier) ) for start, end in all_segments: if within_bounds(*start, min_x, min_y, max_x, max_y) and within_bounds( *end, min_x, min_y, max_x, max_y ): ls = LineString([start, end]) if ls.length > snap_threshold * overlap_detection_multiplier and ls.within( buffered_linestring ): return True return False
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 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