Example #1
0
def simple_snap(trace: LineString, trace_candidates: List[LineString],
                snap_threshold: float) -> Tuple[LineString, bool]:
    """
    Modify conditionally trace to snap to any of trace_candidates.

    E.g.

    >>> trace = LineString([(0, 0), (1, 0), (2, 0), (3, 0)])
    >>> trace_candidates = gpd.GeoSeries(
    ...     [LineString([(3.0001, -3), (3.0001, 0), (3, 3)])]
    ... )
    >>> snap_threshold = 0.001
    >>> snapped = simple_snap(trace, trace_candidates, snap_threshold)
    >>> snapped[0].wkt, snapped[1]
    ('LINESTRING (0 0, 1 0, 2 0, 3.0001 0)', True)

    Do not snap overlapping.

    >>> trace = LineString([(0, 0), (1, 0), (2, 0), (3.0002, 0)])
    >>> trace_candidates = gpd.GeoSeries(
    ...     [LineString([(3.0001, -3), (3.0001, 0), (3, 3)])]
    ... )
    >>> snap_threshold = 0.001
    >>> snapped = simple_snap(trace, trace_candidates, snap_threshold)
    >>> snapped[0].wkt, snapped[1]
    ('LINESTRING (0 0, 1 0, 2 0, 3.0002 0)', False)
    """
    trace_endpoints = get_trace_endpoints(trace)
    traces_to_snap_to = [
        candidate for candidate in trace_candidates if any(
            endpoint.distance(candidate) < snap_threshold
            for endpoint in trace_endpoints)
    ]
    replace_endpoint: Dict[str, Point] = dict()
    for trace_to_snap_to in traces_to_snap_to:
        assert isinstance(trace_to_snap_to, LineString)

        # Get coordinate points of trace_to_snap_to expect endpoints
        coord_points = get_trace_coord_points(trace_to_snap_to)[1:-1]
        if len(coord_points) == 0:
            # Cannot snap trace to any point as there are only endpoints
            continue

        # Check both trace endpoints
        for endpoint in trace_endpoints:

            # Check for already existing snap
            if any(
                    endpoint.intersects(coord_point)
                    for coord_point in coord_points):
                # Already snapped
                continue

            # Get distances between endpoint in coord_points
            distances: List[float] = [
                coord_point.distance(endpoint) for coord_point in coord_points
            ]
            # If not within snap_threshold -> continue
            if not min(distances) < snap_threshold:
                continue

            # Get intersection points
            intersection_points = line_intersection_to_points(
                first=trace, second=trace_to_snap_to)
            # Check for overlapping snap
            if any(
                    intersection_point.distance(endpoint) < snap_threshold
                    for intersection_point in intersection_points):
                # Overlapping snap
                # These are not fixed here and fallback to the other snapping
                # method
                continue

            sorted_points = sorted(zip(coord_points, distances),
                                   key=lambda vals: vals[1])
            if endpoint.wkt in replace_endpoint:
                error = "Found endpoint in replace_endpoint dict."
                logging.error(
                    error,
                    extra=(dict(
                        endpoint_wkt=endpoint.wkt,
                        replace_endpoint_len=len(replace_endpoint),
                    )),
                )
                raise ValueError(error)
            assert endpoint.wkt not in replace_endpoint
            replace_endpoint[endpoint.wkt] = sorted_points[0][0]

    # If not replacing is done -> return original
    if len(replace_endpoint) == 0:
        return trace, False

    # Replace one of the endpoints based on replace_endpoint
    trace_coords = get_trace_coord_points(trace)
    trace_coords = [
        point
        if point.wkt not in replace_endpoint else replace_endpoint[point.wkt]
        for point in trace_coords
    ]

    # Return modified
    modified = LineString(trace_coords)
    return modified, True
