class EventsQueue: __slots__ = 'context', 'key', '_queue' def __init__(self, context: Context) -> None: self.context = context key = self.key = partial(EventsQueueKey, context.angle_orientation) self._queue = PriorityQueue(key=key) __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) def peek(self) -> Event: return self._queue.peek() @abstractmethod def register(self, segments_endpoints: Iterable[SegmentEndpoints], *, from_test: bool) -> None: """ Registers segments in the events queue. """ @abstractmethod def sweep(self, stop_x: Scalar) -> Iterable[LeftEvent]: """ Sweeps plane and emits processed segments' events. """ def _divide_segment(self, event: LeftEvent, break_point: Point) -> None: self._queue.push(event.divide(break_point)) self._queue.push(event.right)
class Builder: __slots__ = ('index', '_beach_line', '_circle_events', '_end_points', 'site_events', '_site_event_index') def __init__(self, index: int = 0, site_events: Optional[List[SiteEvent]] = None) -> None: self.index = index self._beach_line = red_black.Tree.from_components([]) self._circle_events = PriorityQueue(key=itemgetter(0)) self._end_points = PriorityQueue(key=itemgetter(0)) self.site_events = [] if site_events is None else site_events self._site_event_index = None __repr__ = generate_repr(__init__) @property def beach_line(self) -> List[Tuple[BeachLineKey, BeachLineValue]]: return [node.item for node in self._beach_line] @property def site_event(self) -> SiteEvent: return self.site_events[self._site_event_index] @property def site_event_index(self) -> int: return (len(self.site_events) if self._site_event_index is None else self._site_event_index) def activate_circle_event(self, first_site: SiteEvent, second_site: SiteEvent, third_site: SiteEvent, bisector_node: red_black.Node) -> None: event = CircleEvent(0., 0., 0.) # check if the three input sites create a circle event if compute_circle_event(event, first_site, second_site, third_site): # add the new circle event to the circle events queue; # update bisector's circle event iterator to point # to the new circle event in the circle event queue self._circle_events.push((event, bisector_node)) bisector_node.value.circle_event = event def construct(self, output: 'Diagram') -> None: self.init_sites_queue() self.init_beach_line(output) while (self._circle_events or self._site_event_index < len(self.site_events)): if (not self._circle_events or (self._site_event_index < len(self.site_events) and self.site_event < self._circle_events.peek()[0])): self.process_site_event(output) else: self.process_circle_event(output) while (self._circle_events and not self._circle_events.peek()[0].is_active): self._circle_events.pop() self._beach_line.clear() output._build() @staticmethod def deactivate_circle_event(value: BeachLineValue) -> None: if value.circle_event is not None: value.circle_event.deactivate() value.circle_event = None def init_beach_line(self, diagram: 'Diagram') -> None: if not self.site_events: return elif len(self.site_events) == 1: # handle single site event case diagram._process_single_site(self.site_events[0]) self._site_event_index += 1 else: skip = 0 while (self._site_event_index < len(self.site_events) and are_vertical_endpoints(self.site_event.start, self.site_events[0].start) and self.site_event.is_vertical): self._site_event_index += 1 skip += 1 if skip == 1: # init beach line with the first two sites self.init_beach_line_default(diagram) else: # init beach line with collinear vertical sites self.init_beach_line_collinear_sites(diagram) def init_beach_line_collinear_sites(self, diagram: 'Diagram') -> None: first_index, second_index = 0, 1 while second_index != self._site_event_index: # create a new beach line node first_site, second_site = (self.site_events[first_index], self.site_events[second_index]) new_node = BeachLineKey(first_site, second_site) # update the diagram edge, _ = diagram._insert_new_edge(first_site, second_site) # insert a new bisector into the beach line self._beach_line.insert(new_node, BeachLineValue(edge)) first_index += 1 second_index += 1 def init_beach_line_default(self, diagram: 'Diagram') -> None: # get the first and the second site event self.insert_new_arc(self.site_events[0], self.site_events[0], self.site_events[1], diagram) # the second site was already processed, move the position self._site_event_index += 1 def insert_new_arc(self, first_arc_site: SiteEvent, second_arc_site: SiteEvent, site_event: SiteEvent, output: 'Diagram') -> red_black.Node: # create two new bisectors with opposite directions new_left_node = BeachLineKey(first_arc_site, site_event) new_right_node = BeachLineKey(site_event, second_arc_site) # set correct orientation for the first site of the second node if site_event.is_segment: new_right_node.left_site.inverse() # update the output edges = output._insert_new_edge(second_arc_site, site_event) self._beach_line.insert(new_right_node, BeachLineValue(edges[1])) if site_event.is_segment: # update the beach line with temporary bisector, # that will # disappear after processing site event # corresponding to the second endpoint of the segment site new_node = BeachLineKey(site_event, site_event) new_node.right_site.inverse() node = self._beach_line.insert(new_node, BeachLineValue(None)) # update the data structure that holds temporary bisectors self._end_points.push((site_event.end, node)) return self._beach_line.insert(new_left_node, BeachLineValue(edges[0])) def init_sites_queue(self) -> None: self.site_events.sort() self.site_events = to_unique_just_seen(self.site_events) for index, event in enumerate(self.site_events): event.sorted_index = index self._site_event_index = 0 def insert_point(self, point: Point) -> int: index = self.index self.site_events.append( SiteEvent.from_point(point, initial_index=index, source_category=SourceCategory.SINGLE_POINT)) self.index += 1 return index def insert_segment(self, segment: Segment) -> int: site_events = self.site_events index = self.index start, end = segment.start, segment.end site_events.append( SiteEvent.from_point( start, initial_index=index, source_category=SourceCategory.SEGMENT_START_POINT)) site_events.append( SiteEvent.from_point( end, initial_index=index, source_category=SourceCategory.SEGMENT_END_POINT)) site_events.append( SiteEvent(start, end, source_category=SourceCategory.INITIAL_SEGMENT, initial_index=index) if start < end else SiteEvent( end, start, source_category=SourceCategory.REVERSE_SEGMENT, initial_index=index)) self.index += 1 return index def process_circle_event(self, output: 'Diagram') -> None: circle_event, first_node = self._circle_events.pop() last_node = first_node second_site = copy(first_node.key.right_site) second_bisector = first_node.value.edge first_node = self._beach_line.predecessor(first_node) first_bisector = first_node.value.edge first_site = copy(first_node.key.left_site) if (not first_site.is_segment and second_site.is_segment and second_site.end == first_site.start): second_site.inverse() first_node.key.right_site = second_site first_node.value.edge, _ = output._insert_new_edge_from_intersection( first_site, second_site, circle_event, first_bisector, second_bisector) self._beach_line.remove(last_node) last_node = first_node if first_node is not self._beach_line.min(): self.deactivate_circle_event(first_node.value) first_node = self._beach_line.predecessor(first_node) self.activate_circle_event(first_node.key.left_site, first_site, second_site, last_node) last_node = self._beach_line.successor(last_node) if last_node is not red_black.NIL: self.deactivate_circle_event(last_node.value) self.activate_circle_event(first_site, second_site, last_node.key.right_site, last_node) def process_site_event(self, output: 'Diagram') -> None: last_index = self._site_event_index + 1 if not self.site_event.is_segment: while (self._end_points and self._end_points.peek()[0] == self.site_event.start): _, node = self._end_points.pop() self._beach_line.remove(node) else: while (last_index < len(self.site_events) and self.site_events[last_index].is_segment and (self.site_events[last_index].start == self.site_event.start)): last_index += 1 # find the node in the binary search tree # with left arc lying above the new site point new_key = BeachLineKey(self.site_event, self.site_event) right_node = self._beach_line.supremum(new_key) while self._site_event_index < last_index: site_event = copy(self.site_event) left_node = right_node if right_node is red_black.NIL: # the above arc corresponds to the second arc of the last node, # move the iterator to the last node left_node = self._beach_line.max() # get the second site of the last node arc_site = left_node.key.right_site # insert new nodes into the beach line, update the output right_node = self.insert_new_arc(arc_site, arc_site, site_event, output) # add a candidate circle to the circle event queue; # there could be only one new circle event # formed by a new bisector and the one on the left self.activate_circle_event(left_node.key.left_site, left_node.key.right_site, site_event, right_node) elif right_node is self._beach_line.min(): # the above arc corresponds to the first site of the first node arc_site = right_node.key.left_site # Insert new nodes into the beach line. Update the output. left_node = self.insert_new_arc(arc_site, arc_site, site_event, output) # if the site event is a segment, update its direction if site_event.is_segment: site_event.inverse() # add a candidate circle to the circle event queue; # there could be only one new circle event # formed by a new bisector and the one on the right self.activate_circle_event(site_event, right_node.key.left_site, right_node.key.right_site, right_node) right_node = left_node else: # the above arc corresponds neither to the first, # nor to the last site in the beach line second_arc_site = right_node.key.left_site third_site = right_node.key.right_site # remove the candidate circle from the event queue self.deactivate_circle_event(right_node.value) left_node = self._beach_line.predecessor(left_node) first_arc_site = left_node.key.right_site first_site = left_node.key.left_site # insert new nodes into the beach line. Update the output new_node = self.insert_new_arc(first_arc_site, second_arc_site, site_event, output) # add candidate circles to the circle event queue; # there could be up to two circle events # formed by a new bisector and the one on the left or right self.activate_circle_event(first_site, first_arc_site, site_event, new_node) # if the site event is a segment, update its direction if site_event.is_segment: site_event.inverse() self.activate_circle_event(site_event, second_arc_site, third_site, right_node) right_node = new_node self._site_event_index += 1
class Operation: __slots__ = ('_left', '_right', '_type', '_events_queue', '_resultant', '_already_run') def __init__(self, left: Polygon, right: Polygon, type_: OperationType) -> None: self._left = left self._right = right self._type = type_ self._events_queue = PriorityQueue(key=EventsQueueKey, reverse=True) self._resultant = Polygon([]) self._already_run = False __repr__ = generate_repr(__init__, field_seeker=seekers.complex_) def __eq__(self, other: 'Operation') -> bool: return (self._left == other._left and self._right == other._right and self._type is other._type if isinstance(other, Operation) else NotImplemented) @property def left(self) -> Polygon: return self._left @property def right(self) -> Polygon: return self._right @property def events(self) -> List[SweepEvent]: events_queue = copy(self._events_queue) return [events_queue.pop() for _ in range(len(events_queue))] @property def resultant(self) -> Polygon: return self._resultant @property def type(self) -> OperationType: return self._type @property def is_trivial(self) -> bool: # test 1 for trivial result case if not (self._left.contours and self._right.contours): # at least one of the polygons is empty if self._type is OperationType.DIFFERENCE: self._resultant = self._left if (self._type is OperationType.UNION or self._type is OperationType.XOR): self._resultant = (self._left if self._left.contours else self._right) self._already_run = True return True # test 2 for trivial result case left_bounding_box = self._left.bounding_box right_bounding_box = self._right.bounding_box if (left_bounding_box.x_min > right_bounding_box.x_max or right_bounding_box.x_min > left_bounding_box.x_max or left_bounding_box.y_min > right_bounding_box.y_max or right_bounding_box.y_min > left_bounding_box.y_max): # the bounding boxes do not overlap if self._type is OperationType.DIFFERENCE: self._resultant = self._left elif (self._type is OperationType.UNION or self._type is OperationType.XOR): self._resultant = self._left self._resultant.join(self._right) self._already_run = True return True return False @staticmethod def collect_events(events: List[SweepEvent]) -> List[SweepEvent]: result = [ event for event in events if event.is_left and event.in_result or not event.is_left and event.other_event.in_result ] is_sorted = False while not is_sorted: is_sorted = True for index in range(len(result) - 1): if (EventsQueueKey(result[index]) < EventsQueueKey( result[index + 1])): result[index], result[index + 1] = (result[index + 1], result[index]) is_sorted = False for index, event in enumerate(result): event.position = index if not event.is_left: event.position, event.other_event.position = ( event.other_event.position, event.position) return result def connect_edges(self, events: List[SweepEvent]) -> None: self.process_events(self.collect_events(events)) def divide_segment(self, event: SweepEvent, point: Point) -> None: # "left event" of the "right line segment" # resulting from dividing event.segment left_event = SweepEvent(True, point, event.other_event, event.polygon_type, EdgeType.NORMAL) # "right event" of the "left line segment" # resulting from dividing event.segment right_event = SweepEvent(False, point, event, event.polygon_type, EdgeType.NORMAL) if EventsQueueKey(left_event) < EventsQueueKey(event.other_event): # avoid a rounding error, # the left event would be processed after the right event event.other_event.is_left = True left_event.is_left = False event.other_event.other_event = left_event event.other_event = right_event self._events_queue.push(left_event) self._events_queue.push(right_event) def in_result(self, event: SweepEvent) -> bool: operation_type = self._type edge_type = event.edge_type if edge_type is EdgeType.NORMAL: if operation_type is OperationType.INTERSECTION: return not event.other_in_out elif operation_type is OperationType.UNION: return event.other_in_out elif operation_type is OperationType.DIFFERENCE: return (event.polygon_type is PolygonType.SUBJECT and event.other_in_out or event.polygon_type is PolygonType.CLIPPING and not event.other_in_out) else: return operation_type is OperationType.XOR elif edge_type is EdgeType.SAME_TRANSITION: return (operation_type is OperationType.INTERSECTION or operation_type is OperationType.UNION) elif edge_type is EdgeType.DIFFERENT_TRANSITION: return operation_type is OperationType.DIFFERENCE else: return False def possible_intersection(self, first_event: SweepEvent, second_event: SweepEvent) -> int: intersections_count, first_point, second_point = find_intersections( first_event.segment, second_event.segment) if not intersections_count: # no intersection return 0 if ((intersections_count == 1) and (first_event.point == second_event.point or (first_event.other_event.point == second_event.other_event.point))): # the line segments intersect at an endpoint of both line segments return 0 if (intersections_count == 2 and first_event.polygon_type is second_event.polygon_type): raise ValueError("Edges of the same polygon should not overlap.") # The line segments associated to le1 and le2 intersect if intersections_count == 1: if (first_event.point != first_point and first_event.other_event.point != first_point): # if the intersection point is not an endpoint of le1.segment self.divide_segment(first_event, first_point) if (second_event.point != first_point and second_event.other_event.point != first_point): # if the intersection point is not an endpoint of le2.segment self.divide_segment(second_event, first_point) return 1 # The line segments associated to le1 and le2 overlap sorted_events = [] if first_event.point == second_event.point: sorted_events.append(None) elif EventsQueueKey(first_event) < EventsQueueKey(second_event): sorted_events.append(second_event) sorted_events.append(first_event) else: sorted_events.append(first_event) sorted_events.append(second_event) if first_event.other_event.point == second_event.other_event.point: sorted_events.append(None) elif (EventsQueueKey(first_event.other_event) < EventsQueueKey( second_event.other_event)): sorted_events.append(second_event.other_event) sorted_events.append(first_event.other_event) else: sorted_events.append(first_event.other_event) sorted_events.append(second_event.other_event) if (len(sorted_events) == 2 or len(sorted_events) == 3 and sorted_events[2]): # both line segments are equal or share the left endpoint first_event.edge_type = EdgeType.NON_CONTRIBUTING second_event.edge_type = (EdgeType.SAME_TRANSITION if first_event.in_out is second_event.in_out else EdgeType.DIFFERENT_TRANSITION) if len(sorted_events) == 3: self.divide_segment(sorted_events[2].other_event, sorted_events[1].point) return 2 if len(sorted_events) == 3: # the line segments share the right endpoint self.divide_segment(sorted_events[0], sorted_events[1].point) return 3 if sorted_events[0] is not sorted_events[3].other_event: # no line segment includes totally the other one self.divide_segment(sorted_events[0], sorted_events[1].point) self.divide_segment(sorted_events[1], sorted_events[2].point) return 3 # one line segment includes the other one self.divide_segment(sorted_events[0], sorted_events[1].point) self.divide_segment(sorted_events[3].other_event, sorted_events[2].point) return 3 def process_events(self, events: List[SweepEvent]) -> None: depth, hole_of = [], [] processed = [False] * len(events) contours = self._resultant.contours for index, event in enumerate(events): if processed[index]: continue contour = Contour([], [], True) contour_id = len(contours) contours.append(contour) depth.append(0) hole_of.append(-1) if event.prev_in_result_event is not None: lower_contour_id = event.prev_in_result_event.contour_id if not event.prev_in_result_event.result_in_out: contours[lower_contour_id].add_hole(contour_id) hole_of[contour_id] = lower_contour_id depth[contour_id] = depth[lower_contour_id] + 1 contour.is_external = False elif not contours[lower_contour_id].is_external: contours[hole_of[lower_contour_id]].add_hole(contour_id) hole_of[contour_id] = hole_of[lower_contour_id] depth[contour_id] = depth[lower_contour_id] contour.is_external = False position = index initial = event.point contour.add(initial) event = events[position] while event.other_event.point != initial: processed[position] = True if event.is_left: event.result_in_out = False event.contour_id = contour_id else: event.other_event.result_in_out = True event.other_event.contour_id = contour_id position = event.position processed[position] = True contour.add(events[position].point) position = self.to_next_position(position, events, processed) event = events[position] processed[position] = processed[event.position] = True event.other_event.result_in_out = True event.other_event.contour_id = contour_id if depth[contour_id] & 1: contour.reverse() def process_segments(self) -> None: for contour in self._left.contours: for segment in to_segments(contour.points): self._process_segment(segment, PolygonType.SUBJECT) for contour in self._right.contours: for segment in to_segments(contour.points): self._process_segment(segment, PolygonType.CLIPPING) def _process_segment(self, segment: Segment, polygon_type: PolygonType) -> None: source_event = SweepEvent(True, segment.source, None, polygon_type, EdgeType.NORMAL) target_event = SweepEvent(True, segment.target, source_event, polygon_type, EdgeType.NORMAL) source_event.other_event = target_event if segment.min == segment.source: target_event.is_left = False else: source_event.is_left = False self._events_queue.push(source_event) self._events_queue.push(target_event) def sweep(self) -> List[SweepEvent]: min_max_x = min(self._left.bounding_box.x_max, self._right.bounding_box.x_max) result = [] events_queue = self._events_queue sweep_line = red_black.set_(key=SweepLineKey) while events_queue: event = events_queue.peek() if (self._type is OperationType.INTERSECTION and event.point.x > min_max_x or self._type is OperationType.DIFFERENCE and event.point.x > self._left.bounding_box.x_max): break result.append(event) events_queue.pop() if event.is_left: sweep_line.add(event) try: next_event = sweep_line.next(event) except ValueError: next_event = None try: previous_event = sweep_line.prev(event) except ValueError: previous_event = None self.compute_fields(event, previous_event) if next_event is not None: if self.possible_intersection(event, next_event) == 2: self.compute_fields(event, previous_event) self.compute_fields(next_event, event) if previous_event is not None: if self.possible_intersection(previous_event, event) == 2: try: pre_previous_event = sweep_line.prev( previous_event) except ValueError: pre_previous_event = None self.compute_fields(previous_event, pre_previous_event) self.compute_fields(event, previous_event) else: event = event.other_event if event not in sweep_line: continue try: next_event = sweep_line.next(event) except ValueError: next_event = None try: previous_event = sweep_line.prev(event) except ValueError: previous_event = None sweep_line.remove(event) if next_event is not None and previous_event is not None: self.possible_intersection(previous_event, next_event) return result def compute_fields(self, event: SweepEvent, previous_event: Optional[SweepEvent]) -> None: if previous_event is None: event.in_out = False event.other_in_out = True else: if event.polygon_type is previous_event.polygon_type: event.in_out = not previous_event.in_out event.other_in_out = previous_event.other_in_out else: event.in_out = not previous_event.other_in_out event.other_in_out = (not previous_event.in_out if previous_event.is_vertical else previous_event.in_out) event.prev_in_result_event = ( previous_event.prev_in_result_event if (not self.in_result(previous_event) or previous_event.is_vertical) else previous_event) event.in_result = self.in_result(event) def run(self) -> None: if self._already_run: return if self.is_trivial: return self.process_segments() self.connect_edges(self.sweep()) self._already_run = True @staticmethod def to_next_position(position: int, events: List[SweepEvent], processed: List[bool]) -> int: result = position + 1 while (result < len(events) and events[result].point == events[position].point): if not processed[result]: return result else: result += 1 if not position: return 0 result = position - 1 while processed[result]: if not result: break result -= 1 return result
class ShapedEventsQueue(Generic[LeftShapedEvent]): __slots__ = 'context', 'event_cls', '_queue' def __init__(self, event_cls: Type[LeftShapedEvent], context: Context) -> None: self.event_cls, self.context = event_cls, context self._queue = PriorityQueue(key=partial(BinaryEventsQueueKey, context.angle_orientation)) __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) @property def key(self) -> Callable[[ShapedEvent], BinaryEventsQueueKey]: return self._queue.key def detect_intersection(self, below_event: LeftShapedEvent, event: LeftShapedEvent) -> bool: relation = self.context.segments_relation(below_event, event) if relation is Relation.CROSS or relation is Relation.TOUCH: if (event.start != below_event.start and event.end != below_event.end): # segments do not intersect_multipolygons at endpoints point = self.context.segments_intersection(below_event, event) if point != below_event.start and point != below_event.end: self._divide_segment(below_event, point) if point != event.start and point != event.end: self._divide_segment(event, point) elif relation is not Relation.DISJOINT: # segments overlap if below_event.from_first is event.from_first: raise ValueError('Edges of the same geometry ' 'should not overlap.') starts_equal = below_event.start == event.start if starts_equal: start_min = start_max = None elif self.key(event) < self.key(below_event): start_min, start_max = event, below_event else: start_min, start_max = below_event, event ends_equal = event.end == below_event.end if ends_equal: end_min = end_max = None elif self.key(event.opposite) < self.key(below_event.opposite): end_min, end_max = event.opposite, below_event.opposite else: end_min, end_max = below_event.opposite, event.opposite if starts_equal: # both line segments are equal or share the left endpoint below_event.overlap_kind = event.overlap_kind = ( OverlapKind.SAME_ORIENTATION if event.interior_to_left is below_event.interior_to_left else OverlapKind.DIFFERENT_ORIENTATION) if not ends_equal: self._divide_segment(end_max.opposite, end_min.start) return True elif ends_equal: # the line segments share the right endpoint self._divide_segment(start_min, start_max.start) elif start_min is end_max.opposite: # one line segment includes the other one self._divide_segment(start_min, end_min.start) self._divide_segment(start_min, start_max.start) else: # no line segment includes the other one self._divide_segment(start_max, end_min.start) self._divide_segment(start_min, start_max.start) return False def pop(self) -> ShapedEvent: return self._queue.pop() def register(self, segments_endpoints: Iterable[SegmentEndpoints], from_first: bool) -> None: event_cls, push = self.event_cls, self._queue.push for segment_endpoints in segments_endpoints: event = event_cls.from_endpoints(segment_endpoints, from_first) push(event) push(event.opposite) def _divide_segment(self, event: LeftShapedEvent, point: Point) -> None: tail = event.divide(point) self._queue.push(tail) self._queue.push(event.opposite)
class NaryEventsQueue: __slots__ = 'context', '_queue' def __init__(self, context: Context) -> None: self.context = context self._queue = PriorityQueue(key=partial(NaryEventsQueueKey, context.angle_orientation)) __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) @property def key(self) -> Callable[[NaryEvent], NaryEventsQueueKey]: return self._queue.key def detect_intersection(self, below_event: LeftNaryEvent, event: LeftNaryEvent) -> None: relation = self.context.segments_relation(below_event, event) if relation is Relation.CROSS or relation is Relation.TOUCH: if (event.start != below_event.start and event.end != below_event.end): # segments do not intersect_multipolygons at endpoints point = self.context.segments_intersection(below_event, event) if point != below_event.start and point != below_event.end: self._divide_segment(below_event, point) if point != event.start and point != event.end: self._divide_segment(event, point) elif relation is not Relation.DISJOINT: # segments overlap starts_equal = below_event.start == event.start if starts_equal: start_min = start_max = None elif self.key(event) < self.key(below_event): start_min, start_max = event, below_event else: start_min, start_max = below_event, event ends_equal = event.end == below_event.end if ends_equal: end_min = end_max = None elif self.key(event.opposite) < self.key(below_event.opposite): end_min, end_max = event.opposite, below_event.opposite else: end_min, end_max = below_event.opposite, event.opposite if starts_equal: # both line segments are equal or share the left endpoint if not ends_equal: self._divide_segment(end_max.opposite, end_min.start) elif ends_equal: # the line segments share the right endpoint self._divide_segment(start_min, start_max.start) elif start_min is end_max.opposite: # one line segment includes the other one self._divide_segment(start_min, end_min.start) self._divide_segment(start_min, start_max.start) else: # no line segment includes the other one self._divide_segment(start_max, end_min.start) self._divide_segment(start_min, start_max.start) def pop(self) -> NaryEvent: return self._queue.pop() def register(self, segments_endpoints: Iterable[SegmentEndpoints]) -> None: push = self._queue.push for segment_endpoints in segments_endpoints: event = LeftNaryEvent.from_segment_endpoints(segment_endpoints) push(event) push(event.opposite) def _divide_segment(self, event: LeftNaryEvent, point: Point) -> None: tail = LeftNaryEvent.divide(event, point) self._queue.push(tail) self._queue.push(event.opposite)
class MixedEventsQueue: __slots__ = 'context', '_queue' def __init__(self, context: Context) -> None: self.context = context self._queue = PriorityQueue(key=partial(BinaryEventsQueueKey, context.angle_orientation)) @property def key(self) -> Callable[[MixedEvent], BinaryEventsQueueKey]: return self._queue.key __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) def detect_intersection(self, below_event: LeftMixedEvent, event: LeftMixedEvent) -> bool: relation = self.context.segments_relation(below_event, event) if relation is Relation.CROSS or relation is Relation.TOUCH: if (event.start != below_event.start and event.end != below_event.end): # segments do not intersect_multipolygons at endpoints point = self.context.segments_intersection(below_event, event) if point != below_event.start and point != below_event.end: self._divide_segment(below_event, point) if point != event.start and point != event.end: self._divide_segment(event, point) elif relation is not Relation.DISJOINT: # segments overlap if below_event.from_first is event.from_first: raise ValueError('Edges of the {geometry} ' 'should not overlap.' .format(geometry=('multisegment' if event.from_first else 'multipolygon'))) event.is_overlap = below_event.is_overlap = True starts_equal = below_event.start == event.start if starts_equal: start_min = start_max = None elif self.key(event) < self.key(below_event): start_min, start_max = event, below_event else: start_min, start_max = below_event, event ends_equal = event.end == below_event.end if ends_equal: end_min = end_max = None elif self.key(event.opposite) < self.key(below_event.opposite): end_min, end_max = event.opposite, below_event.opposite else: end_min, end_max = below_event.opposite, event.opposite if starts_equal: # both line segments are equal or share the left endpoint if not ends_equal: self._divide_segment(end_max.opposite, end_min.start) return True elif ends_equal: # the line segments share the right endpoint self._divide_segment(start_min, start_max.start) elif start_min is end_max.opposite: # one line segment includes the other one self._divide_segment(start_min, end_min.start) self._divide_segment(start_min, start_max.start) else: # no line segment includes the other one self._divide_segment(start_max, end_min.start) self._divide_segment(start_min, start_max.start) return False def pop(self) -> MixedEvent: return self._queue.pop() def register(self, segments_endpoints: Iterable[SegmentEndpoints], from_first: bool) -> None: push = self._queue.push for segment_endpoints in segments_endpoints: event = LeftMixedEvent.from_endpoints(segment_endpoints, from_first) push(event) push(event.opposite) def _divide_segment(self, event: LeftMixedEvent, point: Point) -> None: tail = event.divide(point) self._queue.push(tail) self._queue.push(event.opposite)
class EventsQueue: __slots__ = 'context', '_queue' def __init__(self, context: Context) -> None: self.context = context self._queue = PriorityQueue(key=partial(EventsQueueKey, context.angle_orientation)) @staticmethod def compute_position(below_event: Optional[LeftEvent], event: LeftEvent) -> None: if below_event is not None: event.other_interior_to_left = (below_event.other_interior_to_left if (event.from_first is below_event.from_first) else below_event.interior_to_left) def detect_intersection(self, below_event: LeftEvent, event: LeftEvent) -> bool: """ Populates events queue with intersection events. Checks if events' segments overlap and have the same start. """ relation = self.context.segments_relation(below_event, event) if relation is Relation.CROSS or relation is Relation.TOUCH: point = self.context.segments_intersection(below_event, event) if point != below_event.start and point != below_event.end: self.divide_segment(below_event, point) if point != event.start and point != event.end: self.divide_segment(event, point) elif relation is not Relation.DISJOINT: # segments overlap if below_event.from_first is event.from_first: raise ValueError('Edges of the same polygon ' 'should not overlap.') starts_equal = below_event.start == event.start ends_equal = below_event.end == event.end start_min, start_max = ( (event, below_event) if starts_equal or (self._queue.key(event) < self._queue.key(below_event)) else (below_event, event)) end_min, end_max = ( (event.right, below_event.right) if ends_equal or (self._queue.key(event.right) < self._queue.key(below_event.right)) else (below_event.right, event.right)) if starts_equal: # both line segments are equal or share the left endpoint event.is_overlap = below_event.is_overlap = True if not ends_equal: self.divide_segment(end_max.left, end_min.start) return True elif ends_equal: # the line segments share the right endpoint self.divide_segment(start_min, start_max.start) else: self.divide_segment(start_min # one line segment includes the other one if start_min is end_max.left # no line segment includes the other one else start_max, end_min.start) self.divide_segment(start_min, start_max.start) return False def divide_segment(self, event: LeftEvent, point: Point) -> None: self.push(event.divide(point)) self.push(event.right) def push(self, event: Event) -> None: self._queue.push(event) def register_edge(self, edge: QuadEdge, *, from_first: bool, is_counterclockwise_contour: bool) -> None: start_event = LeftEvent.from_segment_endpoints( to_endpoints(edge), from_first, is_counterclockwise_contour) start_event.edge = edge self.push(start_event) self.push(start_event.right) def register_segment(self, endpoints: SegmentEndpoints, *, from_first: bool, is_counterclockwise_contour: bool) -> None: start_event = LeftEvent.from_segment_endpoints( endpoints, from_first, is_counterclockwise_contour) self.push(start_event) self.push(start_event.right) def sweep(self) -> Iterable[LeftEvent]: sweep_line = SweepLine(self.context) queue = self._queue while queue: event = queue.pop() if event.is_left: sweep_line.add(event) above_event, below_event = (sweep_line.above(event), sweep_line.below(event)) self.compute_position(below_event, event) if (above_event is not None and self.detect_intersection(event, above_event)): self.compute_position(event, above_event) if (below_event is not None and self.detect_intersection(below_event, event)): self.compute_position(sweep_line.below(below_event), below_event) else: event = event.left if event in sweep_line: above_event, below_event = (sweep_line.above(event), sweep_line.below(event)) sweep_line.remove(event) if above_event is not None and below_event is not None: self.detect_intersection(below_event, above_event) yield event
class EventsQueue: __slots__ = 'context', '_queue' def __init__(self, context: Context) -> None: self.context = context self._queue = PriorityQueue(key=EventsQueueKey) __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) def detect_intersection(self, below_event: Event, event: Event) -> None: relation = self.context.segments_relation(below_event.start, below_event.end, event.start, event.end) if relation is Relation.TOUCH or relation is Relation.CROSS: # segments touch or cross point = self.context.segments_intersection(below_event.start, below_event.end, event.start, event.end) if point != below_event.start and point != below_event.end: self._divide_segment(below_event, point) if point != event.start and point != event.end: self._divide_segment(event, point) event.set_both_relations(max(event.relation, relation)) below_event.set_both_relations(max(below_event.relation, relation)) elif relation is not Relation.DISJOINT: # segments overlap starts_equal = event.start == below_event.start start_min, start_max = ((None, None) if starts_equal else ( (event, below_event) if EventsQueueKey(event) < EventsQueueKey(below_event) else (below_event, event))) ends_equal = event.end == below_event.end end_min, end_max = ((None, None) if ends_equal else ((event.complement, below_event.complement) if (EventsQueueKey(event.complement) < EventsQueueKey(below_event.complement)) else (below_event.complement, event.complement))) if starts_equal: if ends_equal: # segments are equal event.set_both_relations(relation) below_event.set_both_relations(relation) else: # segments share the left endpoint end_min.set_both_relations(relation) end_max.complement.relation = relation self._divide_segment(end_max.complement, end_min.start) elif ends_equal: # segments share the right endpoint start_max.set_both_relations(relation) start_min.complement.relation = relation self._divide_segment(start_min, start_max.start) elif start_min is end_max.complement: # one line segment includes the other one start_max.set_both_relations(relation) start_min_original_relationship = start_min.relation start_min.relation = relation self._divide_segment(start_min, end_min.start) start_min.relation = start_min_original_relationship start_min.complement.relation = relation self._divide_segment(start_min, start_max.start) else: # no line segment includes the other one start_max.relation = relation self._divide_segment(start_max, end_min.start) start_min.complement.relation = relation self._divide_segment(start_min, start_max.start) def peek(self) -> Event: return self._queue.peek() def pop(self) -> Event: return self._queue.pop() def push(self, event: Event) -> None: if event.start == event.end: raise ValueError('Degenerate segment found ' 'with both endpoints being: {}.'.format( event.start)) self._queue.push(event) def _divide_segment(self, event: Event, break_point: Point) -> None: left_event = event.complement.complement = Event( start=break_point, complement=event.complement, is_left_endpoint=True, relation=event.complement.relation, segments_ids=event.segments_ids) right_event = event.complement = Event( start=break_point, complement=event, is_left_endpoint=False, relation=event.relation, segments_ids=event.complement.segments_ids) self.push(left_event) self.push(right_event)
class EventsQueue: @classmethod def from_segments(cls, segments: Sequence[Segment], *, context: Context) -> 'EventsQueue': result = cls(context) for index, segment in enumerate(segments): event = LeftEvent.from_segment(segment, index) result.push(event) result.push(event.right) return result __slots__ = 'context', '_queue' def __init__(self, context: Context) -> None: self.context = context self._queue = PriorityQueue(key=EventsQueueKey) __repr__ = generate_repr(__init__) def __bool__(self) -> bool: return bool(self._queue) def detect_intersection(self, below_event: LeftEvent, event: LeftEvent, sweep_line: SweepLine) -> None: relation = self.context.segments_relation(below_event, event) if relation is Relation.DISJOINT: return elif relation is Relation.TOUCH or relation is Relation.CROSS: # segments touch or cross point = self.context.segments_intersection(below_event, event) assert event.segments_ids.isdisjoint(below_event.segments_ids) if point != below_event.start and point != below_event.end: below_below = sweep_line.below(below_event) assert not (below_below is not None and below_below.start == below_event.start and below_below.end == point) self.push(below_event.divide(point)) self.push(below_event.right) if point != event.start and point != event.end: above_event = sweep_line.above(event) if (above_event is not None and above_event.start == event.start and above_event.end == point): sweep_line.remove(above_event) self.push(event.divide(point)) self.push(event.right) event.merge_with(above_event) else: self.push(event.divide(point)) self.push(event.right) else: # segments overlap starts_equal = event.start == below_event.start start_min, start_max = ((event, below_event) if ( starts_equal or EventsQueueKey(event) < EventsQueueKey(below_event)) else (below_event, event)) ends_equal = event.end == below_event.end end_min, end_max = ((event.right, below_event.right) if ends_equal or (EventsQueueKey(event.right) < EventsQueueKey( below_event.right)) else (below_event.right, event.right)) if starts_equal: assert not ends_equal # segments share the left endpoint sweep_line.remove(end_max.left) self.push(end_max.left.divide(end_min.start)) event.merge_with(below_event) elif ends_equal: # segments share the right endpoint start_max.merge_with(start_min.divide(start_max.start)) self.push(start_min.right) elif start_min is end_max.left: # one line segment includes the other one self.push(start_min.divide(end_min.start)) self.push(start_min.right) start_max.merge_with(start_min.divide(start_max.start)) self.push(start_min.right) else: # no line segment includes the other one self.push(start_max.divide(end_min.start)) start_max.merge_with(start_min.divide(start_max.start)) self.push(start_max.right) self.push(start_min.right) def peek(self) -> Event: return self._queue.peek() def pop(self) -> Event: return self._queue.pop() def push(self, event: Event) -> None: if event.start == event.end: raise ValueError('Degenerate segment found ' 'with both endpoints being: {}.'.format( event.start)) self._queue.push(event)
class EventsQueue: __slots__ = '_queue', def __init__(self) -> None: self._queue = PriorityQueue(key=EventsQueueKey) @staticmethod def compute_position(below_event: Optional[Event], event: Event) -> None: if below_event is not None: event.other_interior_to_left = ( below_event.other_interior_to_left if (event.from_left is below_event.from_left) else below_event.interior_to_left) def detect_intersection(self, below_event: Event, event: Event) -> bool: """ Populates events queue with intersection events. Checks if events' segments overlap and have the same start. """ below_segment, segment = below_event.segment, event.segment relationship = segments_relationship(below_segment, segment) if relationship is SegmentsRelationship.OVERLAP: # segments overlap if below_event.from_left is event.from_left: raise ValueError('Edges of the same polygon ' 'should not overlap.') starts_equal = below_event.start == event.start ends_equal = below_event.end == event.end start_min, start_max = ((None, None) if starts_equal else ( (event, below_event) if (EventsQueueKey(event) < EventsQueueKey(below_event)) else (below_event, event))) end_min, end_max = ((None, None) if ends_equal else ((event.complement, below_event.complement) if (EventsQueueKey(event.complement) < EventsQueueKey(below_event.complement)) else (below_event.complement, event.complement))) if starts_equal: # both line segments are equal or share the left endpoint event.is_overlap = below_event.is_overlap = True if not ends_equal: self.divide_segment(end_max.complement, end_min.start) return True elif ends_equal: # the line segments share the right endpoint self.divide_segment(start_min, start_max.start) else: self.divide_segment( start_min # one line segment includes the other one if start_min is end_max.complement # no line segment includes the other one else start_max, end_min.start) self.divide_segment(start_min, start_max.start) elif (relationship is not SegmentsRelationship.NONE and below_event.start != event.start and below_event.end != event.end): # segments do not intersect at endpoints point = segments_intersection(below_segment, segment) if point != below_event.start and point != below_event.end: self.divide_segment(below_event, point) if point != event.start and point != event.end: self.divide_segment(event, point) return False def divide_segment(self, event: Event, point: Point) -> None: left_event = Event(True, point, event.complement, event.from_left, event.interior_to_left, event.edge) right_event = Event(False, point, event, event.from_left, event.interior_to_left, event.edge) event.complement.complement, event.complement = left_event, right_event self._queue.push(left_event) self._queue.push(right_event) def register_edge(self, edge: QuadEdge, *, from_left: bool, is_counterclockwise_contour: bool) -> None: start, end = edge.start, edge.end interior_to_left = is_counterclockwise_contour if start > end: start, end = end, start interior_to_left = not interior_to_left start_event = Event(True, start, None, from_left, interior_to_left, edge) end_event = Event(False, end, start_event, from_left, interior_to_left, edge) start_event.complement = end_event self._queue.push(start_event) self._queue.push(end_event) def register_segment(self, segment: Segment, *, from_left: bool, is_counterclockwise_contour: bool) -> None: start, end = segment interior_to_left = is_counterclockwise_contour if start > end: start, end = end, start interior_to_left = not interior_to_left start_event = Event(True, start, None, from_left, interior_to_left) end_event = Event(False, end, start_event, from_left, interior_to_left) start_event.complement = end_event self._queue.push(start_event) self._queue.push(end_event) def sweep(self) -> Iterable[Event]: sweep_line = SweepLine() while self._queue: event = self._queue.pop() if event.is_left_endpoint: sweep_line.add(event) above_event, below_event = (sweep_line.above(event), sweep_line.below(event)) self.compute_position(below_event, event) if (above_event is not None and self.detect_intersection(event, above_event)): self.compute_position(event, above_event) if (below_event is not None and self.detect_intersection(below_event, event)): self.compute_position(sweep_line.below(below_event), below_event) else: event = event.complement if event in sweep_line: above_event, below_event = (sweep_line.above(event), sweep_line.below(event)) sweep_line.remove(event) if above_event is not None and below_event is not None: self.detect_intersection(below_event, above_event) yield event