Example #1
0
def cut_polygon_with_line(polygon: Union[Polygon, MultiPolygon,
                                         Sequence[Polygon]],
                          line: LineString) -> List[Polygon]:
    orig_polygon = assert_multipolygon(polygon) if isinstance(
        polygon, (MultiPolygon, Polygon)) else polygon
    polygons: List[List[LinearRing]] = []
    # noinspection PyTypeChecker
    for polygon in orig_polygon:
        rings = getattr(polygon, 'c3nav_cache', None)
        if not rings:
            rings = [polygon.exterior, *polygon.interiors]
            polygon.c3nav_cache = rings
        polygons.append(rings)

    # find intersection points between the line and polygon rings
    points = deque()
    line_prep = prepared.prep(line)
    for i, polygon in enumerate(polygons):
        for j, ring in enumerate(polygon):
            if not line_prep.intersects(ring):
                continue
            intersection = ring.intersection(line)
            for item in getattr(intersection, 'geoms', (intersection, )):
                if isinstance(item, Point):
                    points.append(cutpoint(item, i, j))
                elif isinstance(item, LineString):
                    points.append(cutpoint(Point(*item.coords[0]), i, j))
                    points.append(cutpoint(Point(*item.coords[-1]), i, j))
                else:
                    raise ValueError

    # sort the points by distance along the line
    points = deque(sorted(points, key=lambda p: line.project(p.point)))

    if not points:
        return orig_polygon

    # go through all points and cut pair-wise
    last = points.popleft()
    while points:
        current = points.popleft()

        # don't to anything between different polygons
        if current.polygon != last.polygon:
            last = current
            continue

        polygon = polygons[current.polygon]
        segment = cut_line_with_point(
            cut_line_with_point(line, last.point)[-1], current.point)[0]

        if current.ring != last.ring:
            # connect rings
            ring1 = cut_line_with_point(polygon[last.ring], last.point)
            ring2 = cut_line_with_point(polygon[current.ring], current.point)
            new_ring = LinearRing(ring1[1].coords[:-1] + ring1[0].coords[:-1] +
                                  segment.coords[:-1] + ring2[1].coords[:-1] +
                                  ring2[0].coords[:-1] + segment.coords[::-1])
            if current.ring == 0 or last.ring == 0:
                # join an interior with exterior
                new_i = 0
                polygon[0] = new_ring
                interior = current.ring if last.ring == 0 else last.ring
                polygon[interior] = None
                mapping = {interior: new_i}
            else:
                # join two interiors
                new_i = len(polygon)
                mapping = {last.ring: new_i, current.ring: new_i}
                polygon.append(new_ring)
                polygon[last.ring] = None
                polygon[current.ring] = None

            # fix all remaining cut points that refer to the rings we just joined to point the the correct ring
            points = deque(
                (cutpoint(item.point, item.polygon, mapping[item.ring]) if (
                    item.polygon == current.polygon and item.ring in mapping
                ) else item) for item in points)
            last = cutpoint(current.point, current.polygon, new_i)
            continue

        # check if this is not a cut through emptyness
        # half-cut polygons are invalid geometry and shapely won't deal with them
        # so we have to do this the complicated way
        ring = cut_line_with_point(polygon[current.ring], current.point)
        ring = ring[0] if len(ring) == 1 else LinearRing(ring[1].coords[:-1] +
                                                         ring[0].coords[0:])
        ring = cut_line_with_point(ring, last.point)

        point_forwards = ring[1].coords[1]
        point_backwards = ring[0].coords[-2]
        angle_forwards = math.atan2(point_forwards[0] - last.point.x,
                                    point_forwards[1] - last.point.y)
        angle_backwards = math.atan2(point_backwards[0] - last.point.x,
                                     point_backwards[1] - last.point.y)
        next_segment_point = Point(segment.coords[1])
        angle_segment = math.atan2(next_segment_point.x - last.point.x,
                                   next_segment_point.y - last.point.y)

        while angle_forwards <= angle_backwards:
            angle_forwards += 2 * math.pi
        if angle_segment < angle_backwards:
            while angle_segment < angle_backwards:
                angle_segment += 2 * math.pi
        else:
            while angle_segment > angle_forwards:
                angle_segment -= 2 * math.pi

        # if we cut through emptiness, continue
        if not (angle_backwards < angle_segment < angle_forwards):
            last = current
            continue

        # split ring
        new_i = len(polygons)
        old_ring = LinearRing(ring[0].coords[:-1] + segment.coords[0:])
        new_ring = LinearRing(ring[1].coords[:-1] + segment.coords[::-1])

        # if this is not an exterior cut but creates a new polygon inside a hole,
        # make sure that new_ring contains the exterior for the new polygon
        if current.ring != 0 and not new_ring.is_ccw:
            new_ring, old_ring = old_ring, new_ring

        new_geom = Polygon(new_ring)
        polygon[current.ring] = old_ring
        new_polygon = [new_ring]
        polygons.append(new_polygon)
        mapping = {}

        # assign all [other] interiors of the old polygon to one of the two new polygons
        for i, interior in enumerate(polygon[1:], start=1):
            if i == current.ring:
                continue
            if interior is not None and new_geom.contains(interior):
                polygon[i] = None
                mapping[i] = len(new_polygon)
                new_polygon.append(interior)

        # fix all remaining cut points to point to the new polygon if they refer to moved interiors
        points = deque((cutpoint(item.point, new_i, mapping[item.ring]) if (
            item.polygon == current.polygon and item.ring in mapping) else item
                        ) for item in points)

        # fix all remaining cut points that refer to the ring we just split to point the the correct new ring
        points = deque((cutpoint(item.point, new_i, 0) if (
            item.polygon == current.polygon and item.ring == current.ring
            and not old_ring.contains(item.point)) else item)
                       for item in points)

        last = cutpoint(current.point, new_i, 0)

    result = deque()
    for polygon in polygons:
        polygon = [ring for ring in polygon if ring is not None]
        new_polygon = Polygon(polygon[0], tuple(polygon[1:]))
        new_polygon.c3nav_cache = polygon
        result.append(new_polygon)
    return list(result)