Source code for cam.voronoi

"""BlenderCAM 'voronoi.py'

Voronoi diagram calculator/ Delaunay triangulator

- Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, 1987, http://ect.bell-labs.com/who/sjf/
- Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/
- Additional changes for QGIS by Carson Farmer added November 2010
- 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com

Calculate Delaunay triangulation or the Voronoi polygons for a set of
2D input points.

Derived from code bearing the following notice:

The author of this software is Steven Fortune.  Copyright (c) 1994 by AT&T
Bell Laboratories.
Permission to use, copy, modify, and distribute this software for any
purpose without fee is hereby granted, provided that this entire notice
is included in all copies of any software which is or includes a copy
or modification of this software and in all copies of the supporting
documentation for such software.
THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY
REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.

Comments were incorporated from Shane O'Sullivan's translation of the
original code into C++ (http://mapviewer.skynet.ie/voronoi.html)

Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html


For programmatic use two functions are available:

computeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False) :
Takes :
	- a list of point objects (which must have x and y fields).
	- x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points.
	Returns :
	- With default options :
	  A list of 2-tuples, representing the two points of each Voronoi diagram edge.
	  Each point contains 2-tuples which are the x,y coordinates of point.
	  if formatOutput is True, returns :
			- a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.
			- and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram.
			  v1 and v2 are the indices of the vertices at the end of the edge.
	- If polygonsOutput option is True, returns :
	  A dictionary of polygons, keys are the indices of the input points,
	  values contains n-tuples representing the n points of each Voronoi diagram polygon.
	  Each point contains 2-tuples which are the x,y coordinates of point.
	  if formatOutput is True, returns :
			- A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.
			- and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon.
			  Each tuple contains the vertex indices of the polygon vertices.

computeDelaunayTriangulation(points):
	Takes a list of point objects (which must have x and y fields).
	Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle.
"""

import math
import sys

