diff --git a/main.js b/main.js index 3add6200..bf2adc37 100644 --- a/main.js +++ b/main.js @@ -12,7 +12,7 @@ document.title += " v" + version; // Switches to disable/enable logging features const INFO = 0; -const TIME = 0; +const TIME = 1; const WARN = 1; const ERROR = 1; @@ -610,7 +610,7 @@ function calculateVoronoi(graph, points) { TIME && console.timeEnd("calculateDelaunay"); TIME && console.time("calculateVoronoi"); - const voronoi = Voronoi(delaunay, allPoints, n); + const voronoi = new Voronoi(delaunay, allPoints, n); graph.cells = voronoi.cells; graph.cells.i = n < 65535 ? Uint16Array.from(d3.range(n)) : Uint32Array.from(d3.range(n)); // array of indexes graph.vertices = voronoi.vertices; diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index bbb48e4f..924280e3 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -394,7 +394,7 @@ const hull = getHull(start, s.i, s.cells / 10); const points = [...hull].map(v => pack.vertices.p[v]); const delaunay = Delaunator.from(points); - const voronoi = Voronoi(delaunay, points, points.length); + const voronoi = new Voronoi(delaunay, points, points.length); const chain = connectCenters(voronoi.vertices, s.pole[1]); const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length); paths.push([s.i, relaxed]); diff --git a/modules/voronoi.js b/modules/voronoi.js index f7b82292..7889b91e 100644 --- a/modules/voronoi.js +++ b/modules/voronoi.js @@ -1,82 +1,136 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.Voronoi = factory()); -}(this, (function () { 'use strict'; +class Voronoi { + /** + * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm} + * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles. + * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance. + * @param {[number, number][]} points A list of coordinates. + * @param {number} pointsN The number of points. + */ + constructor(delaunay, points, pointsN) { + this.delaunay = delaunay; + this.points = points; + this.pointsN = pointsN; + this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell + this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells - var Voronoi = function Voronoi(delaunay, points, pointsN) { - const cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell - const vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells - for (let e=0; e < delaunay.triangles.length; e++) { + // Half-edges are the indices into the delaunator outputs: + // delaunay.triangles[e] gives the point ID where the half-edge starts + // delaunay.triangles[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle. + for (let e = 0; e < this.delaunay.triangles.length; e++) { - const p = delaunay.triangles[nextHalfedge(e)]; - if (p < pointsN && !cells.c[p]) { - const edges = edgesAroundPoint(e); - cells.v[p] = edges.map(e => triangleOfEdge(e)); // cell: adjacent vertex - cells.c[p] = edges.map(e => delaunay.triangles[e]).filter(c => c < pointsN); // cell: adjacent valid cells - cells.b[p] = edges.length > cells.c[p].length ? 1 : 0; // cell: is border + const p = this.delaunay.triangles[this.nextHalfedge(e)]; + if (p < this.pointsN && !this.cells.c[p]) { + const edges = this.edgesAroundPoint(e); + this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex + this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells + this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border } - const t = triangleOfEdge(e); - if (!vertices.p[t]) { - vertices.p[t] = triangleCenter(t); // vertex: coordinates - vertices.v[t] = trianglesAdjacentToTriangle(t); // vertex: adjacent vertices - vertices.c[t] = pointsOfTriangle(t); // vertex: adjacent cells + const t = this.triangleOfEdge(e); + if (!this.vertices.p[t]) { + this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates + this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices + this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells } } - - function pointsOfTriangle(t) { - return edgesOfTriangle(t).map(e => delaunay.triangles[e]); - } - - function trianglesAdjacentToTriangle(t) { - let triangles = []; - for (let e of edgesOfTriangle(t)) { - let opposite = delaunay.halfedges[e]; - triangles.push(triangleOfEdge(opposite)); - } - return triangles; - } - - function edgesAroundPoint(start) { - let result = [], incoming = start; - do { - result.push(incoming); - const outgoing = nextHalfedge(incoming); - incoming = delaunay.halfedges[outgoing]; - } while (incoming !== -1 && incoming !== start && result.length < 20); - return result; - } - - function triangleCenter(t) { - let vertices = pointsOfTriangle(t).map(p => points[p]); - return circumcenter(vertices[0], vertices[1], vertices[2]); - } - - return {cells, vertices} - } - function edgesOfTriangle(t) {return [3*t, 3*t+1, 3*t+2];} + /** + * Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.} + * @param {number} t The index of the triangle + * @returns {[number, number, number]} The IDs of the points comprising the given triangle. + */ + pointsOfTriangle(t) { + return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]); + } - function triangleOfEdge(e) {return Math.floor(e/3);} + /** + * Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.} + * @param {number} t The index of the triangle + * @returns {number[]} The indices of the triangles that share half-edges with this triangle. + */ + trianglesAdjacentToTriangle(t) { + let triangles = []; + for (let edge of this.edgesOfTriangle(t)) { + let opposite = this.delaunay.halfedges[edge]; + triangles.push(this.triangleOfEdge(opposite)); + } + return triangles; + } - function nextHalfedge(e) {return (e % 3 === 2) ? e-2 : e+1;} + /** + * Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.} + * @param {number} start The index of an incoming half-edge that leads to the desired point + * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point. + */ + edgesAroundPoint(start) { + const result = []; + let incoming = start; + do { + result.push(incoming); + const outgoing = this.nextHalfedge(incoming); + incoming = this.delaunay.halfedges[outgoing]; + } while (incoming !== -1 && incoming !== start && result.length < 20); + return result; + } - function prevHalfedge(e) {return (e % 3 === 0) ? e+2 : e-1;} + /** + * Returns the center of the triangle located at the given index. + * @param {number} t The index of the triangle + * @returns {[number, number]} + */ + triangleCenter(t) { + let vertices = this.pointsOfTriangle(t).map(p => this.points[p]); + return this.circumcenter(vertices[0], vertices[1], vertices[2]); + } - function circumcenter(a, b, c) { - let ad = a[0]*a[0] + a[1]*a[1], - bd = b[0]*b[0] + b[1]*b[1], - cd = c[0]*c[0] + c[1]*c[1]; - let D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])); + /** + * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.} + * @param {number} t The index of the triangle + * @returns {[number, number, number]} The edges of the triangle. + */ + edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; } + + /** + * Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.} + * @param {number} e The index of the edge + * @returns {number} The index of the triangle + */ + triangleOfEdge(e) { return Math.floor(e / 3); } + + /** + * Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.} + * @param {number} e The index of the current half edge + * @returns {number} The index of the next half edge + */ + nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; } + + /** + * Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.} + * @param {number} e The index of the current half edge + * @returns {number} The index of the previous half edge + */ + prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; } + + /** + * Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia} + * @param {[number, number]} a The coordinates of the first point of the triangle + * @param {[number, number]} b The coordinates of the second point of the triangle + * @param {[number, number]} c The coordinates of the third point of the triangle + * @return {[number, number]} The coordinates of the circumcenter of the triangle. + */ + circumcenter(a, b, c) { + const [ax, ay] = a; + const [bx, by] = b; + const [cx, cy] = c; + const ad = ax * ax + ay * ay; + const bd = bx * bx + by * by; + const cd = cx * cx + cy * cy; + const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); return [ - Math.floor(1/D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1]))), - Math.floor(1/D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0]))) + Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))), + Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax))) ]; } - - return Voronoi; - -}))); \ No newline at end of file +} \ No newline at end of file