Ejemplo n.º 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
Ejemplo n.º 2
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
Ejemplo n.º 3
0
def resolve_trace_candidates(
    trace: LineString,
    idx: int,
    traces_spatial_index: PyGEOSSTRTreeIndex,
    traces: List[LineString],
    snap_threshold: float,
) -> List[LineString]:
    """
    Resolve PyGEOSSTRTreeIndex intersection to actual intersection candidates.
    """
    assert isinstance(trace, LineString)

    # Get trace bounds and extend them
    # minx, miny, maxx, maxy = trace.bounds
    minx, miny, maxx, maxy = geom_bounds(trace)
    extended_bounds = (
        minx - snap_threshold * 20,
        miny - snap_threshold * 20,
        maxx + snap_threshold * 20,
        maxy + snap_threshold * 20,
    )

    # Use extended trace bounds to catch all possible intersecting traces
    trace_candidate_idxs_raw = list(
        traces_spatial_index.intersection(extended_bounds))
    trace_candidate_idxs = [
        i.item() for i in trace_candidate_idxs_raw
        if isinstance(i.item(), int)
    ]
    assert len(trace_candidate_idxs_raw) == len(trace_candidate_idxs)
    assert isinstance(trace_candidate_idxs, list)

    # Remove current trace
    trace_candidate_idxs.remove(idx)

    # Filter to only candidates based on spatial index
    # trace_candidates = traces.iloc[trace_candidate_idxs]
    # using operator.itemgetter() to
    # elements from list
    assert isinstance(trace_candidate_idxs, list)
    assert isinstance(traces, list)
    # trace_candidates = (
    #     list(itemgetter(*trace_candidate_idxs)(traces))
    #     if len(trace_candidate_idxs) > 0
    #     else []
    # )

    trace_candidates = [traces[i] for i in trace_candidate_idxs]

    return trace_candidates
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 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
Ejemplo n.º 7
0
 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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
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