예제 #1
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
예제 #2
0
def sample_grid(
    grid: gpd.GeoDataFrame,
    traces: gpd.GeoDataFrame,
    nodes: gpd.GeoDataFrame,
    branches: gpd.GeoDataFrame,
    snap_threshold: float,
    resolve_branches_and_nodes: bool = False,
) -> gpd.GeoDataFrame:
    """
    Populate a sample polygon grid with geometrical and topological parameters.
    """
    sample_cell_area = grid.geometry.iloc[0].area
    assert sample_cell_area != 0
    # String identifiers as parameters
    # dict with lists of parameter values
    params = dict()  # type: Dict[str, list]
    # Iterate over sample cells
    # Uses a buffer 1.5 times the size of the sample_cell side length
    # Make sure index does not cause issues TODO
    traces_reset, nodes_reset, branches_reset = (
        traces.reset_index(drop=True),
        nodes.reset_index(drop=True),
        branches.reset_index(drop=True),
    )
    assert isinstance(traces_reset, gpd.GeoDataFrame)
    assert isinstance(nodes_reset, gpd.GeoDataFrame)
    assert isinstance(branches_reset, gpd.GeoDataFrame)
    traces, nodes, branches = traces_reset, nodes_reset, branches_reset
    # [gdf.reset_index(inplace=True, drop=True) for gdf in (traces, nodes)]
    traces_sindex = pygeos_spatial_index(traces)
    # nodes_sindex = pygeos_spatial_index(nodes)

    params_for_cells = list(
        map(
            lambda sample_cell: populate_sample_cell(
                sample_cell=sample_cell,
                sample_cell_area=sample_cell_area,
                traces_sindex=traces_sindex,
                traces=traces,
                nodes=nodes,
                branches=branches,
                snap_threshold=snap_threshold,
                resolve_branches_and_nodes=resolve_branches_and_nodes,
            ),
            grid.geometry.values,
        ))
    for key in [param.value.name for param in Param]:
        assert isinstance(key, str)
        try:
            params[key] = [cell_param[key] for cell_param in params_for_cells]
        except KeyError:
            continue
    for key, value in params.items():
        grid[key] = value
    return grid
예제 #3
0
def snap_traces(
    traces: List[LineString],
    snap_threshold: float,
    areas: Optional[List[Union[Polygon, MultiPolygon]]] = None,
    final_allowed_loop=False,
) -> Tuple[List[LineString], bool]:
    """
    Snap traces to end exactly at other traces.
    """
    if len(traces) == 0:
        return ([], False)
    # Only handle LineStrings
    assert all(isinstance(trace, LineString) for trace in traces)

    # Spatial index for traces
    traces_spatial_index = pygeos_spatial_index(geodataset=gpd.GeoSeries(traces))

    # Collect simply snapped (and non-snapped) traces to list
    simply_snapped_traces, simple_changes = zip(
        *[
            snap_trace_simple(
                idx,
                trace,
                snap_threshold,
                traces,
                traces_spatial_index,
                final_allowed_loop=final_allowed_loop,
            )
            for idx, trace in enumerate(traces)
        ]
    )
    assert len(simply_snapped_traces) == len(traces)
    simply_snapped_traces_list: List[LineString] = list(simply_snapped_traces)

    # Collect snapped (and non-snapped) traces to list
    snapped_traces, changes = zip(
        *[
            snap_others_to_trace(
                idx=idx,
                trace=trace,
                snap_threshold=snap_threshold,
                traces_spatial_index=traces_spatial_index,
                areas=areas,
                traces=simply_snapped_traces_list,
                final_allowed_loop=final_allowed_loop,
            )
            for idx, trace in enumerate(simply_snapped_traces)
        ]
    )

    assert len(snapped_traces) == len(simply_snapped_traces)
    assert all(isinstance(ls, LineString) for ls in snapped_traces)

    return list(snapped_traces), any(changes + simple_changes)
예제 #4
0
파일: network.py 프로젝트: nialov/fractopo
    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
예제 #5
0
def test_populate_sample_cell(sample_cell, traces, snap_threshold):
    """
    Test populate_sample_cell.
    """
    result = contour_grid.populate_sample_cell(
        sample_cell,
        sample_cell.area,
        pygeos_spatial_index(traces),
        nodes=gpd.GeoDataFrame(),
        traces=traces,
        snap_threshold=snap_threshold,
        resolve_branches_and_nodes=True,
    )

    assert isinstance(result, dict)
예제 #6
0
def test_snap_trace_simple(
    idx,
    trace,
    snap_threshold,
    traces,
    intersects_idx,
):
    """
    Test snap_trace_simple.
    """
    traces_spatial_index = general.pygeos_spatial_index(gpd.GeoSeries(traces))
    result, was_simple_snapped = branches_and_nodes.snap_trace_simple(
        idx, trace, snap_threshold, traces, traces_spatial_index)
    if intersects_idx is not None:
        assert was_simple_snapped
    assert result.intersects(traces[intersects_idx])
예제 #7
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
예제 #8
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
예제 #9
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
예제 #10
0
def node_identities_from_branches(
    branches: gpd.GeoSeries,
    areas: Union[gpd.GeoSeries, gpd.GeoDataFrame],
    snap_threshold: float,
) -> Tuple[List[Point], List[str]]:
    """
    Resolve node identities from branch data.

    >>> branches_list = [
    ...     LineString([(0, 0), (1, 1)]),
    ...     LineString([(2, 2), (1, 1)]),
    ...     LineString([(2, 0), (1, 1)]),
    ... ]
    >>> area_polygon = Polygon([(-5, -5), (-5, 5), (5, 5), (5, -5)])
    >>> branches = gpd.GeoSeries(branches_list)
    >>> areas = gpd.GeoSeries([area_polygon])
    >>> snap_threshold = 0.001
    >>> nodes, identities = node_identities_from_branches(
    ...     branches, areas, snap_threshold
    ... )
    >>> [node.wkt for node in nodes]
    ['POINT (0 0)', 'POINT (1 1)', 'POINT (2 2)', 'POINT (2 0)']
    >>> identities
    ['I', 'Y', 'I', 'I']

    """
    # Get list of all branch endpoints
    all_endpoints: List[Point] = list(
        chain(*[
            list(get_trace_endpoints(branch))
            for branch in branches.geometry.values
        ]))
    # Collect into GeoSeries
    all_endpoints_geoseries = gpd.GeoSeries(all_endpoints)

    # Get spatial index
    endpoints_spatial_index = pygeos_spatial_index(all_endpoints_geoseries)

    # Collect resolved nodes
    collected_nodes: Dict[str, Tuple[Point, str]] = dict()

    for idx, endpoint in enumerate(all_endpoints):

        # Do not resolve nodes that have already been resolved
        if endpoint.wkt in collected_nodes:
            continue

        # Determine node identity
        identity = node_identity(
            endpoint=endpoint,
            idx=idx,
            areas=areas,
            endpoints_geoseries=all_endpoints_geoseries,
            endpoints_spatial_index=endpoints_spatial_index,
            snap_threshold=snap_threshold,
        )

        # Add to resolved
        collected_nodes[endpoint.wkt] = (endpoint, identity)

    if len(collected_nodes) == 0:
        return [], []

    # Collect into two lists
    values = list(collected_nodes.values())
    nodes = [value[0] for value in values]
    identities = [value[1] for value in values]

    return nodes, identities
예제 #11
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