Example #1
0
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
Example #2
0
    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
Example #3
0
    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
Example #4
0
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
Example #6
0
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
Example #7
0
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