import numpy import math import copy class Delaunay2d: EPS = 1.23456789e-14 def __init__(self, points): # data structures self.points = points[:] # copy self.triangles = [] # cells self.edge2Triangles = {} # edge to triangle(s) map self.boundaryEdges = set() self.appliedBoundaryEdges = None self.holes = None # compute center of gravity cg = numpy.zeros((2,), numpy.float64) for pt in points: cg += pt cg /= len(points) # sort def distanceSquare(pt): d = pt - cg return numpy.dot(d, d) self.points.sort(key = distanceSquare) # create first triangle, make sure we're getting a non-zero area otherwise # drop the points area = 0.0 index = 0 stop = False while not stop and index + 2 < len(points): area = self.getArea(index, index + 1, index + 2) if abs(area) < self.EPS: del self.points[index] else: stop = True if index <= len(self.points) - 3: tri = [index, index + 1, index + 2] self.makeCounterClockwise(tri) self.triangles.append(tri) # boundary edges e01 = (tri[0], tri[1]) self.boundaryEdges.add(e01) e12 = (tri[1], tri[2]) self.boundaryEdges.add(e12) e20 = (tri[2], tri[0]) self.boundaryEdges.add(e20) e01 = self.makeKey(e01[0], e01[1]) self.edge2Triangles[e01] = [0,] e12 = self.makeKey(e12[0], e12[1]) self.edge2Triangles[e12] = [0,] e20 = self.makeKey(e20[0], e20[1]) self.edge2Triangles[e20] = [0,] else: # all the points fall on a line return # add additional points for i in range(3, len(self.points)): self.addPoint(i) # remove all triangles inside holes # TO DO def getTriangles(self): """ @return triangles """ return self.triangles def getEdges(self): """ @return egdes """ return self.edge2Triangles.keys() def getArea(self, ip0, ip1, ip2): """ Compute the parallelipiped area @param ip0 index of first vertex @param ip1 index of second vertex @param ip2 index of third vertex """ d1 = self.points[ip1] - self.points[ip0] d2 = self.points[ip2] - self.points[ip0] return (d1[0]*d2[1] - d1[1]*d2[0]) def isEdgeVisible(self, ip, edge): """ Return true iff the point lies to its right when the edge points down @param ip point index @param edge (2 point indices with orientation) @return True if visible """ area = self.getArea(ip, edge[0], edge[1]) if area < self.EPS: return True return False def makeCounterClockwise(self, ips): """ Re-order nodes to ensure positive area (in-place operation) """ area = self.getArea(ips[0], ips[1], ips[2]) if area < -self.EPS: ip1, ip2 = ips[1], ips[2] # swap ips[1], ips[2] = ip2, ip1 def flipOneEdge(self, edge): """ Flip one edge then update the data structures @return set of edges to interate over at next iteration """ # start with empty set res = set() # assume edge is sorted tris = self.edge2Triangles.get(edge, []) if len(tris) < 2: # nothing to do, just return return res iTri1, iTri2 = tris tri1 = self.triangles[iTri1] tri2 = self.triangles[iTri2] # find the opposite vertices, not part of the edge iOpposite1 = -1 iOpposite2 = -1 for i in range(3): if not tri1[i] in edge: iOpposite1 = tri1[i] if not tri2[i] in edge: iOpposite2 = tri2[i] # compute the 2 angles at the opposite vertices da1 = self.points[edge[0]] - self.points[iOpposite1] db1 = self.points[edge[1]] - self.points[iOpposite1] da2 = self.points[edge[0]] - self.points[iOpposite2] db2 = self.points[edge[1]] - self.points[iOpposite2] crossProd1 = self.getArea(iOpposite1, edge[0], edge[1]) crossProd2 = self.getArea(iOpposite2, edge[1], edge[0]) dotProd1 = numpy.dot(da1, db1) dotProd2 = numpy.dot(da2, db2) angle1 = abs(math.atan2(crossProd1, dotProd1)) angle2 = abs(math.atan2(crossProd2, dotProd2)) # Delaunay's test if angle1 + angle2 > math.pi*(1.0 + self.EPS): # flip the triangles # / ^ \ / b \ # iOpposite1 + a|b + iOpposite2 => + - > + # \ / \ a / newTri1 = [iOpposite1, edge[0], iOpposite2] # triangle a newTri2 = [iOpposite1, iOpposite2, edge[1]] # triangle b # update the triangle data structure self.triangles[iTri1] = newTri1 self.triangles[iTri2] = newTri2 # now handle the topolgy of the edges # remove this edge del self.edge2Triangles[edge] # add new edge e = self.makeKey(iOpposite1, iOpposite2) self.edge2Triangles[e] = [iTri1, iTri2] # modify two edge entries which now connect to # a different triangle e = self.makeKey(iOpposite1, edge[1]) v = self.edge2Triangles[e] for i in range(len(v)): if v[i] == iTri1: v[i] = iTri2 res.add(e) e = self.makeKey(iOpposite2, edge[0]) v = self.edge2Triangles[e] for i in range(len(v)): if v[i] == iTri2: v[i] = iTri1 res.add(e) # these two edges might need to be flipped at the # next iteration res.add(self.makeKey(iOpposite1, edge[0])) res.add(self.makeKey(iOpposite2, edge[1])) return res def flipEdges(self): """ Flip edges to statisfy Delaunay's criterion """ # start with all the edges edgeSet = set(self.edge2Triangles.keys()) continueFlipping = True while continueFlipping: # # iterate until there are no more edges to flip # # collect the edges to flip newEdgeSet = set() for edge in edgeSet: # union newEdgeSet |= self.flipOneEdge(edge) edgeSet = copy.copy(newEdgeSet) continueFlipping = (len(edgeSet) > 0) def addPoint(self, ip): """ Add point @param ip point index """ # collection for later updates boundaryEdgesToRemove = set() boundaryEdgesToAdd = set() for edge in self.boundaryEdges: if self.isEdgeVisible(ip, edge): # create new triangle newTri = [edge[0], edge[1], ip] newTri.sort() self.makeCounterClockwise(newTri) self.triangles.append(newTri) # update the edge to triangle map e = list(edge[:]) e.sort() iTri = len(self.triangles) - 1 self.edge2Triangles[tuple(e)].append(iTri) # add the two boundary edges e1 = [ip, edge[0]] e1.sort() e1 = tuple(e1) e2 = [edge[1], ip] e2.sort() e2 = tuple(e2) v1 = self.edge2Triangles.get(e1, []) v1.append(iTri) v2 = self.edge2Triangles.get(e2, []) v2.append(iTri) self.edge2Triangles[e1] = v1 self.edge2Triangles[e2] = v2 # keep track of the boundary edges to update boundaryEdgesToRemove.add(edge) boundaryEdgesToAdd.add( (edge[0], ip) ) boundaryEdgesToAdd.add( (ip, edge[1]) ) # update the boundary edges for bedge in boundaryEdgesToRemove: self.boundaryEdges.remove(bedge) for bedge in boundaryEdgesToAdd: bEdgeSorted = list(bedge) bEdgeSorted.sort() bEdgeSorted = tuple(bEdgeSorted) if len(self.edge2Triangles[bEdgeSorted]) == 1: # only add boundary edge if it does not appear # twice in different order self.boundaryEdges.add(bedge) # recursively flip edges flipped = True while flipped: flipped = self.flipEdges() def makeKey(self, i1, i2): """ Make a tuple key such at i1 < i2 """ if i1 < i2: return (i1, i2) return (i2, i1)