/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ import ol_source_Vector from 'ol/source/Vector.js' import ol_Feature from 'ol/Feature.js' import ol_geom_Polygon from 'ol/geom/Polygon.js' import {boundingExtent as ol_extent_boundingExtent} from 'ol/extent.js' import {buffer as ol_extent_buffer} from 'ol/extent.js' import {ol_coordinate_equal, ol_coordinate_dist2d} from '../geom/GeomUtils.js' import ol_coordinate_convexHull from '../geom/ConvexHull.js' /** Delaunay source * Calculate a delaunay triangulation from points in a source * @param {*} options extend ol/source/Vector options * @param {ol/source/Vector} options.source the source that contains the points */ var ol_source_Delaunay = class olsourceDelaunay extends ol_source_Vector { constructor(options) { options = options || {} var source = options.source delete options.source super(options) // Source this._nodes = source; // Convex hull this.hull = [] // A new node is added to the source node: calculate the new triangulation this._nodes.on('addfeature', this._onAddNode.bind(this)) // A new node is removed from the source node: calculate the new triangulation this._nodes.on('removefeature', this._onRemoveNode.bind(this)) this.set('epsilon', options.epsilon || .0001) } /** Clear source (and points) * @param {boolean} opt_fast */ clear(opt_fast) { super.clear(opt_fast) this.getNodeSource().clear(opt_fast) } /** Add a new triangle in the source * @param {Array
    } pts */ _addTriangle(pts) { pts.push(pts[0]) var triangle = new ol_Feature(new ol_geom_Polygon([pts])) this.addFeature(triangle) this.flip.push(triangle) return triangle } /** Get nodes */ getNodes() { return this._nodes.getFeatures() } /** Get nodes source */ getNodeSource() { return this._nodes } /** * A point has been removed * @param {ol/source/Vector.Event} evt */ _onRemoveNode(evt) { // console.log(evt) var pt = evt.feature.getGeometry().getCoordinates() if (!pt) return // Still there (when removing duplicated points) if (this.getNodesAt(pt).length) return // console.log('removenode', evt.feature) // Get associated triangles var triangles = this.getTrianglesAt(pt) this.flip = [] // Get hole var i var edges = [] while (triangles.length) { var tr = triangles.pop() this.removeFeature(tr) tr = tr.getGeometry().getCoordinates()[0] var pts = [] for (i = 0; i < 3; i++) { p = tr[i] if (!ol_coordinate_equal(p, pt)) { pts.push(p) } } edges.push(pts) } pts = edges.pop() /* DEBUG var se = ''; edges.forEach(function(e){ se += ' - '+this.listpt(e); }.bind(this)); console.log('EDGES', se); */ i = 0 function testEdge(p0, p1, index) { if (ol_coordinate_equal(p0, pts[index])) { if (index) pts.push(p1) else pts.unshift(p1) return true } return false } while (true) { var e = edges[i] if (testEdge(e[0], e[1], 0) || testEdge(e[1], e[0], 0) || testEdge(e[0], e[1], pts.length - 1) || testEdge(e[1], e[0], pts.length - 1)) { edges.splice(i, 1) i = 0 } else { i++ } if (!edges.length) break if (i >= edges.length) { // console.log(this.listpt(pts), this.listpt(edges)); throw '[DELAUNAY:removePoint] No edge found' } } // Closed = interior // console.log('PTS', this.listpt(pts)) var closed = ol_coordinate_equal(pts[0], pts[pts.length - 1]) if (closed) pts.pop() // Update convex hull: remove pt + add new ones var p for (i; p = this.hull[i]; i++) { if (ol_coordinate_equal(pt, p)) { this.hull.splice(i, 1) break } } this.hull = ol_coordinate_convexHull(this.hull.concat(pts)) // select.getFeatures().clear(); // var clockwise = function (t) { var i1, s = 0 for (var i = 0; i < t.length; i++) { i1 = (i + 1) % t.length s += (t[i1][0] - t[i][0]) * (t[i1][1] + t[i][1]) } // console.log(s) return (s >= 0 ? 1 : -1) } // Add ears // interior point : ear area and object area have the same sign // extrior point : add a new point and close var clock var enveloppe = pts.slice() if (closed) { clock = clockwise(pts) } else { // console.log('ouvert', pts, pts.slice().push(pt)) enveloppe.push(pt) clock = clockwise(enveloppe) } // console.log('S=',clock,'CLOSED',closed) // console.log('E=',this.listpt(enveloppe)) for (i = 0; i <= pts.length + 1; i++) { if (pts.length < 3) break var t = [ pts[i % pts.length], pts[(i + 1) % pts.length], pts[(i + 2) % pts.length] ] if (clockwise(t) === clock) { var ok = true for (var k = i + 3; k < i + pts.length; k++) { // console.log('test '+k, this.listpt([pts[k % pts.length]])) if (this.inCircle(pts[k % pts.length], t)) { ok = false break } } if (ok) { // console.log(this.listpt(t),'ok'); this._addTriangle(t) // remove pts.splice((i + 1) % pts.length, 1) // and restart i = -1 } } // else console.log(this.listpt(t),'nok'); } /* DEBUG * / if (pts.length>3) console.log('oops'); console.log('LEAV',this.listpt(pts)); var ul = $('ul.triangles').html(''); $('
  1. ') .text('E:'+this.listpt(enveloppe)+' - '+clock+' - '+closed) .data('triangle', new ol_Feature(new ol_geom_Polygon([enveloppe]))) .click(function(){ var t = $(this).data('triangle'); select.getFeatures().clear(); select.getFeatures().push(t); }) .appendTo(ul); for (var i=0; i') .text(this.listpt(this.flip[i].getGeometry().getCoordinates()[0]) +' - ' + clockwise(this.flip[i].getGeometry().getCoordinates()[0])) .data('triangle', this.flip[i]) .click(function(){ var t = $(this).data('triangle'); select.getFeatures().clear(); select.getFeatures().push(t); }) .appendTo(ul); } /**/ // Flip? this.flipTriangles() } /** * A new point has been added * @param {ol/source/VectorEvent} e */ _onAddNode(e) { var finserted = e.feature var i, p // Not a point! if (finserted.getGeometry().getType() !== 'Point') { this._nodes.removeFeature(finserted) return } // Reset flip table this.flip = [] var nodes = this.getNodes() // The point var pt = finserted.getGeometry().getCoordinates() // Test existing point if (this.getNodesAt(pt).length > 1) { // console.log('remove duplicated points') this._nodes.removeFeature(finserted) return } // Triangle needs at least 3 points if (nodes.length <= 3) { if (nodes.length === 3) { var pts = [] for (i = 0; i < 3; i++) pts.push(nodes[i].getGeometry().getCoordinates()) this._addTriangle(pts) this.hull = ol_coordinate_convexHull(pts) } return } // Get the triangle var t = this.getFeaturesAtCoordinate(pt)[0] if (t) { this.removeFeature(t) t.set('del', true) var c = t.getGeometry().getCoordinates()[0] for (i = 0; i < 3; i++) { this._addTriangle([pt, c[i], c[(i + 1) % 3]]) } } else { // Calculate new convex hull var hull2 = this.hull.slice() hull2.push(pt) hull2 = ol_coordinate_convexHull(hull2) // Search for points for (i = 0; p = hull2[i]; i++) { if (ol_coordinate_equal(p, pt)) break } i = (i !== 0 ? i - 1 : hull2.length - 1) var p0 = hull2[i] var stop = hull2[(i + 2) % hull2.length] for (i = 0; p = this.hull[i]; i++) { if (ol_coordinate_equal(p, p0)) break } // Connect to the hull while (true) { // DEBUG: prevent infinit loop if (i > 1000) { console.error('[DELAUNAY:addPoint] Too many iterations') break } i++ p = this.hull[i % this.hull.length] this._addTriangle([pt, p, p0]) p0 = p if (p[0] === stop[0] && p[1] === stop[1]) break } this.hull = hull2 } this.flipTriangles() } /** Flipping algorithme: test new inserted triangle and flip */ flipTriangles() { var count = 1000 // Count to prevent too many iterations var pi while (this.flip.length) { // DEBUG: prevent infinite loop if (count-- < 0) { console.error('[DELAUNAY:flipTriangles] Too many iterations') break } var tri = this.flip.pop() if (tri.get('del')) continue var ti = tri.getGeometry().getCoordinates()[0] for (var k = 0; k < 3; k++) { // Get facing triangles var mid = [(ti[(k + 1) % 3][0] + ti[k][0]) / 2, (ti[(k + 1) % 3][1] + ti[k][1]) / 2] var triangles = this.getTrianglesAt(mid) var pt1 = null // Get opposite point if (triangles.length > 1) { var t0 = triangles[0].getGeometry().getCoordinates()[0] var t1 = triangles[1].getGeometry().getCoordinates()[0] for (pi = 0; pi < t1.length; pi++) { if (!this._ptInTriangle(t1[pi], t0)) { pt1 = t1[pi] break } } } if (pt1) { // Is in circle ? if (this.inCircle(pt1, t0)) { var pt2 // Get opposite point for (pi = 0; pi < t0.length; pi++) { if (!this._ptInTriangle(t0[pi], t1)) { pt2 = t0.splice(pi, 1)[0] break } } // Flip triangles if (this.intersectSegs([pt1, pt2], t0)) { while (triangles.length) { var tmp = triangles.pop() tmp.set('del', true) this.removeFeature(tmp) } this._addTriangle([pt1, pt2, t0[0]]) this._addTriangle([pt1, pt2, t0[1]]) } } } } } } /** Test intersection beetween 2 segs * @param {Array} d1 * @param {Array} d2 * @return {bbolean} */ intersectSegs(d1, d2) { var d1x = d1[1][0] - d1[0][0] var d1y = d1[1][1] - d1[0][1] var d2x = d2[1][0] - d2[0][0] var d2y = d2[1][1] - d2[0][1] var det = d1x * d2y - d1y * d2x if (det != 0) { var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det // Intersection: return [d2[0][0] + k*d2x, d2[0][1] + k*d2y]; return (0 < k && k < 1) } else return false } /** Test pt is a triangle's node * @param {ol.coordinate} pt * @param {Array} triangle * @return {boolean} */ _ptInTriangle(pt, triangle) { for (var i = 0, p; p = triangle[i]; i++) { if (ol_coordinate_equal(pt, p)) return true } return false } /** List points in a triangle (assume points get an id) for debug purposes * @param {Array} pts * @return {String} ids list */ listpt(pts) { var s = '' for (var i = 0, p; p = pts[i]; i++) { var c = this._nodes.getClosestFeatureToCoordinate(p) if (!ol_coordinate_equal(c.getGeometry().getCoordinates(), p)) c = null s += (s ? ', ' : '') + (c ? c.get('id') : '?') } return s } /** Test if coord is within triangle's circumcircle * @param {ol.coordinate} coord * @param {Array} triangle * @return {boolean} */ inCircle(coord, triangle) { var c = this.getCircumCircle(triangle) return ol_coordinate_dist2d(coord, c.center) < c.radius } /** Calculate the circumcircle of a triangle * @param {Array} triangle * @return {*} */ getCircumCircle(triangle) { var x1 = triangle[0][0] var y1 = triangle[0][1] var x2 = triangle[1][0] var y2 = triangle[1][1] var x3 = triangle[2][0] var y3 = triangle[2][1] var m1 = (x1 - x2) / (y2 - y1) var m2 = (x1 - x3) / (y3 - y1) var b1 = ((y1 + y2) / 2) - m1 * (x1 + x2) / 2 var b2 = ((y1 + y3) / 2) - m2 * (x1 + x3) / 2 var cx = (b2 - b1) / (m1 - m2) var cy = m1 * cx + b1 var center = [cx, cy] return { center: center, radius: ol_coordinate_dist2d(center, triangle[0]) } } /** Get triangles at a point */ getTrianglesAt(coord) { var extent = ol_extent_buffer(ol_extent_boundingExtent([coord]), this.get('epsilon')) var result = [] this.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f) }) return result } /** Get nodes at a point */ getNodesAt(coord) { var extent = ol_extent_buffer(ol_extent_boundingExtent([coord]), this.get('epsilon')) return this._nodes.getFeaturesInExtent(extent) } /** Get Voronoi * @param {boolean} border include border, default false * @return { Array< ol.geom.Polygon > } */ calculateVoronoi(border) { var voronoi = [] this.getNodes().forEach(function (f) { var pt = f.getGeometry().getCoordinates() var isborder = false if (border !== true) { for (var i = 0; i < this.hull.length; i++) { if (ol_coordinate_equal(pt, this.hull[i])) { isborder = true break } } } if (!isborder) { var tr = this.getTrianglesAt(pt) var pts = [] tr.forEach(function (triangle) { var c = this.getCircumCircle(triangle.getGeometry().getCoordinates()[0]) pts.push({ pt: c.center, d: Math.atan2(c.center[1] - pt[1], c.center[0] - pt[0]) }) }.bind(this)) pts.sort(function (a, b) { return a.d - b.d }) var poly = [] pts.forEach(function (p) { poly.push(p.pt) }) poly.push(poly[0]) var prop = f.getProperties() prop.geometry = new ol_geom_Polygon([poly]) voronoi.push(new ol_Feature(prop)) } }.bind(this)) return voronoi } } export default ol_source_Delaunay