Example #2
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
Example #3
0
def snap_others_to_trace(
    idx: int,
    trace: LineString,
    snap_threshold: float,
    traces: List[LineString],
    traces_spatial_index: PyGEOSSTRTreeIndex,
    areas: Optional[List[Union[Polygon, MultiPolygon]]],
    final_allowed_loop: bool = False,
) -> Tuple[LineString, bool]:
    """
    Determine whether and how to snap `trace` to `traces`.

    E.g.

    Trace gets new coordinates to snap other traces to it:

    >>> idx = 0
    >>> trace = LineString([(0, 0), (1, 0), (2, 0), (3, 0)])
    >>> snap_threshold = 0.001
    >>> traces = [trace, LineString([(1.5, 3), (1.5, 0.00001)])]
    >>> traces_spatial_index = pygeos_spatial_index(gpd.GeoSeries(traces))
    >>> areas = None
    >>> snapped = snap_others_to_trace(
    ...     idx, trace, snap_threshold, traces, traces_spatial_index, areas
    ... )
    >>> snapped[0].wkt, snapped[1]
    ('LINESTRING (0 0, 1 0, 1.5 1e-05, 2 0, 3 0)', True)

    Trace itself is not snapped by snap_others_to_trace:

    >>> idx = 0
    >>> trace = LineString([(0, 0), (1, 0), (2, 0), (3, 0)])
    >>> snap_threshold = 0.001
    >>> traces = [trace, LineString([(3.0001, -3), (3.0001, 0), (3, 3)])]
    >>> traces_spatial_index = pygeos_spatial_index(gpd.GeoSeries(traces))
    >>> areas = None
    >>> snapped = snap_others_to_trace(
    ...     idx, trace, snap_threshold, traces, traces_spatial_index, areas
    ... )
    >>> snapped[0].wkt, snapped[1]
    ('LINESTRING (0 0, 1 0, 2 0, 3 0)', False)
    """
    trace_candidates = resolve_trace_candidates(
        trace=trace,
        idx=idx,
        traces=traces,
        traces_spatial_index=traces_spatial_index,
        snap_threshold=snap_threshold,
    )

    if trace in list(trace_candidates):
        error = "Found trace in trace_candidates."
        logging.error(
            error,
            extra=dict(trace_wkt=trace.wkt,
                       trace_candidates_len=len(trace_candidates)),
        )
        raise ValueError(error)

    # If no candidates -> no intersecting -> trace is isolated
    if len(trace_candidates) == 0:
        return trace, False

    # Get all endpoints of trace_candidates
    endpoints: List[Point] = list(
        chain(*[
            list(get_trace_endpoints(trace_candidate))
            for trace_candidate in trace_candidates
        ]))

    # Filter endpoints out that are near to the area boundary
    if areas is not None:
        endpoints = [
            ep for ep in endpoints if not is_endpoint_close_to_boundary(
                ep, areas, snap_threshold=snap_threshold)
        ]

    # Add/replace endpoints into trace if they are within snap_threshold
    trace, was_snapped = snap_trace_to_another(trace_endpoints=endpoints,
                                               another=trace,
                                               snap_threshold=snap_threshold)

    # Debugging
    if final_allowed_loop and was_snapped:
        logging.error("In final_allowed_loop and still snapping:")
        logging.error(f"{traces, endpoints}")

    # Return trace and information if it was changed
    return trace, was_snapped
Example #4
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
Example #5
0
    def validation_method(
        geom: LineString,
        area: gpd.GeoDataFrame,
        snap_threshold: float,
        snap_threshold_error_multiplier: float,
        area_edge_snap_multiplier: float,
        **_,
    ) -> bool:
        """
        Validate for trace underlaps.

        >>> geom = LineString([(0, 0), (0, 1)])
        >>> area = gpd.GeoDataFrame(
        ...     geometry=[Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]
        ... )
        >>> snap_threshold = 0.01
        >>> snap_threshold_error_multiplier = 1.1
        >>> area_edge_snap_multiplier = 1.0
        >>> TargetAreaSnapValidator.validation_method(
        ...     geom,
        ...     area,
        ...     snap_threshold,
        ...     snap_threshold_error_multiplier,
        ...     area_edge_snap_multiplier,
        ... )
        True

        >>> geom = LineString([(0.5, 0.5), (0.5, 0.98)])
        >>> area = gpd.GeoDataFrame(
        ...     geometry=[Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]
        ... )
        >>> snap_threshold = 0.01
        >>> snap_threshold_error_multiplier = 1.1
        >>> area_edge_snap_multiplier = 10
        >>> TargetAreaSnapValidator.validation_method(
        ...     geom,
        ...     area,
        ...     snap_threshold,
        ...     snap_threshold_error_multiplier,
        ...     area_edge_snap_multiplier,
        ... )
        False

        """
        endpoints = get_trace_endpoints(geom)
        for endpoint in endpoints:
            for area_polygon in area.geometry.values:
                if TargetAreaSnapValidator.is_candidate_underlapping(
                        endpoint,
                        geom,
                        area_polygon,
                        snap_threshold=snap_threshold,
                ):
                    # if endpoint.within(area_polygon) and geom.within(area_polygon):
                    # Point and trace completely within the area, does not
                    # intersect its edge.
                    if (snap_threshold <= endpoint.distance(
                            area_polygon.boundary) <
                            snap_threshold * snap_threshold_error_multiplier *
                            area_edge_snap_multiplier):
                        return False
        return True