[docs] TOLERANCE = 1e-9
[docs] BIG_FLOAT = 1e38
if sys.version > '3':
[docs] PY3 = True
else: PY3 = False # ------------------------------------------------------------------
[docs] class Context(object): def __init__(self): """Init function."""
[docs] self.doPrint = 0
[docs] self.debug = 0
[docs] self.extent = () # tuple (xmin, xmax, ymin, ymax)
[docs] self.triangulate = False
[docs] self.vertices = [] # list of vertex 2-tuples: (x,y)
# equation of line 3-tuple (a b c), for the equation of the line a*x+b*y = c
[docs] self.lines = []
# edge 3-tuple: (line index, vertex 1 index, vertex 2 index) if either vertex index is -1, the edge extends to infinity
[docs] self.edges = []
[docs] self.triangles = [] # 3-tuple of vertex indices
[docs] self.polygons = {} # a dict of site:[edges] pairs
########Clip functions########
[docs] def getClipEdges(self): """Get the clipped edges based on the current extent. This function iterates through the edges of a geometric shape and determines which edges are within the specified extent. It handles both finite and infinite lines, clipping them as necessary to fit within the defined boundaries. For finite lines, it checks if both endpoints are within the extent, and if not, it calculates the intersection points using the line equations. For infinite lines, it checks if at least one endpoint is within the extent and clips accordingly. Returns: list: A list of tuples, where each tuple contains two points representing the clipped edges. """ xmin, xmax, ymin, ymax = self.extent clipEdges = [] for edge in self.edges: equation = self.lines[edge[0]] # line equation if edge[1] != -1 and edge[2] != -1: # finite line x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] pt1, pt2 = (x1, y1), (x2, y2) inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) if inExtentP1 and inExtentP2: clipEdges.append((pt1, pt2)) elif inExtentP1 and not inExtentP2: pt2 = self.clipLine(x1, y1, equation, leftDir=False) clipEdges.append((pt1, pt2)) elif not inExtentP1 and inExtentP2: pt1 = self.clipLine(x2, y2, equation, leftDir=True) clipEdges.append((pt1, pt2)) else: # infinite line if edge[1] != -1: x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] leftDir = False else: x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] leftDir = True if self.inExtent(x1, y1): pt1 = (x1, y1) pt2 = self.clipLine(x1, y1, equation, leftDir) clipEdges.append((pt1, pt2)) return clipEdges
[docs] def getClipPolygons(self, closePoly): """Get clipped polygons based on the provided edges. This function processes a set of polygons defined by their edges and vertices, clipping them according to the specified extent. It checks whether each edge is finite or infinite and determines if the endpoints of each edge are within the defined extent. If they are not, the function calculates the intersection points with the extent boundaries. The resulting clipped edges are then used to create polygons, which are returned as a dictionary. The user can specify whether to close the polygons or leave them open. Args: closePoly (bool): A flag indicating whether to close the polygons. Returns: dict: A dictionary where keys are polygon indices and values are lists of points defining the clipped polygons. """ xmin, xmax, ymin, ymax = self.extent poly = {} for inPtsIdx, edges in self.polygons.items(): clipEdges = [] for edge in edges: equation = self.lines[edge[0]] # line equation if edge[1] != -1 and edge[2] != -1: # finite line x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] pt1, pt2 = (x1, y1), (x2, y2) inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) if inExtentP1 and inExtentP2: clipEdges.append((pt1, pt2)) elif inExtentP1 and not inExtentP2: pt2 = self.clipLine(x1, y1, equation, leftDir=False) clipEdges.append((pt1, pt2)) elif not inExtentP1 and inExtentP2: pt1 = self.clipLine(x2, y2, equation, leftDir=True) clipEdges.append((pt1, pt2)) else: # infinite line if edge[1] != -1: x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] leftDir = False else: x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] leftDir = True if self.inExtent(x1, y1): pt1 = (x1, y1) pt2 = self.clipLine(x1, y1, equation, leftDir) clipEdges.append((pt1, pt2)) # create polygon definition from edges and check if polygon is completely closed polyPts, complete = self.orderPts(clipEdges) if not complete: startPt = polyPts[0] endPt = polyPts[-1] if startPt[0] == endPt[0] or startPt[1] == endPt[1]: # if start & end points are collinear then they are along an extent border polyPts.append(polyPts[0]) # simple close else: # close at extent corner if (startPt[0] == xmin and endPt[1] == ymax) or ( endPt[0] == xmin and startPt[1] == ymax): # upper left polyPts.append((xmin, ymax)) # corner point polyPts.append(polyPts[0]) # close polygon if (startPt[0] == xmax and endPt[1] == ymax) or ( endPt[0] == xmax and startPt[1] == ymax): # upper right polyPts.append((xmax, ymax)) polyPts.append(polyPts[0]) if (startPt[0] == xmax and endPt[1] == ymin) or ( endPt[0] == xmax and startPt[1] == ymin): # bottom right polyPts.append((xmax, ymin)) polyPts.append(polyPts[0]) if (startPt[0] == xmin and endPt[1] == ymin) or ( endPt[0] == xmin and startPt[1] == ymin): # bottom left polyPts.append((xmin, ymin)) polyPts.append(polyPts[0]) if not closePoly: # unclose polygon polyPts = polyPts[:-1] poly[inPtsIdx] = polyPts return poly
[docs] def clipLine(self, x1, y1, equation, leftDir): """Clip a line segment defined by its endpoints against a bounding box. This function calculates the intersection points of a line defined by the given equation with the bounding box defined by the extent of the object. Depending on the direction specified (left or right), it will return the appropriate intersection point that lies within the bounds. Args: x1 (float): The x-coordinate of the first endpoint of the line. y1 (float): The y-coordinate of the first endpoint of the line. equation (tuple): A tuple containing the coefficients (a, b, c) of the line equation in the form ax + by + c = 0. leftDir (bool): A boolean indicating the direction to clip the line. If True, clip towards the left; otherwise, clip towards the right. Returns: tuple: The coordinates of the clipped point as (x, y). """ xmin, xmax, ymin, ymax = self.extent a, b, c = equation if b == 0: # vertical line if leftDir: # left is bottom of vertical line return (x1, ymax) else: return (x1, ymin) elif a == 0: # horizontal line if leftDir: return (xmin, y1) else: return (xmax, y1) else: y2_at_xmin = (c - a * xmin) / b y2_at_xmax = (c - a * xmax) / b x2_at_ymin = (c - b * ymin) / a x2_at_ymax = (c - b * ymax) / a intersectPts = [] if ymin <= y2_at_xmin <= ymax: # valid intersect point intersectPts.append((xmin, y2_at_xmin)) if ymin <= y2_at_xmax <= ymax: intersectPts.append((xmax, y2_at_xmax)) if xmin <= x2_at_ymin <= xmax: intersectPts.append((x2_at_ymin, ymin)) if xmin <= x2_at_ymax <= xmax: intersectPts.append((x2_at_ymax, ymax)) # delete duplicate (happens if intersect point is at extent corner) intersectPts = set(intersectPts) # choose target intersect point if leftDir: pt = min(intersectPts) # smaller x value else: pt = max(intersectPts) return pt
[docs] def inExtent(self, x, y): """Check if a point is within the defined extent. This function determines whether the given coordinates (x, y) fall within the boundaries defined by the extent of the object. The extent is defined by its minimum and maximum x and y values (xmin, xmax, ymin, ymax). The function returns True if the point is within these bounds, and False otherwise. Args: x (float): The x-coordinate of the point to check. y (float): The y-coordinate of the point to check. Returns: bool: True if the point (x, y) is within the extent, False otherwise. """ xmin, xmax, ymin, ymax = self.extent return x >= xmin and x <= xmax and y >= ymin and y <= ymax
[docs] def orderPts(self, edges): """Order points to form a polygon. This function takes a list of edges, where each edge is represented as a pair of points, and orders the points to create a polygon. It identifies the starting and ending points of the polygon and ensures that the points are connected in the correct order. If all points are duplicates, it recognizes that the polygon is complete and handles it accordingly. Args: edges (list): A list of edges, where each edge is a tuple or list containing two points. Returns: tuple: A tuple containing: - list: The ordered list of polygon points. - bool: A flag indicating whether the polygon is complete. """ poly = [] # returned polygon points list [pt1, pt2, pt3, pt4 ....] pts = [] # get points list for edge in edges: pts.extend([pt for pt in edge]) # try to get start & end point try: # start and end point aren't duplicate startPt, endPt = [pt for pt in pts if pts.count(pt) < 2] except: # all points are duplicate --> polygon is complete --> append some or other edge points complete = True firstIdx = 0 poly.append(edges[0][0]) poly.append(edges[0][1]) else: # incomplete --> append the first edge points complete = False # search first edge for i, edge in enumerate(edges): if startPt in edge: # find firstIdx = i break poly.append(edges[firstIdx][0]) poly.append(edges[firstIdx][1]) if poly[0] != startPt: poly.reverse() # append next points in list del edges[firstIdx] while edges: # all points will be treated when edges list will be empty currentPt = poly[-1] # last item for i, edge in enumerate(edges): if currentPt == edge[0]: poly.append(edge[1]) break elif currentPt == edge[1]: poly.append(edge[0]) break del edges[i] return poly, complete
[docs] def setClipBuffer(self, xpourcent, ypourcent): """Set the clipping buffer based on percentage adjustments. This function modifies the clipping extent of an object by adjusting its boundaries according to the specified percentage values for both the x and y axes. It calculates the new minimum and maximum values for the x and y coordinates by applying the given percentages to the current extent. Args: xpourcent (float): The percentage adjustment for the x-axis. ypourcent (float): The percentage adjustment for the y-axis. Returns: None: This function does not return a value; it modifies the object's extent in place. """ xmin, xmax, ymin, ymax = self.extent witdh = xmax - xmin height = ymax - ymin xmin = xmin - witdh * xpourcent / 100 xmax = xmax + witdh * xpourcent / 100 ymin = ymin - height * ypourcent / 100 ymax = ymax + height * ypourcent / 100 self.extent = xmin, xmax, ymin, ymax
# End clip functions########
[docs] def outSite(self, s): """Handle output for a site object. This function processes the output based on the current settings of the instance. If debugging is enabled, it prints the site number and its coordinates. If triangulation is enabled, no action is taken. If printing is enabled, it prints the coordinates of the site. Args: s (object): An object representing a site, which should have attributes 'sitenum', 'x', and 'y'. Returns: None: This function does not return a value. """ if self.debug: print("site (%d) at %f %f" % (s.sitenum, s.x, s.y)) elif self.triangulate: pass elif self.doPrint: print("s %f %f" % (s.x, s.y))
[docs] def outVertex(self, s): """Add a vertex to the list of vertices. This function appends the coordinates of a given vertex to the internal list of vertices. Depending on the state of the debug, triangulate, and doPrint flags, it may also print debug information or vertex coordinates to the console. Args: s (object): An object containing the attributes `x`, `y`, and `sitenum` which represent the coordinates and identifier of the vertex. Returns: None: This function does not return a value. """ self.vertices.append((s.x, s.y)) if self.debug: print("vertex(%d) at %f %f" % (s.sitenum, s.x, s.y)) elif self.triangulate: pass elif self.doPrint: print("v %f %f" % (s.x, s.y))
[docs] def outTriple(self, s1, s2, s3): """Add a triangle defined by three site numbers to the list of triangles. This function takes three site objects, extracts their site numbers, and appends a tuple of these site numbers to the `triangles` list. If debugging is enabled, it prints the site numbers to the console. Additionally, if triangulation is enabled and printing is allowed, it prints the site numbers in a formatted manner. Args: s1 (Site): The first site object. s2 (Site): The second site object. s3 (Site): The third site object. Returns: None: This function does not return a value. """ self.triangles.append((s1.sitenum, s2.sitenum, s3.sitenum)) if self.debug: print("circle through left=%d right=%d bottom=%d" % (s1.sitenum, s2.sitenum, s3.sitenum)) elif self.triangulate and self.doPrint: print("%d %d %d" % (s1.sitenum, s2.sitenum, s3.sitenum))
[docs] def outBisector(self, edge): """Process and log the outbisector of a given edge. This function appends the parameters of the edge (a, b, c) to the lines list and optionally prints debugging information or the parameters based on the state of the debug and doPrint flags. The function is designed to handle geometric edges and their properties in a computational geometry context. Args: edge (Edge): An object representing an edge with attributes a, b, c, edgenum, and reg. Returns: None: This function does not return a value. """ self.lines.append((edge.a, edge.b, edge.c)) if self.debug: print("line(%d) %gx+%gy=%g, bisecting %d %d" % ( edge.edgenum, edge.a, edge.b, edge.c, edge.reg[0].sitenum, edge.reg[1].sitenum)) elif self.doPrint: print("l %f %f %f" % (edge.a, edge.b, edge.c))
[docs] def outEdge(self, edge): """Process an edge and update the associated polygons and edges. This function takes an edge as input and retrieves the site numbers associated with its left and right endpoints. It then updates the polygons dictionary to include the edge information for the regions associated with the edge. If the regions are not already present in the polygons dictionary, they are initialized. The function also appends the edge information to the edges list. If triangulation is not enabled, it prints the edge number and its associated site numbers. Args: edge (Edge): An instance of the Edge class containing information Returns: None: This function does not return a value. """ sitenumL = -1 if edge.ep[Edge.LE] is not None: sitenumL = edge.ep[Edge.LE].sitenum sitenumR = -1 if edge.ep[Edge.RE] is not None: sitenumR = edge.ep[Edge.RE].sitenum # polygons dict add by CF if edge.reg[0].sitenum not in self.polygons: self.polygons[edge.reg[0].sitenum] = [] if edge.reg[1].sitenum not in self.polygons: self.polygons[edge.reg[1].sitenum] = [] self.polygons[edge.reg[0].sitenum].append((edge.edgenum, sitenumL, sitenumR)) self.polygons[edge.reg[1].sitenum].append((edge.edgenum, sitenumL, sitenumR)) self.edges.append((edge.edgenum, sitenumL, sitenumR)) if not self.triangulate: if self.doPrint: print("e %d" % edge.edgenum) print(" %d " % sitenumL) print("%d" % sitenumR)
# ------------------------------------------------------------------
[docs] def voronoi(siteList, context): """Generate a Voronoi diagram from a list of sites. This function computes the Voronoi diagram for a given list of sites. It utilizes a sweep line algorithm to process site events and circle events, maintaining a priority queue and edge list to manage the geometric relationships between the sites. The function outputs the resulting edges, vertices, and bisectors to the provided context. Args: siteList (SiteList): A list of sites represented by their coordinates. context (Context): An object that handles the output of the Voronoi diagram elements, including sites, edges, and vertices. Returns: None: This function does not return a value; it outputs results directly to the context provided. """ context.extent = siteList.extent edgeList = EdgeList(siteList.xmin, siteList.xmax, len(siteList)) priorityQ = PriorityQueue(siteList.ymin, siteList.ymax, len(siteList)) siteIter = siteList.iterator() bottomsite = siteIter.next() context.outSite(bottomsite) newsite = siteIter.next() minpt = Site(-BIG_FLOAT, -BIG_FLOAT) while True: if not priorityQ.isEmpty(): minpt = priorityQ.getMinPt() if newsite and (priorityQ.isEmpty() or newsite < minpt): # newsite is smallest - this is a site event context.outSite(newsite) # get first Halfedge to the LEFT and RIGHT of the new site lbnd = edgeList.leftbnd(newsite) rbnd = lbnd.right # if this halfedge has no edge, bot = bottom site (whatever that is) # create a new edge that bisects bot = lbnd.rightreg(bottomsite) edge = Edge.bisect(bot, newsite) context.outBisector(edge) # create a new Halfedge, setting its pm field to 0 and insert # this new bisector edge between the left and right vectors in # a linked list bisector = Halfedge(edge, Edge.LE) edgeList.insert(lbnd, bisector) # if the new bisector intersects with the left edge, remove # the left edge's vertex, and put in the new one p = lbnd.intersect(bisector) if p is not None: priorityQ.delete(lbnd) priorityQ.insert(lbnd, p, newsite.distance(p)) # create a new Halfedge, setting its pm field to 1 # insert the new Halfedge to the right of the original bisector lbnd = bisector bisector = Halfedge(edge, Edge.RE) edgeList.insert(lbnd, bisector) # if this new bisector intersects with the right Halfedge p = bisector.intersect(rbnd) if p is not None: # push the Halfedge into the ordered linked list of vertices priorityQ.insert(bisector, p, newsite.distance(p)) newsite = siteIter.next() elif not priorityQ.isEmpty(): # intersection is smallest - this is a vector (circle) event # pop the Halfedge with the lowest vector off the ordered list of # vectors. Get the Halfedge to the left and right of the above HE # and also the Halfedge to the right of the right HE lbnd = priorityQ.popMinHalfedge() llbnd = lbnd.left rbnd = lbnd.right rrbnd = rbnd.right # get the Site to the left of the left HE and to the right of # the right HE which it bisects bot = lbnd.leftreg(bottomsite) top = rbnd.rightreg(bottomsite) # output the triple of sites, stating that a circle goes through them mid = lbnd.rightreg(bottomsite) context.outTriple(bot, top, mid) # get the vertex that caused this event and set the vertex number # couldn't do this earlier since we didn't know when it would be processed v = lbnd.vertex siteList.setSiteNumber(v) context.outVertex(v) # set the endpoint of the left and right Halfedge to be this vector if lbnd.edge.setEndpoint(lbnd.pm, v): context.outEdge(lbnd.edge) if rbnd.edge.setEndpoint(rbnd.pm, v): context.outEdge(rbnd.edge) # delete the lowest HE, remove all vertex events to do with the # right HE and delete the right HE edgeList.delete(lbnd) priorityQ.delete(rbnd) edgeList.delete(rbnd) # if the site to the left of the event is higher than the Site # to the right of it, then swap them and set 'pm' to RIGHT pm = Edge.LE if bot.y > top.y: bot, top = top, bot pm = Edge.RE # Create an Edge (or line) that is between the two Sites. This # creates the formula of the line, and assigns a line number to it edge = Edge.bisect(bot, top) context.outBisector(edge) # create a HE from the edge bisector = Halfedge(edge, pm) # insert the new bisector to the right of the left HE # set one endpoint to the new edge to be the vector point 'v' # If the site to the left of this bisector is higher than the right # Site, then this endpoint is put in position 0; otherwise in pos 1 edgeList.insert(llbnd, bisector) if edge.setEndpoint(Edge.RE - pm, v): context.outEdge(edge) # if left HE and the new bisector don't intersect, then delete # the left HE, and reinsert it p = llbnd.intersect(bisector) if p is not None: priorityQ.delete(llbnd) priorityQ.insert(llbnd, p, bot.distance(p)) # if right HE and the new bisector don't intersect, then reinsert it p = bisector.intersect(rrbnd) if p is not None: priorityQ.insert(bisector, p, bot.distance(p)) else: break he = edgeList.leftend.right while he is not edgeList.rightend: context.outEdge(he.edge) he = he.right Edge.EDGE_NUM = 0 # CF
# ------------------------------------------------------------------
[docs] def isEqual(a, b, relativeError=TOLERANCE): """Check if two values are nearly equal within a specified relative error. This function determines if the absolute difference between two values is within a specified relative error of the larger of the two values. It is useful for comparing floating-point numbers where precision issues may arise. Args: a (float): The first value to compare. b (float): The second value to compare. relativeError (float): The allowed relative error for the comparison. Returns: bool: True if the values are considered nearly equal, False otherwise. """ # is nearly equal to within the allowed relative error norm = max(abs(a), abs(b)) return (norm < relativeError) or (abs(a - b) < (relativeError * norm))
# ------------------------------------------------------------------
[docs] class Site(object): def __init__(self, x=0.0, y=0.0, sitenum=0): """Init function."""
[docs] self.x = x
[docs] self.y = y
[docs] self.sitenum = sitenum
[docs] def dump(self): """Dump the site information. This function prints the site number along with its x and y coordinates in a formatted string. It is primarily used for debugging or logging purposes to provide a quick overview of the site's attributes. Returns: None: This function does not return any value. """ print("Site #%d (%g, %g)" % (self.sitenum, self.x, self.y))
[docs] def __lt__(self, other): """Compare two objects based on their coordinates. This method implements the less-than comparison for objects that have x and y attributes. It first compares the y coordinates; if they are equal, it then compares the x coordinates. The method returns True if the current object is considered less than the other object based on these comparisons. Args: other (object): The object to compare against, which must have x and y attributes. Returns: bool: True if the current object is less than the other object, otherwise False. """ if self.y < other.y: return True elif self.y > other.y: return False elif self.x < other.x: return True elif self.x > other.x: return False else: return False
[docs] def __eq__(self, other): """Determine equality between two objects. This method checks if the current object is equal to another object by comparing their 'x' and 'y' attributes. If both attributes are equal for the two objects, it returns True; otherwise, it returns False. Args: other (object): The object to compare with the current object. Returns: bool: True if both objects are equal, False otherwise. """ if self.y == other.y and self.x == other.x: return True
[docs] def distance(self, other): """Calculate the distance between two points in a 2D space. This function computes the Euclidean distance between the current point (represented by the instance's coordinates) and another point provided as an argument. It uses the Pythagorean theorem to calculate the distance based on the differences in the x and y coordinates of the two points. Args: other (Point): Another point in 2D space to calculate the distance from. Returns: float: The Euclidean distance between the two points. """ dx = self.x - other.x dy = self.y - other.y return math.sqrt(dx * dx + dy * dy)
# ------------------------------------------------------------------
[docs] class Edge(object):
[docs] LE = 0 # left end indice --> edge.ep[Edge.LE]
[docs] RE = 1 # right end indice
[docs] EDGE_NUM = 0
[docs] DELETED = {} # marker value
def __init__(self): """Init function."""
[docs] self.a = 0.0 # equation of the line a*x+b*y = c
[docs] self.b = 0.0
[docs] self.c = 0.0
[docs] self.ep = [None, None] # end point (2 tuples of site)
[docs] self.reg = [None, None]
[docs] self.edgenum = 0
[docs] def dump(self): """Dump the current state of the object. This function prints the values of the object's attributes, including the edge number, and the values of a, b, c, as well as the ep and reg attributes. It is useful for debugging purposes to understand the current state of the object. Attributes: edgenum (int): The edge number of the object. a (float): The value of attribute a. b (float): The value of attribute b. c (float): The value of attribute c. ep: The value of the ep attribute. reg: The value of the reg attribute. """ print("(#%d a=%g, b=%g, c=%g)" % (self.edgenum, self.a, self.b, self.c)) print("ep", self.ep) print("reg", self.reg)
[docs] def setEndpoint(self, lrFlag, site): """Set the endpoint for a given flag. This function assigns a site to the specified endpoint flag. It checks if the corresponding endpoint for the opposite flag is not set to None. If it is None, the function returns False; otherwise, it returns True. Args: lrFlag (int): The flag indicating which endpoint to set. site (str): The site to be assigned to the specified endpoint. Returns: bool: True if the opposite endpoint is set, False otherwise. """ self.ep[lrFlag] = site if self.ep[Edge.RE - lrFlag] is None: return False return True
@staticmethod
[docs] def bisect(s1, s2): """Bisect two sites to create a new edge. This function takes two site objects and computes the bisector edge between them. It calculates the slope and intercept of the line that bisects the two sites, storing the necessary parameters in a new edge object. The edge is initialized with no endpoints, as it extends to infinity. The function determines whether to fix x or y based on the relative distances between the sites. Args: s1 (Site): The first site to be bisected. s2 (Site): The second site to be bisected. Returns: Edge: A new edge object representing the bisector between the two sites. """ newedge = Edge() newedge.reg[0] = s1 # store the sites that this edge is bisecting newedge.reg[1] = s2 # to begin with, there are no endpoints on the bisector - it goes to infinity # ep[0] and ep[1] are None # get the difference in x dist between the sites dx = float(s2.x - s1.x) dy = float(s2.y - s1.y) adx = abs(dx) # make sure that the difference in positive ady = abs(dy) # get the slope of the line newedge.c = float(s1.x * dx + s1.y * dy + (dx * dx + dy * dy) * 0.5) if adx > ady: # set formula of line, with x fixed to 1 newedge.a = 1.0 newedge.b = dy / dx newedge.c /= dx else: # set formula of line, with y fixed to 1 newedge.b = 1.0 newedge.a = dx / dy newedge.c /= dy newedge.edgenum = Edge.EDGE_NUM Edge.EDGE_NUM += 1 return newedge
# ------------------------------------------------------------------
[docs] class Halfedge(object): def __init__(self, edge=None, pm=Edge.LE): """Init function."""
[docs] self.left = None # left Halfedge in the edge list
[docs] self.right = None # right Halfedge in the edge list
[docs] self.qnext = None # priority queue linked list pointer
[docs] self.edge = edge # edge list Edge
[docs] self.pm = pm
[docs] self.vertex = None # Site()
[docs] self.ystar = BIG_FLOAT
[docs] def dump(self): """Dump the internal state of the object. This function prints the current values of the object's attributes, including left, right, edge, pm, vertex, and ystar. If the vertex attribute is present and has a dump method, it will call that method to print the vertex's internal state. Otherwise, it will print "None" for the vertex. Attributes: left: The left halfedge associated with this object. right: The right halfedge associated with this object. edge: The edge associated with this object. pm: The PM associated with this object. vertex: The vertex associated with this object, which may have its own dump method. ystar: The ystar value associated with this object. """ print("Halfedge--------------------------") print("Left: ", self.left) print("Right: ", self.right) print("Edge: ", self.edge) print("PM: ", self.pm) print("Vertex: "), if self.vertex: self.vertex.dump() else: print("None") print("Ystar: ", self.ystar)
[docs] def __lt__(self, other): """Compare two objects based on their ystar and vertex attributes. This method implements the less-than comparison for objects. It first compares the `ystar` attributes of the two objects. If they are equal, it then compares the x-coordinate of their `vertex` attributes to determine the order. Args: other (YourClass): The object to compare against. Returns: bool: True if the current object is less than the other object, False otherwise. """ if self.ystar < other.ystar: return True elif self.ystar > other.ystar: return False elif self.vertex.x < other.vertex.x: return True elif self.vertex.x > other.vertex.x: return False else: return False
[docs] def __eq__(self, other): """Check equality of two objects. This method compares the current object with another object to determine if they are equal. It checks if the 'ystar' attribute and the 'x' coordinate of the 'vertex' attribute are the same for both objects. Args: other (object): The object to compare with the current instance. Returns: bool: True if both objects are considered equal, False otherwise. """ if self.ystar == other.ystar and self.vertex.x == other.vertex.x: return True
[docs] def leftreg(self, default): """Retrieve the left registration value based on the edge state. This function checks the state of the edge attribute. If the edge is not set, it returns the provided default value. If the edge is set and its property indicates a left edge (Edge.LE), it returns the left registration value. Otherwise, it returns the right registration value. Args: default: The value to return if the edge is not set. Returns: The left registration value if applicable, otherwise the default value. """ if not self.edge: return default elif self.pm == Edge.LE: return self.edge.reg[Edge.LE] else: return self.edge.reg[Edge.RE]
[docs] def rightreg(self, default): """Retrieve the appropriate registration value based on the edge state. This function checks if the current edge is set. If it is not set, it returns the provided default value. If the edge is set and the current state is Edge.LE, it returns the registration value associated with Edge.RE. Otherwise, it returns the registration value associated with Edge.LE. Args: default: The value to return if there is no edge set. Returns: The registration value corresponding to the current edge state or the default value if no edge is set. """ if not self.edge: return default elif self.pm == Edge.LE: return self.edge.reg[Edge.RE] else: return self.edge.reg[Edge.LE]
# returns True if p is to right of halfedge self
[docs] def isPointRightOf(self, pt): """Determine if a point is to the right of a half-edge. This function checks whether the given point `pt` is located to the right of the half-edge represented by the current object. It takes into account the position of the top site of the edge and various geometric properties to make this determination. The function uses the edge's parameters to evaluate the relationship between the point and the half- edge. Args: pt (Point): A point object with x and y coordinates. Returns: bool: True if the point is to the right of the half-edge, False otherwise. """ e = self.edge topsite = e.reg[1] right_of_site = pt.x > topsite.x if right_of_site and self.pm == Edge.LE: return True if not right_of_site and self.pm == Edge.RE: return False if e.a == 1.0: dyp = pt.y - topsite.y dxp = pt.x - topsite.x fast = 0 if (not right_of_site and e.b < 0.0) or (right_of_site and e.b >= 0.0): above = dyp >= e.b * dxp fast = above else: above = pt.x + pt.y * e.b > e.c if e.b < 0.0: above = not above if not above: fast = 1 if not fast: dxs = topsite.x - (e.reg[0]).x above = e.b * (dxp * dxp - dyp * dyp) < dxs * dyp * \ (1.0 + 2.0 * dxp / dxs + e.b * e.b) if e.b < 0.0: above = not above else: # e.b == 1.0 yl = e.c - e.a * pt.x t1 = pt.y - yl t2 = pt.x - topsite.x t3 = yl - topsite.y above = t1 * t1 > t2 * t2 + t3 * t3 if self.pm == Edge.LE: return above else: return not above
# -------------------------- # create a new site where the Halfedges el1 and el2 intersect
[docs] def intersect(self, other): """Create a new site where two edges intersect. This function calculates the intersection point of two edges, represented by the current instance and another instance passed as an argument. It first checks if either edge is None, and if they belong to the same parent region. If the edges are parallel or do not intersect, it returns None. If an intersection point is found, it creates and returns a new Site object at the intersection coordinates. Args: other (Edge): Another edge to intersect with the current edge. Returns: Site or None: A Site object representing the intersection point if an intersection occurs; otherwise, None. """ e1 = self.edge e2 = other.edge if (e1 is None) or (e2 is None): return None # if the two edges bisect the same parent return None if e1.reg[1] is e2.reg[1]: return None d = e1.a * e2.b - e1.b * e2.a if isEqual(d, 0.0): return None xint = (e1.c * e2.b - e2.c * e1.b) / d yint = (e2.c * e1.a - e1.c * e2.a) / d if e1.reg[1] < e2.reg[1]: he = self e = e1 else: he = other e = e2 rightOfSite = xint >= e.reg[1].x if ((rightOfSite and he.pm == Edge.LE) or (not rightOfSite and he.pm == Edge.RE)): return None # create a new site at the point of intersection - this is a new # vector event waiting to happen return Site(xint, yint)
# ------------------------------------------------------------------
[docs] class EdgeList(object): def __init__(self, xmin, xmax, nsites): """Init function.""" if xmin > xmax: xmin, xmax = xmax, xmin
[docs] self.hashsize = int(2 * math.sqrt(nsites + 4))
[docs] self.xmin = xmin
[docs] self.deltax = float(xmax - xmin)
[docs] self.hash = [None] * self.hashsize
[docs] self.leftend = Halfedge()
[docs] self.rightend = Halfedge()
[docs] self.leftend.right = self.rightend
[docs] self.rightend.left = self.leftend
self.hash[0] = self.leftend self.hash[-1] = self.rightend
[docs] def insert(self, left, he): """Insert a node into a doubly linked list. This function takes a node and inserts it into the list immediately after the specified left node. It updates the pointers of the surrounding nodes to maintain the integrity of the doubly linked list. Args: left (Node): The node after which the new node will be inserted. he (Node): The new node to be inserted into the list. """ he.left = left he.right = left.right left.right.left = he left.right = he
[docs] def delete(self, he): """Delete a node from a doubly linked list. This function updates the pointers of the neighboring nodes to remove the specified node from the list. It also marks the node as deleted by setting its edge attribute to Edge.DELETED. Args: he (Node): The node to be deleted from the list. """ he.left.right = he.right he.right.left = he.left he.edge = Edge.DELETED
# Get entry from hash table, pruning any deleted nodes
[docs] def gethash(self, b): """Retrieve an entry from the hash table, ignoring deleted nodes. This function checks if the provided index is within the valid range of the hash table. If the index is valid, it retrieves the corresponding entry. If the entry is marked as deleted, it updates the hash table to remove the reference to the deleted entry and returns None. Args: b (int): The index in the hash table to retrieve the entry from. Returns: object: The entry at the specified index, or None if the index is out of bounds or if the entry is marked as deleted. """ if (b < 0 or b >= self.hashsize): return None he = self.hash[b] if he is None or he.edge is not Edge.DELETED: return he # Hash table points to deleted half edge. Patch as necessary. self.hash[b] = None return None
[docs] def leftbnd(self, pt): """Find the left boundary half-edge for a given point. This function computes the appropriate half-edge that is to the left of the specified point. It utilizes a hash table to quickly locate the half-edge that is closest to the desired position based on the x-coordinate of the point. If the initial bucket derived from the point's x-coordinate does not contain a valid half-edge, the function will search adjacent buckets until it finds one. Once a half-edge is located, it will traverse through the linked list of half-edges to find the correct one that lies to the left of the point. Args: pt (Point): A point object containing x and y coordinates. Returns: HalfEdge: The half-edge that is to the left of the given point. """ # Use hash table to get close to desired halfedge bucket = int(((pt.x - self.xmin) / self.deltax * self.hashsize)) if bucket < 0: bucket = 0 if bucket >= self.hashsize: bucket = self.hashsize - 1 he = self.gethash(bucket) if (he is None): i = 1 while True: he = self.gethash(bucket - i) if (he is not None): break he = self.gethash(bucket + i) if (he is not None): break i += 1 # Now search linear list of halfedges for the corect one if (he is self.leftend) or (he is not self.rightend and he.isPointRightOf(pt)): he = he.right while he is not self.rightend and he.isPointRightOf(pt): he = he.right he = he.left else: he = he.left while he is not self.leftend and not he.isPointRightOf(pt): he = he.left # Update hash table and reference counts if bucket > 0 and bucket < self.hashsize - 1: self.hash[bucket] = he return he
# ------------------------------------------------------------------
[docs] class PriorityQueue(object): def __init__(self, ymin, ymax, nsites): """Init function."""
[docs] self.ymin = ymin
[docs] self.deltay = ymax - ymin
[docs] self.hashsize = int(4 * math.sqrt(nsites))
[docs] self.count = 0
[docs] self.minidx = 0
[docs] self.hash = []
for i in range(self.hashsize): self.hash.append(Halfedge())
[docs] def __len__(self): """Return the length of the object. This method returns the count of items in the object, which is useful for determining how many elements are present. It is typically used to support the built-in `len()` function. Returns: int: The number of items in the object. """ return self.count
[docs] def isEmpty(self): """Check if the object is empty. This method determines whether the object contains any elements by checking the value of the count attribute. If the count is zero, the object is considered empty; otherwise, it is not. Returns: bool: True if the object is empty, False otherwise. """ return self.count == 0
[docs] def insert(self, he, site, offset): """Insert a new element into the data structure. This function inserts a new element represented by `he` into the appropriate position in the data structure based on its value. It updates the `ystar` attribute of the element and links it to the next element in the list. The function also manages the count of elements in the structure. Args: he (Element): The element to be inserted, which contains a vertex and a y-coordinate. site (Site): The site object that provides the y-coordinate for the insertion. offset (float): The offset to be added to the y-coordinate of the site. Returns: None: This function does not return a value. """ he.vertex = site he.ystar = site.y + offset last = self.hash[self.getBucket(he)] next = last.qnext while (next is not None) and he > next: last = next next = last.qnext he.qnext = last.qnext last.qnext = he self.count += 1
[docs] def delete(self, he): """Delete a specified element from the data structure. This function removes the specified element (he) from the linked list associated with the corresponding bucket in the hash table. It traverses the linked list until it finds the element to delete, updates the pointers to bypass the deleted element, and decrements the count of elements in the structure. If the element is found and deleted, its vertex is set to None to indicate that it is no longer valid. Args: he (Element): The element to be deleted from the data structure. """ if he.vertex is not None: last = self.hash[self.getBucket(he)] while last.qnext is not he: last = last.qnext last.qnext = he.qnext self.count -= 1 he.vertex = None
[docs] def getBucket(self, he): """Get the appropriate bucket index for a given value. This function calculates the bucket index based on the provided value and the object's parameters. It ensures that the bucket index is within the valid range, adjusting it if necessary. The calculation is based on the difference between a specified value and a minimum value, scaled by a delta value and the size of the hash table. The function also updates the minimum index if the calculated bucket is lower than the current minimum index. Args: he: An object that contains the attribute `ystar`, which is used in the bucket calculation. Returns: int: The calculated bucket index, constrained within the valid range. """ bucket = int(((he.ystar - self.ymin) / self.deltay) * self.hashsize) if bucket < 0: bucket = 0 if bucket >= self.hashsize: bucket = self.hashsize - 1 if bucket < self.minidx: self.minidx = bucket return bucket
[docs] def getMinPt(self): """Retrieve the minimum point from a hash table. This function iterates through the hash table starting from the current minimum index and finds the next non-null entry. It then extracts the coordinates (x, y) of the vertex associated with that entry and returns it as a Site object. Returns: Site: An object representing the minimum point with x and y coordinates. """ while self.hash[self.minidx].qnext is None: self.minidx += 1 he = self.hash[self.minidx].qnext x = he.vertex.x y = he.ystar return Site(x, y)
[docs] def popMinHalfedge(self): """Remove and return the minimum half-edge from the data structure. This function retrieves the minimum half-edge from a hash table, updates the necessary pointers to maintain the integrity of the data structure, and decrements the count of half-edges. It effectively removes the minimum half-edge while ensuring that the next half-edge in the sequence is correctly linked. Returns: HalfEdge: The minimum half-edge that was removed from the data structure. """ curr = self.hash[self.minidx].qnext self.hash[self.minidx].qnext = curr.qnext self.count -= 1 return curr
# ------------------------------------------------------------------
[docs] class SiteList(object): def __init__(self, pointList): """Init function."""
[docs] self.__sites = []
[docs] self.__sitenum = 0
[docs] self.__xmin = min([pt.x for pt in pointList])
[docs] self.__ymin = min([pt.y for pt in pointList])
[docs] self.__xmax = max([pt.x for pt in pointList])
[docs] self.__ymax = max([pt.y for pt in pointList])
[docs] self.__extent = (self.__xmin, self.__xmax, self.__ymin, self.__ymax)
for i, pt in enumerate(pointList): self.__sites.append(Site(pt.x, pt.y, i)) self.__sites.sort()
[docs] def setSiteNumber(self, site): """Set the site number for a given site. This function assigns a unique site number to the provided site object. It updates the site object's 'sitenum' attribute with the current value of the instance's private '__sitenum' attribute and then increments the '__sitenum' for the next site. Args: site (object): An object representing a site that has a 'sitenum' attribute. Returns: None: This function does not return a value. """ site.sitenum = self.__sitenum self.__sitenum += 1
[docs] class Iterator(object): def __init__(this, lst): """Init function."""
[docs] this.generator = (s for s in lst)
[docs] def __iter__(this): """Return the iterator object itself. This method is part of the iterator protocol. It allows an object to be iterable by returning the iterator object itself when the `__iter__` method is called. This is typically used in conjunction with the `__next__` method to iterate over the elements of the object. Returns: self: The iterator object itself. """ return this
[docs] def next(this): """Retrieve the next item from a generator. This function attempts to get the next value from the provided generator. It handles both Python 2 and Python 3 syntax for retrieving the next item. If the generator is exhausted, it returns None instead of raising an exception. Args: this (object): An object that contains a generator attribute. Returns: object: The next item from the generator, or None if the generator is exhausted. """ try: if PY3: return this.generator.__next__() else: return this.generator.next() except StopIteration: return None
[docs] def iterator(self): """Create an iterator for the sites. This function returns an iterator object that allows iteration over the collection of sites stored in the instance. It utilizes the SiteList.Iterator class to facilitate the iteration process. Returns: Iterator: An iterator for the sites in the SiteList. """ return SiteList.Iterator(self.__sites)
[docs] def __iter__(self): """Iterate over the sites in the SiteList. This method returns an iterator for the SiteList, allowing for traversal of the contained sites. It utilizes the internal Iterator class to manage the iteration process. Returns: Iterator: An iterator for the sites in the SiteList. """ return SiteList.Iterator(self.__sites)
[docs] def __len__(self): """Return the number of sites. This method returns the length of the internal list of sites. It is used to determine how many sites are currently stored in the object. The length is calculated using the built-in `len()` function on the `__sites` attribute. Returns: int: The number of sites in the object. """ return len(self.__sites)
[docs] def _getxmin(self): """Retrieve the minimum x-coordinate value. This function accesses and returns the private attribute __xmin, which holds the minimum x-coordinate value for the object. It is typically used in contexts where the minimum x value is needed for calculations or comparisons. Returns: float: The minimum x-coordinate value. """ return self.__xmin
[docs] def _getymin(self): """Retrieve the minimum y-coordinate value. This function returns the minimum y-coordinate value stored in the instance variable `__ymin`. It is typically used in contexts where the minimum y-value is needed for calculations or comparisons. Returns: float: The minimum y-coordinate value. """ return self.__ymin
[docs] def _getxmax(self): """Retrieve the maximum x value. This function returns the maximum x value stored in the instance. It is a private method intended for internal use within the class and provides access to the __xmax attribute. Returns: float: The maximum x value. """ return self.__xmax
[docs] def _getymax(self): """Retrieve the maximum y-coordinate value. This function accesses and returns the private attribute __ymax, which represents the maximum y-coordinate value stored in the instance. Returns: float: The maximum y-coordinate value. """ return self.__ymax
[docs] def _getextent(self): """Retrieve the extent of the object. This function returns the current extent of the object, which is typically a representation of its boundaries or limits. The extent is stored as a private attribute and can be used for various purposes such as rendering, collision detection, or spatial analysis. Returns: The extent of the object, which may be in a specific format depending on the implementation (e.g., a tuple, list, or custom object). """ return self.__extent
[docs] xmin = property(_getxmin)
[docs] ymin = property(_getymin)
[docs] xmax = property(_getxmax)
[docs] ymax = property(_getymax)
[docs] extent = property(_getextent)
# ------------------------------------------------------------------
[docs] def computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, formatOutput=False, closePoly=True): """Compute the Voronoi diagram for a set of points. This function takes a list of point objects and computes the Voronoi diagram, which partitions the plane into regions based on the distance to the input points. The function allows for optional buffering of the bounding box and can return various formats of the output, including edges or polygons of the Voronoi diagram. Args: points (list): A list of point objects, each having 'x' and 'y' attributes. xBuff (float?): The expansion percentage of the bounding box in the x-direction. Defaults to 0. yBuff (float?): The expansion percentage of the bounding box in the y-direction. Defaults to 0. polygonsOutput (bool?): If True, returns polygons instead of edges. Defaults to False. formatOutput (bool?): If True, formats the output to include vertex coordinates. Defaults to False. closePoly (bool?): If True, closes the polygons by repeating the first point at the end. Defaults to True. Returns: If `polygonsOutput` is False: - list: A list of 2-tuples representing the edges of the Voronoi diagram, where each tuple contains the x and y coordinates of the points. If `formatOutput` is True: - tuple: A list of 2-tuples for vertex coordinates and a list of edges indices. If `polygonsOutput` is True: - dict: A dictionary where keys are indices of input points and values are n-tuples representing the vertices of each Voronoi polygon. If `formatOutput` is True: - tuple: A list of 2-tuples for vertex coordinates and a dictionary of polygon vertex indices. """ siteList = SiteList(points) context = Context() voronoi(siteList, context) context.setClipBuffer(xBuff, yBuff) if not polygonsOutput: clipEdges = context.getClipEdges() if formatOutput: vertices, edgesIdx = formatEdgesOutput(clipEdges) return vertices, edgesIdx else: return clipEdges else: clipPolygons = context.getClipPolygons(closePoly) if formatOutput: vertices, polyIdx = formatPolygonsOutput(clipPolygons) return vertices, polyIdx else: return clipPolygons
[docs] def formatEdgesOutput(edges): """Format edges output for a list of edges. This function takes a list of edges, where each edge is represented as a tuple of points. It extracts unique points from the edges and creates a mapping of these points to their corresponding indices. The function then returns a list of unique points and a list of edges represented by their indices. Args: edges (list): A list of edges, where each edge is a tuple containing points. Returns: tuple: A tuple containing: - list: A list of unique points extracted from the edges. - list: A list of edges represented by their corresponding indices. """ # get list of points pts = [] for edge in edges: pts.extend(edge) # get unique values pts = set(pts) # unique values (tuples are hashable) # get dict {values:index} valuesIdxDict = dict(zip(pts, range(len(pts)))) # get edges index reference edgesIdx = [] for edge in edges: edgesIdx.append([valuesIdxDict[pt] for pt in edge]) return list(pts), edgesIdx
[docs] def formatPolygonsOutput(polygons): """Format the output of polygons into a standardized structure. This function takes a dictionary of polygons, where each polygon is represented as a list of points. It extracts unique points from all polygons and creates an index mapping for these points. The output consists of a list of unique points and a dictionary that maps each polygon's original indices to their corresponding indices in the unique points list. Args: polygons (dict): A dictionary where keys are polygon identifiers and values are lists of points (tuples) representing the vertices of the polygons. Returns: tuple: A tuple containing: - list: A list of unique points (tuples) extracted from the input polygons. - dict: A dictionary mapping each polygon's identifier to a list of indices corresponding to the unique points. """ # get list of points pts = [] for poly in polygons.values(): pts.extend(poly) # get unique values pts = set(pts) # unique values (tuples are hashable) # get dict {values:index} valuesIdxDict = dict(zip(pts, range(len(pts)))) # get polygons index reference polygonsIdx = {} for inPtsIdx, poly in polygons.items(): polygonsIdx[inPtsIdx] = [valuesIdxDict[pt] for pt in poly] return list(pts), polygonsIdx
# ------------------------------------------------------------------
[docs] def computeDelaunayTriangulation(points): """Compute the Delaunay triangulation for a set of points. This function takes a list of point objects, each of which must have 'x' and 'y' fields. It computes the Delaunay triangulation and returns a list of 3-tuples, where each tuple contains the indices of the points that form a Delaunay triangle. The triangulation is performed using the Voronoi diagram method. Args: points (list): A list of point objects with 'x' and 'y' attributes. Returns: list: A list of 3-tuples representing the indices of points that form Delaunay triangles. """ siteList = SiteList(points) context = Context() context.triangulate = True voronoi(siteList, context) return context.triangles
# ----------------------------------------------------------------------------- # def shapely_voronoi(amount): # import random # # rcoord = [] # x = 0 # while x < self.amount: # rcoord.append((width * random.random(), height * random.random(), 0.02 * random.random())) # x += 1 # # points = MultiPoint(rcoord) # voronoi = shapely.ops.voronoi_diagram(points, tolerance=0, edges=False) # # utils.shapelyToCurve('voronoi', voronoi, 0)