def visibility_walk(self, ini, p): """Walk from triangle ini to triangle containing p Note, because this walk can cycle for a non-Delaunay triangulation we pick a random edge to continue the walk (this is a remembering stochastic walk, see RR-4120.pdf, Technical report from HAL-Inria by Olivier Devillers, Sylvain Pion, Monique Teillaud. Walking in a triangulation, https://hal.inria.fr/inria-00072509) For speed we do not check if we stay inside the bounding box that was used when initializing the triangulation, so make sure that a point given fits inside this box! """ t = ini previous = None if t.vertices[2] is None: t = t.neighbours[2] n = len(self.triangulation.triangles) for ct in xrange(n): # get random side to continue walk, this way the walk cannot get # stuck by always picking triangles in the same order # (and get stuck in a cycle in case of non-Delaunay triangulation) e = randint(0, 2) if t.neighbours[e] is not previous and \ orient2d(t.vertices[ccw(e)], t.vertices[ccw(e+1)], p) < 0: previous = t t = t.neighbours[e] continue e = ccw(e + 1) if t.neighbours[e] is not previous and \ orient2d(t.vertices[ccw(e)], t.vertices[ccw(e+1)], p) < 0: previous = t t = t.neighbours[e] continue e = ccw(e + 1) if t.neighbours[e] is not previous and \ orient2d(t.vertices[ccw(e)], t.vertices[ccw(e+1)], p) < 0: previous = t t = t.neighbours[e] continue self.visits += ct return t self.visits += ct return t
def _insert_vertex(self, u, v, w): """Insert a vertex to the triangulated area, while keeping the area of the current polygon triangulated """ x = -1 # Find third vertex in the triangle that has edge (w, v) if (w, v) in self.adjacency: x = self.adjacency[(w, v)] # See if we have to remove some triangle(s) already there, # or that we can add just a new one if x != -1 and \ (orient2d(self.vertices[u], self.vertices[v], self.vertices[w]) <= 0 or incircle(self.vertices[u], self.vertices[v], self.vertices[w], self.vertices[x]) > 0): # Remove triangle (w,v,x), also from adjacency dict self.triangles.remove(permute(w, v, x)) del self.adjacency[(w, v)] del self.adjacency[(v, x)] del self.adjacency[(x, w)] # Recurse self._insert_vertex(u, v, x) self._insert_vertex(u, x, w) else: # Add a triangle (this triangle could be removed later) self._add_triangle(u, v, w)
def triangle_overlaps_ray(vertex, towards): """Returns the triangle that overlaps the ray. In case there are multiple candidates, then the triangle with the right leg overlapping the ray is returned. It's a ValueError if no or multiple candidates are found. """ candidates = [] for edge in StarEdgeIterator(vertex): # if edge.isFinite: start, end = edge.segment # start: turns ccw # end: turns cw ostart = orient2d(start, towards, vertex) oend = orient2d(end, towards, vertex) if ostart >= 0 and oend <= 0: candidates.append((edge, ostart, oend)) # the default, exactly one candidate if len(candidates) == 1: return candidates[0][0] # no candidates found, # this would be the case if towards lies outside # currently triangulated convex hull elif len(candidates) == 0: raise ValueError( "No overlap found (towards outside triangulated convex hull?)") # the ray overlaps the legs of multiple triangles # only return the triangle for which the right leg overlaps with the ray # it is an error if there is not exactly one candidate that we can return else: ostartct = 0 candidateIdx = None for i, (edge, ostart, oend) in enumerate(candidates): if ostart == 0: ostartct += 1 candidateIdx = i if ostartct != 1 or candidateIdx is None: for i, (edge, ostart, oend) in enumerate(candidates): print((ostart, oend)) raise ValueError("Incorrect number of triangles found") return candidates[candidateIdx][0]
def straight_walk(self, ini, p): """Walk straight from triangle ini to triangle containing p""" tri = ini q_idx, r_idx, l_idx = 0, 1, 2 # initialize the walk by rotating ccw or cw q = tri.vertices[q_idx] if orient2d(tri.vertices[r_idx], tri.vertices[q_idx], p) < 0: while orient2d(tri.vertices[l_idx], tri.vertices[q_idx], p) < 0: neighbour = tri.neighbours[r_idx] q_idx = neighbour.vertices.index(q) r_idx = ccw(q_idx) l_idx = ccw(r_idx) tri = neighbour assert tri.vertices[q_idx] == q else: while True: neighbour = tri.neighbours[l_idx] q_idx = neighbour.vertices.index(q) r_idx = ccw(q_idx) l_idx = ccw(r_idx) tri = neighbour assert neighbour.vertices[q_idx] == q if orient2d(tri.vertices[r_idx], tri.vertices[q_idx], p) < 0: break # perform the walk s_idx = q_idx while orient2d(p, tri.vertices[r_idx], tri.vertices[l_idx]) < 0.0: neighbour = tri.neighbours[s_idx] l_idx = neighbour.vertices.index(tri.vertices[l_idx]) r_idx = ccw(l_idx) s_idx = ccw(r_idx) tri = neighbour # 'advance' 1 of the two sides of the neighbour triangle # by swapping either l or r with s if orient2d(tri.vertices[s_idx], q, p) < 0.0: s_idx, r_idx = r_idx, s_idx else: s_idx, l_idx = l_idx, s_idx return tri
def _preprocess(self, cavity_edges): """Set up data structures needed for the re-triangulate part of the algorithm. """ self.constraints = set() for i, edge in enumerate(cavity_edges): xx, yy = edge.segment # Both directions are needed, as this is used # for internal dangling edges inside the cavity, # which are traversed both sides. self.constraints.add((id(xx), id(yy))) self.constraints.add((id(yy), id(xx))) if i: self.vertices.append(yy) else: self.vertices.extend([xx, yy]) # Make the vertices list COUNTERCLOCKWISE here # The algorithm depends on this orientation! self.vertices.reverse() self.surroundings = {} for i, edge in enumerate(cavity_edges): s = edge.segment self.surroundings[id(s[0]), id(s[1])] = edge # Make a "linked list" of polygon vertices self.next = {} self.prev = {} # Relative size of distances to the segment self.distance = {} # Adjacency: third point of a triangle by given oriented side self.adjacency = {} # Set of resulting triangles (vertex indices) self.triangles = set() # Initialization for the algorithm m = len(self.vertices) # Make random permutation of point indices self.pi = list(range(1, m - 1)) # Randomize processing order shuffle(self.pi) # Link all vertices in a circular list that # describes the polygon outline of the cavity for i in range(m): self.next[i] = (i + 1) % m self.prev[i] = (i - 1) % m # Distance to the segment from [0-m] self.distance[i] = orient2d(self.vertices[0], self.vertices[i], self.vertices[m-1])
def mark_cavity(P, Q, triangles): """Returns two lists: Edges above and below the list of triangles. These lists are sorted clockwise around the triangles (this is needed for CavityCDT). """ # From a list of triangles make two lists of edges: # above and below... # It is made sure that the edges that are put # here are forming a polyline # that runs *clockwise* around the cavity # - to output the cavity: # with open('/tmp/cavity.wkt', 'w') as fh: # output_triangles(triangles, fh) assert len(triangles) != 0 above = [] below = [] if len(triangles) == 1: t = triangles[0] pidx = t.vertices.index(P) lidx = (pidx + 1) % 3 ridx = (pidx + 2) % 3 lv = t.vertices[lidx] # r = t.vertices[ridx] # print "p", P # print "q", Q # print "lv", lv # print "r", r assert lv is Q # if lv is Q: # print "L IS Q" below = [] for i in (ridx,): n = t.neighbours[i] b = Edge(n, n.neighbours.index(t)) # b = Edge(n, common(t, i, n)) below.append(b) above = [] for i in (lidx, pidx,): n = t.neighbours[i] # b = Edge(n, common(t, i, n)) b = Edge(n, n.neighbours.index(t)) above.append(b) # below = [Edge(t.getNeighbour(pidx), t.getOppositeSide(pidx))] # above = [Edge(t.getNeighbour(ridx), t.getOppositeSide(ridx)), # Edge(t.getNeighbour(lidx), t.getOppositeSide(lidx))] # elif r is Q: # print "R IS Q" # below = [] # for i in (pidx, lidx,): # n = t.neighbours[i] # b = Edge(n, common(t, i, n)) # below.append(b) # above = [] # for i in (ridx,): # n = t.neighbours[i] # b = Edge(n, common(t, i, n)) # above.append(b) # above = [Edge(t.getNeighbour(ridx), t.getOppositeSide(ridx))] # below = [Edge(t.getNeighbour(lidx), t.getOppositeSide(lidx)), # Edge(t.getNeighbour(pidx), t.getOppositeSide(pidx)) # else: # raise ValueError("Combinations exhausted") else: # precondition here is that triangles their legs # do NOT overlap with the segment that goes # from P -> Q # thus: left and right orientation cannot both be 0 # -> this is an error for t in triangles: for side in range(3): # FIXME: # skip neighbouring side if it is none? # if t.neighbours[side] is None: # continue # FIXME: Do we add None into above/below??? # if that is the case, we should still know which 2 infinite # vertices are between here... edge = Edge(t, side) R, L = edge.segment left = orient2d(L, Q, P) right = orient2d(R, Q, P) # in case both are 0 ... not allowed if (left == 0 and right == 0): raise ValueError("Overlapping triangle leg found," " not allowed") n = t.neighbours[side] e = Edge(n, n.neighbours.index(t)) if left >= 0 and right >= 0: below.append(e) elif right <= 0 and left <= 0: above.append(e) below.reverse() return above, below
def _push_back_triangles(self): """Make new triangles that are inserted in the data structure and that are linked up properly with each other and the surroundings. """ # First make new triangles newtris = {} for three in self.triangles: a, b, c, = three # Index triangle by temporary sorted list of vertex indices # # By indexing triangles this way, we T = Triangle(self.vertices[a], self.vertices[b], self.vertices[c]) newtris[permute(id(T.vertices[0]), id( T.vertices[1]), id(T.vertices[2]))] = T for x in newtris.values(): assert orient2d(x.vertices[0], x.vertices[1], x.vertices[2]) > 0 # Translate adjacency table to new indices of triangles # Note that vertices that are used twice (because of dangling edge # in the cavity) will get the same identifier again # (while previously they would have different id's). adj = {} for (f, t), v in self.adjacency.items(): adj[id(self.vertices[f]), id(self.vertices[t])] = id( self.vertices[v]) # Link all the 3 sides of the new triangles properly for T in newtris.values(): for i in range(3): segment = Edge(T, i).segment side = (id(segment[1]), id(segment[0])) constrained = False # The side is adjacent to another new triangle # The neighbouring triangle at this side will be linked later # In case this is a dangling segment we constrain the segment if side in adj: neighbour = newtris[permute(side[0], side[1], adj[side])] if side in self.constraints: constrained = True # the side is adjacent to an exterior triangle # that lies outside the cavity and will # remain after the re-dt # therefore also change the neighbour of this triangle elif side in self.surroundings: neighbour_side = self.surroundings[side].side neighbour = self.surroundings[side].triangle neighbour.neighbours[neighbour_side] = T # MM fixed # getEdgeType(neighbour_side) constrained = neighbour.constrained[neighbour_side] # the triangle is the bottom of the evacuated cavity # hence it should be linked later to the other # re-dt of the cavity else: if self.edge: print((self.edge.triangle, self.edge.triangle.all_infinite, self.edge.triangle.__str__())) assert self.edge is None neighbour = None self.edge = Edge(T, i) T.neighbours[i] = neighbour # setNeighbour(i, neighbour) # T.setEdgeType(i, constrained) T.constrained[i] = constrained # Append the new triangles to the triangle list of the # dt self.dt.triangles.append(T) assert self.edge is not None
def straight_walk(P, Q): """Obtain the list of triangles that overlap the line segment that goes from Vertex P to Q. Note that P and Q must be Vertex objects that are in the Triangulation already. Raises a ValueError when either a Constrained edge is crossed in the interior of the line segment or when another Vertex lies on the segment. """ edge = triangle_overlaps_ray(P, Q) t = edge.triangle side = edge.side R, L = edge.segment out = [t] if Q in t.vertices: # we do not need to go into walking mode if we found # the exact triangle with the end point already return out # perform walk # pos = t.vertices.index(R) # from end via right to left makes right turn (negative) # if line is collinear with end point then orientation becomes 0 # FIXME: # The way that we now stop the rotation around the vertex # does that make a problem here --> we can get either the lower # or the upper triangle, this depends on the arbitrary start triangle while orient2d(Q, R, L) < 0.: # check if we do not prematurely have a orientation of 0 # at either side, which means that we collide a vertex if (L is not Q and orient2d(L, P, Q) == 0) or \ (R is not Q and orient2d(R, P, Q) == 0): raise ValueError("Unwanted vertex collision detected - inserting: {} -> {} | crossing: {} -> {}". format(P, Q, R, L)) # based on the position of R take next triangle # FIXME: # TEST THIS: check here if taking the neighbour does not take # place over a constrained side of the triangle --> # raise ValueError("Unwanted constrained segment collision detected") # if triangle.getEdgeType(side): # raise TopologyViolationError("Unwanted" # " constrained segment collision detected") if t.constrained[side]: raise ValueError("Unwanted constrained segment collision detected - inserting: {} -> {} | crossing: {} -> {}". format(P, Q, R, L)) t = t.neighbours[side] out.append(t) side = t.vertices.index(R) S = t.vertices[ccw(side)] ori = orient2d(S, Q, P) # if ori < 0: L = S side = ccw(side+1) else: R = S # check if we do not prematurely have a orientation of 0 # at either side, which means that we collide a vertex if (L is not Q and orient2d(L, P, Q) == 0) or \ (R is not Q and orient2d(R, P, Q) == 0): raise ValueError("Unwanted vertex collision detected - inserting: {} -> {} | crossing: {} -> {}". format(P, Q, R, L)) return out
def is_ccw(self): return orient2d(self.vertices[0], self.vertices[1], self.vertices[2]) > 0.
def remove(self, pt, ini): """Remove a point that is present in the triangulation, while keeping the triangulation Delaunay. The algorithm followed is from the paper by Mostafavi, Gold & Dakowicz (2003). @article{Mostafavi2003, doi = {10.1016/s0098-3004(03)00017-7}, url = {https://doi.org/10.1016/s0098-3004(03)00017-7}, year = {2003}, month = may, publisher = {Elsevier {BV}}, volume = {29}, number = {4}, pages = {523--530}, author = {Mir Abolfazl Mostafavi and Christopher Gold and Maciej Dakowicz}, title = {Delete and insert operations in Voronoi/Delaunay methods and applications}, journal = {Computers {\&} Geosciences} } Note 1: the removal does happily remove points if a CDT is used (not ok). Note 2: infinite vertex removal should not happen """ ### FIXME: This removal does not care about presence of constraints in the DT! ### Maybe we should make the Triangulation structure aware of this ### (either by subclassing or by having a bool attribute) tri = self._walk(ini, Vertex(pt[0], pt[1])) pivot = None for v in tri.vertices: if (v[0] == pt[0] and v[1] == pt[1]): pivot = v break del v if pivot is None: raise ValueError( "{} not found in triangulation, are you sure it was inserted?". format(pt)) # -- slice ears for the polygon around the pivot that is to be removed # the polygon around the pivot to be removed is represented by a # collection of triangles, which we call the *star* # the vertices on this polygon are called the *link* star = [edge.triangle for edge in StarEdgeIterator(pivot)] cur = 0 while len(star) > 3: # take 2 triangles (going around ccw around the pivot) tri0 = star[(cur) % len(star)] tri1 = star[(cur + 1) % len(star)] # get the vertices opposite of the pivot # tri0 side0 = tri0.vertices.index(pivot) v1 = tri0.vertices[orig(side0)] v2 = tri0.vertices[dest(side0)] # tri1 side1 = tri1.vertices.index(pivot) v_ = tri1.vertices[orig(side1)] v3 = tri1.vertices[dest(side1)] # they should share 1 vertex assert v2 is v_ #print(" ear to check --> {}".format( # " | ".join(map(lambda v: "{:8.2f} {:8.2f}".format(v.x, v.y), # [v1, v2, v3])))) tri2 = star[(cur + 2) % len(star)] # we have a potential ear to slice off # if (v1, v2, v3 turns left) and (v1, v3, pivot turns left or is straight) if orient2d(v1, v2, v3) > 0 and orient2d(v1, v3, pivot) >= 0: # make sure we really can slice the ear off slice_ear = True # check for other triangles (except tri0, tri1, tri2) its orig()-point # (i.e. circumcircle through ear its points is empty of other points in the link) for i in range(0, len(star) - 3): tri3 = star[(cur + 3 + i) % len(star)] assert tri3 is not tri0 assert tri3 is not tri1 assert tri3 is not tri2 v4 = tri3.vertices[orig(tri3.vertices.index(pivot))] if incircle(v1, v2, v3, v4) > 0: # v4 inside circle, do not slice slice_ear = False break if slice_ear: # -- Ear will be sliced, # flipping 2 triangles reduces the star by one triangle # # flip22 flips CCW # -> tri0 is thus the sliced ear, # so remove tri0 from the star and keep only tri1 in the remaining star self.flip22(tri0, tri0.vertices.index(v1), tri1, tri1.vertices.index(v3)) star.remove(tri0) # pivot should not be in tri0 its vertices assert pivot not in tri0.vertices # yet it should appear in tri1 assert pivot in tri1.vertices # if raw_input('#! write [y/N] $ ') in ('y', 'Y'): # with open("/tmp/all_tris.wkt", "w") as fh: # print(len(self.triangulation.triangles)) # output_triangles(self.triangulation.triangles, fh) # with open("/tmp/all_vertices.wkt", "w") as fh: # output_vertices(self.triangulation.vertices, fh) # increment cur += 1 # and -- possibly -- wrap around to start of list (make cur = 0) cur %= len(star) # with open("/tmp/tri__all_tris__before_flip31.wkt", "w") as fh: # output_triangles(self.triangulation.triangles, fh) # with open("/tmp/tri__all_vertices__before_flip31.wkt", "w") as fh: # output_vertices(self.triangulation.vertices, fh) # print(id(pivot.triangle), "to be removed") assert len(star) == 3 # -- now remove the 3 triangles by performing a flip3->1 self.flip31(pivot)