Example #6
0
    def validation_method(
        cls,
        geom: LineString,
        trace_candidates: gpd.GeoSeries,
        snap_threshold: float,
        snap_threshold_error_multiplier: float,
        **_,
    ) -> bool:
        """
        Validate for UnderlappingSnapValidator errors.

        >>> snap_threshold = 0.01
        >>> snap_threshold_error_multiplier = 1.1
        >>> geom = LineString(
        ...     [
        ...         (0, 0),
        ...         (0, 1 + snap_threshold * snap_threshold_error_multiplier * 0.99),
        ...     ]
        ... )
        >>> trace_candidates = gpd.GeoSeries([LineString([(-1, 1), (1, 1)])])
        >>> UnderlappingSnapValidator.validation_method(
        ...     geom, trace_candidates, snap_threshold, snap_threshold_error_multiplier
        ... )
        False

        >>> snap_threshold = 0.01
        >>> snap_threshold_error_multiplier = 1.1
        >>> geom = LineString([(0, 0), (0, 1)])
        >>> trace_candidates = gpd.GeoSeries([LineString([(-1, 1), (1, 1)])])
        >>> UnderlappingSnapValidator.validation_method(
        ...     geom, trace_candidates, snap_threshold, snap_threshold_error_multiplier
        ... )
        True

        """
        if len(trace_candidates) == 0:
            return True

        endpoints = get_trace_endpoints(geom)
        for endpoint in endpoints:

            # Check that if endpoint is well snapped to one trace another trace
            # won't have chance to cause a validation error (false positive).
            if any(trace_candidates.distance(endpoint) < snap_threshold):
                continue
            trace: LineString
            for trace in trace_candidates.geometry.values:
                if (snap_threshold < trace.distance(endpoint) <
                        snap_threshold * snap_threshold_error_multiplier):
                    # Determine if its underlappings or overlapping
                    is_ul_result = is_underlapping(
                        geom,
                        trace,
                        endpoint,
                        snap_threshold,
                        snap_threshold_error_multiplier,
                    )
                    if is_ul_result is None:
                        raise ValueError(
                            "Expected is_ul_result to not be None.")
                    if is_ul_result:
                        # Underlapping
                        cls.ERROR = cls._UNDERLAPPING
                        return False
                    cls.ERROR = cls._OVERLAPPING
                    return False

        return True
Example #7
0
    def conditional_linemerge(first: LineString, second: LineString,
                              tolerance: float,
                              buffer_value: float) -> Union[None, LineString]:
        """
        Conditionally merge two LineStrings (first and second).

        Merge occurs if:
            1. Their endpoints are within buffer_value of each other.
            2. Their total orientations are within tolerance (degrees) of each
               other.

        Merges by joining their coordinates. The endpoint
        (that is within buffer_value of endpoint of first) of the second
        LineString is trimmed from the resulting coordinates.

        E.g. with merging:

        >>> first = LineString([(0, 0), (0, 2)])
        >>> second = LineString([(0, 2.001), (0, 4)])
        >>> tolerance = 5
        >>> buffer_value = 0.01
        >>> LineMerge.conditional_linemerge(first, second, tolerance, buffer_value).wkt
        'LINESTRING (0 0, 0 2, 0 4)'

        Without merging:

        >>> first = LineString([(0, 0), (0, 2)])
        >>> second = LineString([(0, 2.1), (0, 4)])
        >>> tolerance = 5
        >>> buffer_value = 0.01
        >>> LineMerge.conditional_linemerge(
        ...     first, second, tolerance, buffer_value
        ... ) is None
        True

        """
        assert isinstance(first, LineString) and isinstance(second, LineString)
        # Get trace endpoints
        first_start, first_end = get_trace_endpoints(first)
        second_start, second_end = get_trace_endpoints(second)

        # Get unit vectors from endpoints
        first_unit_vector = create_unit_vector(first_start, first_end)
        second_unit_vector = create_unit_vector(second_start, second_end)

        # Check if unit vectors are close in orientation
        are_close = compare_unit_vector_orientation(first_unit_vector,
                                                    second_unit_vector,
                                                    threshold_angle=tolerance)
        are_close_reverse = compare_unit_vector_orientation(
            -first_unit_vector, second_unit_vector, threshold_angle=tolerance)
        # Get coordinates
        first_coords_list = list(first.coords)
        second_coords_list = list(second.coords)
        if (first_end.buffer(buffer_value).intersects(
                second_start.buffer(buffer_value)) and are_close):
            # First ends in the start of second -> Sequence of coords is correct
            # Do not include first coordinate of second in new
            new_coords = first_coords_list + second_coords_list[1:]
        elif (first_end.buffer(buffer_value).intersects(
                second_end.buffer(buffer_value)) and are_close_reverse):
            # First ends in second end
            new_coords = first_coords_list + list(
                reversed(second_coords_list))[1:]
        elif (first_start.buffer(buffer_value).intersects(
                second_start.buffer(buffer_value)) and are_close_reverse):
            # First starts from the same as second
            new_coords = list(
                reversed(second_coords_list))[:-1] + first_coords_list
        elif (first_start.buffer(buffer_value).intersects(
                second_end.buffer(buffer_value)) and are_close):
            # First starts from end of second
            new_coords = second_coords_list[:-1] + first_coords_list
        else:
            return None
        return LineString(new_coords)