diff --git a/examples/observers.py b/examples/observers.py new file mode 100644 index 0000000..f9c0b75 --- /dev/null +++ b/examples/observers.py @@ -0,0 +1,63 @@ +import os + +from voronoi import Polygon, Voronoi, VoronoiObserver, TreeObserver, DebugObserver +from voronoi.visualization import Presets + +# Define some points (a.k.a sites or cell points) +points = [ + (2.5, 2.5), (4, 7.5), (7.5, 2.5), (6, 7.5), (4, 4), (3, 3), (6, 3) +] + +# Define a bounding box / polygon +polygon = Polygon([ + (2.5, 10), (5, 10), (10, 5), (10, 2.5), (5, 0), (2.5, 0), (0, 2.5), (0, 5) +]) + +# Initialize the algorithm +v = Voronoi(polygon) + +# Attach a Voronoi observer that visualizes the Voronoi diagram every step +v.attach_observer( + VoronoiObserver( + + # Settings to pass into the visualizer's plot_all() method. + # - By default, the observer uses a set of minimalistic presets that are useful for visualizing during + # construction, clipping and the final result. Have a look at Presets.construction, Presets.clipping and + # Presets.final. + # - These settings below will update the default presets used by the observer. For example, by default, + # the arc_labels are not shown, but below we can enable the arc labels. Other parameters can be found in + # the visualizer's plot_all() method. + settings=dict(arc_labels=True, site_labels=True), + + # Callback that saves the figure every step + # If no callback is provided, it will simply display the figure in a matplotlib window + callback=lambda observer, figure: figure.savefig(f"output/voronoi/{observer.n_messages:02d}.png") + ) +) + +# Attach observer that visualizes the tree every step. This is a binary tree data structure that keeps track of where +# the arcs and the breakpoints between the arcs are going. +v.attach_observer( + TreeObserver( + # Callback that saves the figure every step + # If no callback is provided, it will render the figure in a window + callback=lambda observer, dot: dot.render(f"output/tree/{observer.n_messages:02d}") + ) +) + +# Attach a listener that listens to debug messages. +# If no callback is provided, it will print the messages. +v.attach_observer(DebugObserver(callback=lambda _: print(_))) + +# Create the output directory if it doesn't exist +if not os.path.exists("output"): + os.mkdir("output") + +if not os.path.exists("output/tree/"): + os.mkdir("output/tree/") + +if not os.path.exists("output/voronoi/"): + os.mkdir("output/voronoi/") + +# Create the Voronoi diagram +v.create_diagram(points=points) diff --git a/examples/quickstart.py b/examples/quickstart.py index edaae43..461388d 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -1,5 +1,5 @@ from typing import List -from voronoi import Voronoi, Polygon, Visualizer, Point +from voronoi import Voronoi, Polygon, Visualizer, Point, VoronoiObserver from voronoi.graph import HalfEdge, Vertex # Define some points (a.k.a sites or cell points) @@ -15,12 +15,18 @@ # Initialize the algorithm v = Voronoi(polygon) +# Optional: visualize the voronoi diagram at every step. +# You can find more information in the observers.py example file +# v.attach_observer( +# VoronoiObserver() +# ) + # Create the Voronoi diagram v.create_diagram(points=points) # Visualize the Voronoi diagram Visualizer(v) \ - .plot_sites(show_labels=True) \ + .plot_sites(show_labels=False) \ .plot_edges(show_labels=False) \ .plot_vertices() \ .show() @@ -33,23 +39,23 @@ edge, vertex, site = edges[0], vertices[0], sites[0] # Edge operations -origin: Vertex = edge.origin # The vertex in which the edge originates -target: Vertex = edge.twin.origin # The twin is the edge that goes in the other direction -target_alt: Vertex = edge.target # Same as above, but more convenient -twin: HalfEdge = edge.twin # Get the twin of this edge -next: HalfEdge = edge.next # Get the next edge -prev: HalfEdge = edge.twin.next # Get the previous edge -prev_alt: HalfEdge = edge.prev # Same as above, but more convenient +origin: Vertex = edge.origin # The vertex in which the edge originates +target: Vertex = edge.twin.origin # The twin is the edge that goes in the other direction +target_alt: Vertex = edge.target # Same as above, but more convenient +twin: HalfEdge = edge.twin # Get the twin of this edge +next_edge: HalfEdge = edge.next # Get the next edge +prev_edge: HalfEdge = edge.twin.next # Get the previous edge +prev_alt: HalfEdge = edge.prev # Same as above, but more convenient # Site operations -size: float = site.area() # The area of the cell -borders: List[HalfEdge] = site.borders() # A list of all the borders that surround this cell point -vertices: List[Vertex] = site.vertices() # A list of all the vertices around this cell point -site_x: float = site.x # X-coordinate of the site -site_xy: [float, float] = site.xy # (x, y)-coordinates of the site +size: float = site.area() # The area of the cell +borders: List[HalfEdge] = site.borders() # A list of all the borders that surround this cell point +vertices: List[Vertex] = site.vertices() # A list of all the vertices around this cell point +site_x: float = site.x # X-coordinate of the site +site_xy: [float, float] = site.xy # (x, y)-coordinates of the site +first_edge: HalfEdge = site.first_edge # Points to the first edge that is part of the border around the site # Vertex operations connected_edges: List[HalfEdge] = vertex.connected_edges # A list of all edges that are connected to this vertex vertex_x: float = vertex.x # x-coordinate of the vertex vertex_xy: [float, float] = vertex.xy # (x, y)-coordinates of the vertex -print() \ No newline at end of file diff --git a/voronoi/graph/half_edge.py b/voronoi/graph/half_edge.py index 760341c..ec3a6e0 100644 --- a/voronoi/graph/half_edge.py +++ b/voronoi/graph/half_edge.py @@ -26,7 +26,7 @@ def __repr__(self): def set_next(self, next): if next: - next.prev = self + next.prev_edge = self self.next = next def get_origin(self, y=None, max_y=None): diff --git a/voronoi/observers/voronoi_observer.py b/voronoi/observers/voronoi_observer.py index f9bf177..489db74 100644 --- a/voronoi/observers/voronoi_observer.py +++ b/voronoi/observers/voronoi_observer.py @@ -5,12 +5,12 @@ from voronoi.observers.observer import Observer import matplotlib.pyplot as plt -from voronoi.visualization.visualizer import Visualizer +from voronoi.visualization.visualizer import Visualizer, Presets class VoronoiObserver(Observer, ABC): def __init__(self, visualize_steps=True, visualize_before_clipping=False, visualize_result=True, callback=None, - figsize=(8, 8), canvas_offset=5, settings=None): + figsize=(8, 8), canvas_offset=1, settings=None): self.canvas_offset = canvas_offset self.figsize = figsize self.visualize_steps = visualize_steps @@ -28,20 +28,20 @@ def update(self, subject: Algorithm, message: Message, **kwargs): if message == Message.STEP_FINISHED and self.visualize_steps: vis = Visualizer(subject, canvas_offset=self.canvas_offset) - settings = dict(outgoing_edges=False) + settings = Presets.construction settings.update(self.settings) assert subject.sweep_line == subject.event.yd result = vis.plot_all(**settings) plt.title(str(subject.event) + "\n") elif message == Message.SWEEP_FINISHED and self.visualize_before_clipping: vis = Visualizer(subject, canvas_offset=self.canvas_offset) - settings = dict(events=False, beach_line=False, outgoing_edges=False) + settings = Presets.clipping settings.update(self.settings) result = vis.plot_all(**settings) plt.title("Sweep finished\n") elif message == Message.VORONOI_FINISHED and self.visualize_result: vis = Visualizer(subject, canvas_offset=self.canvas_offset) - settings = dict(events=False, outgoing_edges=False, arcs=False, beach_line=False, sweep_line=False) + settings = Presets.final settings.update(self.settings) result = vis.plot_all(**settings) plt.title("Voronoi completed\n") diff --git a/voronoi/visualization/__init__.py b/voronoi/visualization/__init__.py index aefc61e..85b981b 100644 --- a/voronoi/visualization/__init__.py +++ b/voronoi/visualization/__init__.py @@ -1 +1,2 @@ -from voronoi.visualization.visualizer import Visualizer \ No newline at end of file +from voronoi.visualization.visualizer import Visualizer +from voronoi.visualization.visualizer import Presets \ No newline at end of file diff --git a/voronoi/visualization/visualizer.py b/voronoi/visualization/visualizer.py index eed8ac4..bebfba7 100644 --- a/voronoi/visualization/visualizer.py +++ b/voronoi/visualization/visualizer.py @@ -29,6 +29,17 @@ class Colors: FIRST_EDGE = "#2ecc71" +class Presets: + # A minimalistic preset that is useful during construction + construction = dict(polygon=True, events=True, beach_line=True, sweep_line=True) + + # A minimalistic preset that is useful during clipping + clipping = dict(polygon=True) + + # A minimalistic preset that is useful to show the final result + final = dict() + + class Visualizer: def __init__(self, voronoi, canvas_offset=1, figsize=(8, 8)): @@ -52,9 +63,10 @@ def show(self, block=True, **kwargs): plt.show(block=block, **kwargs) return self - def plot_all(self, polygon=True, edges=True, vertices=True, sites=True, - outgoing_edges=False, events=True, beach_line=True, arcs=True, border_to_site=False, scale=1, - edge_labels=True, site_labels=True, triangles=False, sweep_line=True, arc_labels=True): + def plot_all(self, polygon=False, edges=True, vertices=True, sites=True, + outgoing_edges=False, border_to_site=False, scale=1, + edge_labels=False, site_labels=False, triangles=False, arcs=False, sweep_line=False, events=False, + arc_labels=False, beach_line=False): self.plot_sweep_line() if sweep_line else False self.plot_polygon() if polygon else False @@ -72,7 +84,8 @@ def plot_polygon(self): if hasattr(self.voronoi.bounding_poly, 'radius'): # Draw bounding box self.canvas.add_patch( - patches.Circle((self.voronoi.bounding_poly.xd, self.voronoi.bounding_poly.xd), self.voronoi.bounding_poly.radius, + patches.Circle((self.voronoi.bounding_poly.xd, self.voronoi.bounding_poly.xd), + self.voronoi.bounding_poly.radius, fill=False, edgecolor=Colors.BOUNDING_BOX) ) @@ -95,8 +108,9 @@ def plot_vertices(self, vertices=None, **kwargs): return self - def plot_outgoing_edges(self, vertices=None, scale=1, **kwargs): + def plot_outgoing_edges(self, vertices=None, scale=0.5, **kwargs): vertices = vertices or self.voronoi.vertices + scale = Decimal(str(scale)) for vertex in vertices: for edge in vertex.connected_edges: @@ -116,8 +130,9 @@ def plot_outgoing_edges(self, vertices=None, scale=1, **kwargs): direction = (x_diff / length, y_diff / length) new_end = Coordinate(start.xd + direction[0] * scale, start.yd + direction[1] * scale) - props = dict(arrowstyle="->", color=Colors.EDGE_DIRECTION, linewidth=1, **kwargs) - self.canvas.annotate(text='', xy=(new_end.xd, new_end.yd), xytext=(start.xd, start.yd), arrowprops=props) + props = dict(arrowstyle="->", color=Colors.EDGE_DIRECTION, linewidth=3, **kwargs) + self.canvas.annotate(text='', xy=(new_end.xd, new_end.yd), xytext=(start.xd, start.yd), + arrowprops=props) return self @@ -133,7 +148,7 @@ def plot_sites(self, points=None, show_labels=True, color=Colors.CELL_POINTS, zo # Add descriptions if show_labels: for point in points: - self.canvas.text(point.xd, point.yd, s=f"{point} (A={point.area(digits=2)})", zorder=15) + self.canvas.text(point.xd, point.yd, s=f"P{point.name if point.name is not None else ''}", zorder=15) return self