37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | polygonsJS
2 | ==========
3 |
4 |
5 | A mock Bentley-Ottmann for training: http://nraynaud.github.io/polygonsJS/index.html
6 |
7 |
8 |
9 | Beginning of a Medial Axis Transform for simple polygons here: http://nraynaud.github.io/polygonsJS/test_medial_axis.html
10 |
11 | There are still 2 O(n^2) parts: point in polygon test and closest distance test filter, both to be solved with a KD tree
--------------------------------------------------------------------------------
/Skeleton.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nraynaud/polygonsJS/379e73d9b421b7454e548f32e1d3ee14a127cae4/Skeleton.pdf
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple Bentley Ottmann with mock underlying structures
6 |
7 |
22 |
23 |
24 | The source code is there: https://github.com/nraynaud/polygonsJS/
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/kdtree.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class KDtree {
4 | constructor(points) {
5 | this.tree = this.buildTree(points, true)
6 | }
7 |
8 | xComparison = (p1, p2) => p1.x - p2.x
9 | yComparison = (p1, p2) => p1.y - p2.y
10 |
11 | buildTree(points, useXAxis) {
12 | if (points.length === 0)
13 | return null;
14 | const sorted = [...points];
15 | sorted.sort(useXAxis ? this.xComparison : this.yComparison)
16 | let pivotIndex = Math.floor(sorted.length / 2);
17 | const point = sorted[pivotIndex]
18 | return {
19 | point,
20 | left: this.buildTree(sorted.slice(0, pivotIndex), !useXAxis),
21 | right: this.buildTree(sorted.slice(pivotIndex + 1, sorted.length), !useXAxis),
22 | axis: useXAxis ? 'x' : 'y'
23 | }
24 | }
25 |
26 | findMinimum(axis) {
27 | return this.findMinimumNodeInTree(this.tree, axis).point
28 | }
29 |
30 | findMinimumNodeInTree(node, axis) {
31 | if (node.axis === axis) {
32 | return node.left ? this.findMinimumNodeInTree(node.left, axis) : node;
33 | }
34 | let left = node.left ? this.findMinimumNodeInTree(node.left, axis) : null
35 | let right = node.right ? this.findMinimumNodeInTree(node.right, axis) : null
36 | const nodes = [left, right, node]
37 | nodes.sort((p1, p2) => {
38 | if (p1 == null)
39 | return 1
40 | if (p2 == null)
41 | return -1
42 | return p1.point[axis] - p2.point[axis]
43 | })
44 | return nodes[0]
45 | }
46 |
47 | findNearestNeighbor(point) {
48 | return this.findNNInTree(point, this.tree)
49 | }
50 |
51 | findNNInTree(point, node) {
52 | if (node == null)
53 | return null
54 | const closestChoice = (p1, p2) => {
55 | if (p1 == null)
56 | return p2
57 | if (p2 == null)
58 | return p1
59 | return sqSegLength([point, p1]) < sqSegLength([point, p2]) ? p1 : p2
60 | }
61 | const [pointValue, refValue] = [point[node.axis], node.point[node.axis]]
62 | const children = [node.left, node.right]
63 | if (pointValue >= refValue)
64 | children.reverse();
65 | let best = closestChoice(this.findNNInTree(point, children[0]), node.point)
66 | if (sqSegLength([point, best]) > (point[node.axis] - node.point[node.axis]) ** 2) {
67 | best = closestChoice(this.findNNInTree(point, children[1]), best)
68 | }
69 | return best
70 | }
71 |
72 | deletePoint(point) {
73 | this.tree = this.deleteNode(point, this.tree)
74 | }
75 |
76 | deleteNode(point, node) {
77 | // https://www.cs.cmu.edu/~ckingsf/bioinfo-lectures/kdtrees.pdf
78 | if (node == null)
79 | throw new Error('oops, not found' + point)
80 | if (point === node.point) {
81 | if (node.right) {
82 | node.point = this.findMinimumNodeInTree(node.right, node.right.axis).point
83 | node.right = this.deleteNode(node.point, node.right)
84 | return node
85 | } else if (node.left) {
86 | // remove the minimum from the left tree, make what's remaining the new right
87 | // the minimum becomes the new self
88 | node.point = this.findMinimumNodeInTree(node.left, node.left.axis).point
89 | node.right = this.deleteNode(node.point, node.left)
90 | node.left = null
91 | } else
92 | // delete me, I was a leaf
93 | return null
94 | } else {
95 | if (point[node.axis] < node.point[node.axis])
96 | node.left = this.deleteNode(point, node.left)
97 | else
98 | node.right = this.deleteNode(point, node.right)
99 | }
100 | return node
101 | }
102 | }
--------------------------------------------------------------------------------
/medial_axis.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // source for the algorithm is here: http://maptools.home.comcast.net/~maptools/Skeleton.pdf
3 | var medialAxis = (function () {
4 | var ID_COUNTER = 0;
5 |
6 | function getId() {
7 | ID_COUNTER++;
8 | return ID_COUNTER;
9 | }
10 |
11 | function createLinkedList(content) {
12 | var val = null;
13 | for (var i = 0; i < content.length; i++) {
14 | var bucket = {val: content[i], next: null, prev: null};
15 | if (val == null) {
16 | val = bucket;
17 | bucket.next = bucket;
18 | bucket.prev = bucket;
19 | } else {
20 | bucket.next = val.next;
21 | val.next = bucket;
22 | bucket.next.prev = bucket;
23 | bucket.prev = val;
24 | val = bucket;
25 | }
26 | }
27 |
28 | return {
29 | //if handler returns true value, we break the loop
30 | iterate: function (handler) {
31 | if (val == null)
32 | return;
33 | var currentBucket = val;
34 | do {
35 | if (handler(currentBucket))
36 | break;
37 | currentBucket = currentBucket.next;
38 | } while (currentBucket !== val);
39 | },
40 | remove: function (bucket) {
41 | if (val === bucket) {
42 | if (val.next === bucket) {
43 | val = null;
44 | return;
45 | }
46 | val = val.next;
47 | }
48 | bucket.prev.next = bucket.next;
49 | bucket.next.prev = bucket.prev;
50 | },
51 | isEmpty: function () {
52 | return val == null;
53 | }
54 | }
55 | }
56 |
57 | function pointProjectsOnSegments(segments, point) {
58 | for (var j = 0; j < segments.length; j++)
59 | if (!pointProjectedOnSegment(point, segments[j]) || pointEquals(point, segments[j][0]) || pointEquals(point, segments[j][1]))
60 | return false;
61 | return true;
62 | }
63 |
64 | function reversedSegment(segment) {
65 | return [segment[1], segment[0]];
66 | }
67 |
68 | /**
69 | * @param origin
70 | * @param direction
71 | * @param firstSite
72 | * @param secondSite
73 | * @param [candidateFilter]
74 | * @constructor
75 | */
76 | function LinearRay(origin, direction, firstSite, secondSite, candidateFilter) {
77 | this.origin = origin;
78 | this.segment = [origin, direction];
79 | this.firstSite = firstSite;
80 | firstSite.rays.push(this);
81 | this.secondSite = secondSite;
82 | secondSite.backRays.push(this);
83 | this.filterPointCandidate = candidateFilter ? candidateFilter : function () {
84 | return true;
85 | };
86 | }
87 |
88 | function ParabolicRay(vertexSite, edgeSite, origin, firstSite, secondSite) {
89 | this.origin = origin;
90 | this.vertex = vertexSite;
91 | this.edge = edgeSite;
92 | this.firstSite = firstSite;
93 | firstSite.rays.push(this);
94 | this.secondSite = secondSite;
95 | secondSite.backRays.push(this);
96 | }
97 |
98 | ParabolicRay.prototype = {
99 | filterPointCandidate: function (point) {
100 | return pointProjectsOnSegments([this.edge.segment], point);
101 | }
102 | };
103 |
104 | function LineSite(segment) {
105 | this.segment = segment;
106 | this.rays = [];
107 | this.backRays = [];
108 | }
109 |
110 | LineSite.prototype = {
111 | dropEquation: function (equationCreator) {
112 | return equationCreator.addSegment(this.segment);
113 | },
114 | igniteRayWithPreviousSite: function (previousSite, origin) {
115 | return previousSite.igniteRayWithLineSite(this, origin);
116 | },
117 | igniteRayWithLineSite: function (followingLineSite, origin, edgesIntersection) {
118 | if (edgesIntersection == null) {
119 | edgesIntersection = intersectionSegments(this.segment, followingLineSite.segment, true);
120 | if (edgesIntersection == null || !isFinite(edgesIntersection.x) || !isFinite(edgesIntersection.y))
121 | edgesIntersection = origin;
122 | }
123 | var bVector = bisectorVectorFromSegments(reversedSegment(this.segment), followingLineSite.segment);
124 | var bPoint = {x: edgesIntersection.x + bVector.x, y: edgesIntersection.y + bVector.y};
125 | var segment1 = this.segment;
126 | return new LinearRay(origin, bPoint, this, followingLineSite, function (point) {
127 | return pointProjectsOnSegments([segment1], point);
128 | });
129 | },
130 | igniteRayWithReflexVertexSite: function (followingVertexSite, origin) {
131 | return igniteVertexSegment(followingVertexSite, this, origin, this, followingVertexSite);
132 | },
133 | sqDistanceFromPoint: function (point) {
134 | return distToSegmentSquared(point, this.segment);
135 | },
136 | coveredEdges: function () {
137 | return [this];
138 | }
139 | };
140 |
141 | function ReflexVertexSite(vertex, previousSite, nextSite) {
142 | this.vertex = vertex;
143 | this.previousSite = previousSite;
144 | this.nextSite = nextSite;
145 | this.rays = [];
146 | this.backRays = [];
147 | }
148 |
149 | ReflexVertexSite.prototype = {
150 | dropEquation: function (equationCreator) {
151 | return equationCreator.addVertex(this.vertex);
152 | },
153 | igniteRayWithPreviousSite: function (previousSite, origin) {
154 | return previousSite.igniteRayWithReflexVertexSite(this, origin);
155 | },
156 | igniteRayWithLineSite: function (followingLineSite, origin) {
157 | return igniteVertexSegment(this, followingLineSite, origin, this, followingLineSite);
158 | },
159 | igniteRayWithReflexVertexSite: function (nextSite, origin) {
160 | return new LinearRay(origin, perpendicularPoint(origin, [this.vertex, nextSite.vertex]), this, nextSite);
161 | },
162 | ignitePerpendicularRays: function () {
163 | var ray1 = this.previousSite.igniteRayWithReflexVertexSite(this);
164 | var ray2 = this.igniteRayWithLineSite(this.nextSite, this.vertex);
165 | ray1.neverIntersects = ray2;
166 | ray2.neverIntersects = ray1;
167 | return [ray1, ray2];
168 | },
169 | coveredEdges: function () {
170 | return [this.previousSite, this.nextSite];
171 | }
172 | };
173 |
174 | function igniteVertexSegment(vertexSite, lineSite, origin, firstSite, secondSite) {
175 | var segment = lineSite.segment;
176 | if (segment[1] === vertexSite.vertex || segment[0] === vertexSite.vertex)
177 | return new LinearRay(vertexSite.vertex, perpendicularPoint(vertexSite.vertex, segment), firstSite, secondSite);
178 | return new ParabolicRay(vertexSite.vertex, lineSite, origin, firstSite, secondSite);
179 | }
180 |
181 | function createSkeleton(polygon, observers, afterProcess) {
182 | observers = observers ? observers : {};
183 | var area = signedArea(polygon);
184 |
185 | function setIntersection(targetRay, intersectionPoint, nextRay) {
186 | function intersectionDistance(intersection, vertex) {
187 | if (intersection) {
188 | var len = segLength([intersection, vertex]);
189 | return len === 0 ? Infinity : len;
190 | } else
191 | return Infinity;
192 | }
193 |
194 | targetRay.ahead = intersectionDistance(intersectionPoint, targetRay.origin);
195 | nextRay.behind = intersectionDistance(intersectionPoint, nextRay.origin);
196 | nextRay.behindPoint = intersectionPoint;
197 | targetRay.aheadPoint = intersectionPoint;
198 | targetRay.nextRay = nextRay;
199 | }
200 |
201 | function fuseRay(ray) {
202 | var newRay = ray.nextRay.secondSite.igniteRayWithPreviousSite(ray.firstSite, ray.aheadPoint);
203 | newRay.children = [ray, ray.nextRay];
204 | ray.parent = newRay;
205 | ray.nextRay.parent = newRay;
206 | ray.destination = ray.aheadPoint;
207 | ray.nextRay.destination = ray.aheadPoint;
208 | return newRay;
209 | }
210 |
211 | function intersectNextRay(current, next) {
212 | if (current.nextRay === next)
213 | return;
214 | var result;
215 | var rejectedPointsProjection = [];
216 | var rejectedPoint2 = [];
217 | if (current.neverIntersects === next || next.neverIntersects === current)
218 | result = [];
219 | else {
220 | var eq = new solver.EquationSystemCreator();
221 | current.firstSite.dropEquation(eq);
222 | current.secondSite.dropEquation(eq);
223 | next.secondSite.dropEquation(eq);
224 | result = solver.solveEquations(eq, function (point) {
225 | point.id = getId();
226 | var result1 = current.filterPointCandidate(point) && next.filterPointCandidate(point);
227 | var result2 = pointInPolygon(point, polygon);
228 | var result = result1 && result2;
229 | if (!result1)
230 | rejectedPointsProjection.push(point);
231 | if (!result2)
232 | rejectedPoint2.push(point);
233 | return result;
234 | });
235 |
236 | }
237 | setIntersection(current, result[0], next);
238 | }
239 |
240 | function getCoveredSites(ray) {
241 | return ray.firstSite.coveredEdges().concat(ray.secondSite.coveredEdges());
242 | }
243 |
244 |
245 | function isReflexVertex(vertex, previousVertex, nextVertex, polygonArea) {
246 | return signedArea([previousVertex, vertex, nextVertex]) * polygonArea < 0;
247 | }
248 |
249 | var lineSites = [];
250 |
251 | function createInitialRays(polygon) {
252 | var rays = [];
253 | var reflexPoints = [];
254 | var sites = [];
255 | for (var i = 0; i < polygon.length; i++) {
256 | var previousPoint = polygon[(i + polygon.length - 1) % polygon.length];
257 | var vertex = polygon[i];
258 | if (!previousPoint.nextEdge)
259 | previousPoint.nextEdge = new LineSite([previousPoint, vertex]);
260 | if (sites.length === 0)
261 | sites.push(previousPoint.nextEdge)
262 | var nextPoint = polygon[(i + 1) % polygon.length];
263 | if (!vertex.nextEdge)
264 | vertex.nextEdge = new LineSite([vertex, nextPoint]);
265 | previousPoint.nextEdge.nextSite = vertex.nextEdge;
266 | vertex.nextEdge.previousSite = previousPoint.nextEdge;
267 | lineSites.push(vertex.nextEdge);
268 | if (isReflexVertex(vertex, previousPoint, nextPoint, area)) {
269 | vertex.reflex = true;
270 | reflexPoints.push(vertex);
271 | var reflexVertexSite = new ReflexVertexSite(vertex, previousPoint.nextEdge, vertex.nextEdge);
272 | reflexVertexSite.previousSite.nextSite = reflexVertexSite;
273 | reflexVertexSite.nextSite.previousSite = reflexVertexSite;
274 | Array.prototype.push.apply(rays, reflexVertexSite.ignitePerpendicularRays());
275 | sites.push(reflexVertexSite);
276 | } else {
277 | var ray = previousPoint.nextEdge.igniteRayWithLineSite(vertex.nextEdge, vertex, vertex);
278 | rays.push(ray);
279 | ray.medialRay = {type: 'limb', vertex: vertex, edge1: previousPoint.nextEdge, edge2: vertex.nextEdge,
280 | origin: vertex, children: []};
281 | }
282 | sites.push(vertex.nextEdge);
283 | }
284 | if (observers['initialized'])
285 | observers['initialized'](rays, reflexPoints);
286 | return {rays: rays, sites: sites};
287 | }
288 |
289 | var initialRays = createInitialRays(polygon);
290 | var rayList = createLinkedList(initialRays.rays);
291 | var siteList = createLinkedList(initialRays.sites);
292 | var root;
293 |
294 | function isBestIntersection(currentRay, nextRay, lineSites, intersectionPoint, sqRadius) {
295 | var coveredSites = getCoveredSites(currentRay).concat(getCoveredSites(nextRay));
296 | for (var i = 0; i < lineSites.length; i++) {
297 | var otherSqrDist = lineSites[i].sqDistanceFromPoint(intersectionPoint);
298 | if (otherSqrDist < sqRadius && coveredSites.indexOf(lineSites[i]) === -1) {
299 | if (observers['eliminatedRadius'])
300 | observers['eliminatedRadius'](currentRay, nextRay, intersectionPoint, sqRadius, lineSites[i], otherSqrDist);
301 | return false;
302 | }
303 | }
304 | return true;
305 | }
306 |
307 | function run(rayList, lineSites) {
308 | var stop = false;
309 | var hasMoved = false;
310 | rayList.iterate(function (currentBucket) {
311 | intersectNextRay(currentBucket.val, currentBucket.next.val);
312 | });
313 | if (observers['raysIntersectionsComputed'])
314 | observers['raysIntersectionsComputed'](rayList);
315 | var deleteList = [];
316 | rayList.iterate(function (currentBucket) {
317 | var previousRay = currentBucket.prev.val;
318 | var currentRay = currentBucket.val;
319 | var nextRay = currentBucket.next.val;
320 | if (previousRay === nextRay) {
321 | if (observers['last2raysEncountered'])
322 | observers['last2raysEncountered'](currentRay, nextRay);
323 | root = {type: 'root', children: [
324 | {type: 'spine', origin: previousRay.origin, children: [previousRay.medialRay], internalRay: previousRay},
325 | currentRay.medialRay
326 | ], origin: currentRay.origin};
327 | currentRay.destination = previousRay.origin;
328 | previousRay.destination = currentRay.origin;
329 | stop = true;
330 | return true;
331 | } else if (currentRay.aheadPoint && currentRay.behind >= currentRay.ahead && nextRay.ahead >= nextRay.behind) {
332 | var intersectionPoint = currentRay.aheadPoint;
333 | var sqRadius = currentRay.aheadPoint.r * currentRay.aheadPoint.r;
334 | if (!isBestIntersection(currentRay, nextRay, lineSites, intersectionPoint, sqRadius))
335 | return false;
336 | var newRay = fuseRay(currentRay);
337 | newRay.medialRay = {type: 'spine', children: [currentRay.medialRay, nextRay.medialRay], internalRay: newRay, origin: newRay.origin};
338 | if (observers['rayFused'])
339 | observers['rayFused'](previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay);
340 | deleteList.push([currentBucket, newRay]);
341 | }
342 | return false;
343 | });
344 |
345 | var rootMedialRays = [];
346 | var rootOrigin;
347 | for (var i = 0; i < deleteList.length; i++) {
348 | hasMoved = true;
349 | var bucket = deleteList[i][0];
350 | rootMedialRays.push(bucket.val.medialRay);
351 | rootOrigin = bucket.val.aheadPoint;
352 | bucket.val = deleteList[i][1];
353 | rayList.remove(bucket.next);
354 | }
355 |
356 | if (rayList.isEmpty())
357 | root = {type: 'root', children: rootMedialRays, origin: rootOrigin};
358 |
359 | if (observers['stepFinished'])
360 | observers['stepFinished']();
361 | if (!hasMoved && !stop)
362 | throw new Error('skeleton has not moved');
363 | return rayList.isEmpty() || stop;
364 | }
365 |
366 | while (!run(rayList, lineSites)) {
367 | }
368 | if (observers['afterProcess'])
369 | observers['afterProcess'](root, polygon, createLinkedList, run, siteList);
370 | return root;
371 | }
372 |
373 | return {
374 | createSkeleton: createSkeleton,
375 | LinearRay: LinearRay,
376 | ParabolicRay: ParabolicRay,
377 | LineSite: LineSite,
378 | ReflexVertexSite: ReflexVertexSite
379 | }
380 | })();
381 |
--------------------------------------------------------------------------------
/medial_axis_test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function pointOnParabola(t, directrix, focus) {
4 | // http://alecmce.com/category/math
5 | const directrixVector = {x: directrix[0].x - directrix[1].x, y: directrix[0].y - directrix[1].y};
6 | const pt = {x: directrix[0].x * (1 - t) + directrix[1].x * t, y: directrix[0].y * (1 - t) + directrix[1].y * t};
7 | const ptToFocus = {x: focus.x - pt.x, y: focus.y - pt.y};
8 | const dist = sqVectorLength(ptToFocus.x, ptToFocus.y) / (2 * (directrixVector.x * ptToFocus.y - directrixVector.y * ptToFocus.x));
9 | if (isFinite(dist))
10 | return {x: pt.x - directrixVector.y * dist, y: pt.y + directrixVector.x * dist};
11 | return null;
12 | }
13 |
14 | function segmentParabola(directrix, focus) {
15 | const pts = [];
16 | for (let i = 0; i <= 1; i += 0.1) {
17 | const point = pointOnParabola(i, directrix, focus);
18 | if (point)
19 | pts.push(point);
20 | }
21 | return pts;
22 | }
23 |
24 | function createSkeletonWithDisplay(polygon, observers) {
25 | if (!observers)
26 | observers = {};
27 |
28 | function raySitesRepresentation(ray) {
29 | return ray.firstSite.representation() + ray.secondSite.representation();
30 | }
31 |
32 | medialAxis.ReflexVertexSite.prototype.representation = function () {
33 | return pointArray2path([this.vertex], 2);
34 | };
35 |
36 | medialAxis.LineSite.prototype.representation = function () {
37 | return polylines2path([this.segment]) + pointArray2path([this.segment[0]], 1);
38 | };
39 | medialAxis.LinearRay.prototype.representation = function () {
40 | return polylines2path([(this.aheadPoint ? [this.origin, this.aheadPoint] : this.segment)]);
41 | };
42 | medialAxis.LinearRay.prototype.behindRepresentation = function () {
43 | return polylines2path([
44 | [this.behindPoint, this.origin]
45 | ]);
46 | };
47 | medialAxis.ParabolicRay.prototype.representation = function () {
48 | if (this.aheadPoint) {
49 | const p1 = pointProjectedOnSegment(this.aheadPoint, this.edge.segment);
50 | const p2 = pointProjectedOnSegment(this.origin, this.edge.segment);
51 | return polylines2path([segmentParabola([p1, p2], this.vertex)]);
52 | }
53 | return polylines2path([segmentParabola(this.edge.segment, this.vertex)]);
54 | };
55 | medialAxis.ParabolicRay.prototype.behindRepresentation = function () {
56 | const p1 = pointProjectedOnSegment(this.origin, this.edge.segment);
57 | const p2 = pointProjectedOnSegment(this.behindPoint, this.edge.segment);
58 | return p1 && p2 ? polylines2path([segmentParabola([p1, p2], this.vertex)]) : '';
59 | };
60 |
61 | let newSkelRepresentation = '';
62 | let currentRays = '';
63 | let skelPoints = [];
64 | let skelRepr = '';
65 | const root = medialAxis.createSkeleton(polygon, {
66 | initialized: function (rays, reflexPoints) {
67 | if (reflexPoints.length)
68 | svgDisplayTable([
69 | {
70 | label: 'reflex points', content: pathList2svg([
71 | {d: polygon2path(polygon)},
72 | {cssClass: 'red', d: pointArray2path(reflexPoints)}
73 | ])
74 | }
75 | ]);
76 | let rayRepresentation = '';
77 | for (let i = 0; i < rays.length; i++)
78 | rayRepresentation += rays[i].representation();
79 | svgDisplayTable([
80 | {
81 | label: 'initial rays', content: pathList2svg([
82 | {d: polygon2path(polygon)},
83 | {cssClass: 'red', d: rayRepresentation}
84 | ])
85 | }
86 | ]);
87 | },
88 | eliminatedRadius: function (currentRay, nextRay, intersectionPoint, sqRadius, nextEdge, otherSqrDist) {
89 | svgDisplayTable([
90 | {
91 | label: 'eliminated because of radius', content: pathList2svg([
92 | {cssClass: 'gray', d: polygon2path(polygon)},
93 | {
94 | cssClass: 'blue', d: currentRay.representation() + raySitesRepresentation(currentRay)
95 | + nextRay.representation() + pointArray2path([intersectionPoint], Math.sqrt(sqRadius))
96 | },
97 | {
98 | cssClass: 'red', d: nextEdge.representation()
99 | + pointArray2path([intersectionPoint], Math.sqrt(otherSqrDist))
100 | }
101 | ])
102 | }
103 | ]);
104 | },
105 | rayFused: function (previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay) {
106 | newSkelRepresentation += currentRay.representation() + nextRay.behindRepresentation();
107 | skelPoints.push(intersectionPoint);
108 | svgDisplayTable([
109 | {
110 | label: 'selected intersection', content: pathList2svg([
111 | {cssClass: 'gray', d: polygon2path(polygon) + previousRay.representation()},
112 | {d: previousRay.representation()},
113 | {cssClass: 'blue', d: nextRay.representation()},
114 | {cssClass: 'red', d: currentRay.representation() + pointArray2path([intersectionPoint])}
115 | ])
116 | },
117 | {
118 | label: 'new ray and corresponding sites', content: pathList2svg([
119 | {cssClass: 'gray', d: polygon2path(polygon)},
120 | {cssClass: 'blue', d: currentRay.representation() + nextRay.behindRepresentation()},
121 | {
122 | cssClass: 'red', d: pointArray2path([intersectionPoint], Math.sqrt(sqRadius))
123 | + raySitesRepresentation(newRay) + newRay.representation()
124 | }
125 | ])
126 | },
127 | {
128 | label: 'intersection', content: pathList2svg([
129 | {cssClass: 'gray', d: polygon2path(polygon)},
130 | {cssClass: 'blue', d: currentRay.representation() + nextRay.behindRepresentation()}
131 | ])
132 | }
133 | ]);
134 | if (observers['rayFused'])
135 | observers['rayFused'](previousRay, nextRay, currentRay, intersectionPoint, sqRadius, newRay);
136 | },
137 | last2raysEncountered: function (currentRay, nextRay) {
138 | console.log('stop 2');
139 | newSkelRepresentation += polylines2path([
140 | [currentRay.origin, nextRay.origin]
141 | ]);
142 | },
143 | raysIntersectionsComputed: function (rayList) {
144 | currentRays = '';
145 | skelPoints = [];
146 | rayList.iterate(function (currentBucket) {
147 | const current = currentBucket.val;
148 | currentRays += current.representation();
149 | });
150 | },
151 | stepFinished: function () {
152 | skelRepr += newSkelRepresentation;
153 | svgDisplayTable([
154 | {
155 | label: 'input step rays, selected intersections in red', content: pathList2svg([
156 | {cssClass: 'gray', d: polygon2path(polygon)},
157 | {cssClass: 'blue', d: currentRays},
158 | {cssClass: 'red', d: pointArray2path(skelPoints, 2)}
159 | ])
160 | },
161 | {
162 | label: 'added skeleton parts', content: pathList2svg([
163 | {cssClass: 'gray', d: polygon2path(polygon)},
164 | {cssClass: 'blue', d: newSkelRepresentation}
165 | ])
166 | },
167 | {
168 | label: 'skeleton after step', content: pathList2svg([
169 | {cssClass: 'gray', d: polygon2path(polygon)},
170 | {cssClass: 'blue', d: skelRepr}
171 | ])
172 | }
173 | ]);
174 | newSkelRepresentation = '';
175 | },
176 | afterProcess: observers['afterProcess']
177 | });
178 | const medialAxisRepr = displayMedialAxis(root.origin, root);
179 |
180 | function displayMedialAxis(origin, branch) {
181 | if (branch.type === 'limb')
182 | return polylines2path([
183 | [branch.vertex, origin]
184 | ]);
185 | const newOrigin = branch.origin;
186 | let res = polylines2path([
187 | [newOrigin, origin]
188 | ]);
189 | for (let i = 0; i < branch.children.length; i++)
190 | if (branch.children[i] != null)
191 | res += displayMedialAxis(newOrigin, branch.children[i]);
192 | return res;
193 | }
194 |
195 | svgDisplayTable([
196 | {
197 | label: 'medial axis', content: pathList2svg([
198 | {cssClass: 'gray', d: polygon2path(polygon)},
199 | {cssClass: 'blue', d: medialAxisRepr}
200 | ])
201 | }
202 | ]);
203 | }
204 |
205 | function extractDCELAfterProcess(root, polygon, createLinkedList, run, siteList) {
206 | function findRayForPoint(point, site, forbidden) {
207 | for (var i = 0; i < site.rays.length; i++) {
208 | var ray = site.rays[i];
209 | if (forbidden.indexOf(ray) !== -1)
210 | continue;
211 | if (ray.origin === point)
212 | return {ray: ray, direction: "forwards", nextPoint: ray.destination};
213 | if (ray.destination === point)
214 | return {ray: ray, direction: "backwards", nextPoint: ray.origin};
215 | }
216 | for (i = 0; i < site.backRays.length; i++) {
217 | ray = site.backRays[i];
218 | if (forbidden.indexOf(ray) !== -1)
219 | continue;
220 | if (ray.origin === point)
221 | return {ray: ray, direction: "forwards", nextPoint: ray.destination};
222 | if (ray.destination === point)
223 | return {ray: ray, direction: "backwards", nextPoint: ray.origin};
224 | }
225 | return null;
226 | }
227 |
228 | const vVerticesMap = {};
229 |
230 | function vVertexForPoint(point) {
231 | const key = 'p' + point.x + '|' + point.y;
232 | let cached = vVerticesMap[key];
233 | if (cached == null) {
234 | cached = {point: point, outEdges: []};
235 | vVerticesMap[key] = cached;
236 | }
237 | return cached;
238 | }
239 |
240 | function createFace(site, polygon) {
241 | function createVEdge(vvertex1, vvertex2, ray, face) {
242 | const vEdge = {
243 | v1: vvertex1,
244 | v2: vvertex2,
245 | ray: ray,
246 | face: face
247 | };
248 | for (let i = 0; i < vvertex2.outEdges.length; i++)
249 | if (vvertex2.outEdges[i].v1 === vvertex2 && vvertex2.outEdges[i].v2 === vvertex1) {
250 | vEdge.twin = vvertex2.outEdges[i];
251 | vvertex2.outEdges[i].twin = vEdge;
252 | }
253 | vvertex1.outEdges.push(vEdge);
254 | return vEdge;
255 | }
256 |
257 | const finalPoint = site instanceof medialAxis.LineSite ? site.segment[0] : site.vertex;
258 | let point = site instanceof medialAxis.LineSite ? site.segment[1] : site.vertex;
259 | const forbidden = [];
260 | const points = [point];
261 | const vertices = [vVertexForPoint(point)];
262 | const edges = [];
263 | var face = {site: site, edges: edges};
264 | do {
265 | const result = findRayForPoint(point, site, forbidden);
266 | forbidden.push(result.ray);
267 | point = result.nextPoint;
268 | if (result.nextPoint == null)
269 | console.log(result);
270 | points.push(point);
271 | vertices.push(vVertexForPoint(point));
272 | const edge = createVEdge(vertices[vertices.length - 2], vertices[vertices.length - 1], result.ray, face);
273 | if (edges.length)
274 | edges[edges.length - 1].next = edge;
275 | edges.push(edge);
276 | } while (point !== finalPoint);
277 | console.log(points);
278 | svgDisplayTable([
279 | {
280 | label: 'face ' + name, content: pathList2svg([
281 | {cssClass: 'gray', d: polygon2path(polygon)},
282 | {cssClass: 'red', d: site.representation()},
283 | {cssClass: 'green', d: polylines2path([points])}
284 | ])
285 | }
286 | ]);
287 | console.log(face);
288 | return face;
289 | }
290 |
291 | const faces = [];
292 | siteList.iterate(function (bucket) {
293 | faces.push(createFace(bucket.val, polygon));
294 | });
295 | for (var i = 0; i < faces.length; i++) {
296 | var face = faces[i];
297 | for (let j = 0; j < face.edges.length; j++) {
298 | var edge = face.edges[j];
299 | if (edge.twin == null)
300 | svgDisplayTable([
301 | {
302 | label: 'no twin', content: pathList2svg([
303 | {cssClass: 'gray', d: polygon2path(polygon)},
304 | {cssClass: 'green', d: face.site.representation()},
305 | {
306 | cssClass: 'red', d: polylines2path([
307 | [edge.v1.point, edge.v2.point]
308 | ])
309 | }
310 | ])
311 | }
312 | ]);
313 | }
314 | }
315 |
316 | }
317 |
318 | test('parabola', function () {
319 | const focus = p(50, 50);
320 | const directrix = [p(5, 60), p(60, 5)];
321 |
322 | svgDisplayTable([
323 | {
324 | label: 'focus directrix', content: pathList2svg([
325 | {d: polylines2path([directrix]) + pointArray2path([focus]) + polylines2path([segmentParabola(directrix, focus)])}
326 | ])
327 | }
328 | ]);
329 | });
330 |
331 | test('PLL solver non-parallel', function () {
332 | const s1 = [p(10, 10), p(100, 10)];
333 | const s2 = [p(100, 10), p(100, 140)];
334 | const vertex = p(40, 100);
335 |
336 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex), function () {
337 | return true;
338 | });
339 | let resultsDisplay = '';
340 | for (let i = 0; i < result.length; i++) {
341 | const obj = result[i];
342 | resultsDisplay += pointArray2path([obj], obj.r);
343 | }
344 | svgDisplayTable([
345 | {
346 | label: 'focus directrix', content: pathList2svg([
347 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
348 | {
349 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay
350 | + polylines2path([segmentParabola(s2, vertex)])
351 | + polylines2path([segmentParabola(s1, vertex)])
352 | }
353 | ])
354 | }
355 | ]);
356 | });
357 |
358 | test('PLL solver perpendicular', function () {
359 | let s1 = [p(10, 10), p(100, 10)];
360 | let s2 = [p(100, 10), p(100, 140)];
361 | let vertex = p(10, 10);
362 | let result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex));
363 | let resultsDisplay = '';
364 | for (var i = 0; i < result.length; i++)
365 | resultsDisplay += pointArray2path([result[i]], result[i].r);
366 | svgDisplayTable([
367 | {
368 | label: 'focus directrix', content: pathList2svg([
369 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
370 | {
371 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay
372 | + polylines2path([segmentParabola(s2, vertex)])
373 | + polylines2path([segmentParabola(s1, vertex)])
374 | }
375 | ])
376 | }
377 | ]);
378 |
379 | s1 = [p(20, 150), p(10, 140)];
380 | s2 = [p(10, 140), p(10, 95)];
381 | vertex = p(10, 95);
382 |
383 | result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex));
384 | resultsDisplay = '';
385 | for (i = 0; i < result.length; i++)
386 | resultsDisplay += pointArray2path([result[i]], result[i].r);
387 | svgDisplayTable([
388 | {
389 | label: 'focus directrix', content: pathList2svg([
390 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
391 | {
392 | cssClass: 'red', d: pointArray2path(result, 2) + resultsDisplay
393 | + polylines2path([segmentParabola(s2, vertex)])
394 | + polylines2path([segmentParabola(s1, vertex)])
395 | }
396 | ])
397 | }
398 | ]);
399 | });
400 |
401 | test('PLL solver parallel', function () {
402 | const s1 = [p(10, 10), p(10, 140)];
403 | const s2 = [p(100, 140), p(100, 10)];
404 | const vertex = p(25, 100);
405 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex));
406 | let resultsDisplay = '';
407 | for (let i = 0; i < result.length; i++)
408 | resultsDisplay += pointArray2path([result[i]], result[i].r);
409 | svgDisplayTable([
410 | {
411 | label: 'edges and circle', content: pathList2svg([
412 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
413 | {
414 | cssClass: 'red',
415 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s2, vertex)]) + polylines2path([segmentParabola(s1, vertex)])
416 | }
417 | ])
418 | }
419 | ]);
420 | deepEqual(result, [
421 | {x: 55, y: 133.54101966249684, r: -45},
422 | {x: 55, y: 66.45898033750316, r: -45}
423 | ]);
424 | });
425 |
426 | test('PLL solver point on side', function () {
427 | const s1 = [p(10, 10), p(10, 140)];
428 | const s2 = [p(100, 140), p(100, 10)];
429 | const vertex = p(10, 50);
430 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex));
431 | let resultsDisplay = '';
432 | for (let i = 0; i < result.length; i++) {
433 | const obj = result[i];
434 | resultsDisplay += pointArray2path([obj], obj.r);
435 | }
436 | svgDisplayTable([
437 | {
438 | label: 'edges and circle', content: pathList2svg([
439 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
440 | {
441 | cssClass: 'red',
442 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s2, vertex)]) + polylines2path([segmentParabola(s1, vertex)])
443 | }
444 | ])
445 | }
446 | ]);
447 | deepEqual(result, [
448 | {x: 55, y: 50, r: -45}
449 | ])
450 | });
451 |
452 | test('PLL solver unstable', function () {
453 | const factor = 1;
454 | const s1 = [p(30 / factor, 100 / factor), p(10 / factor, 10 / factor)];
455 | const s2 = [p(100 / factor, 10 / factor), p(50 / factor, 65 / factor)];
456 | const vertex = p(50 / factor, 65 / factor);
457 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addVertex(vertex));
458 | let resultsDisplay = '';
459 | for (let i = 0; i < result.length; i++) {
460 | const obj = result[i];
461 | resultsDisplay += pointArray2path([obj], obj.r);
462 | }
463 | deepEqual(result.length, 1);
464 | svgDisplayTable([
465 | {
466 | label: 'edges and circle', content: pathList2svg([
467 | {cssClass: 'blue', d: polylines2path([s1, s2]) + pointArray2path([vertex])},
468 | {
469 | cssClass: 'red',
470 | d: pointArray2path(result) + resultsDisplay + polylines2path([segmentParabola(s1, vertex)]) + polylines2path([segmentParabola(s1, vertex)])
471 | }
472 | ])
473 | }
474 | ]);
475 | });
476 |
477 | test('LLL solver', function () {
478 | const s1 = [p(10, 10), p(100, 10)];
479 | const s2 = [p(100, 10), p(100, 140)];
480 | const s3 = [p(100, 140), p(10, 140)];
481 |
482 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addSegment(s3));
483 | svgDisplayTable([
484 | {
485 | label: 'edges and circle', content: pathList2svg([
486 | {cssClass: 'blue', d: polylines2path([s1, s2, s3])},
487 | {cssClass: 'red', d: pointArray2path(result, result[0].r)}
488 | ])
489 | }
490 | ]);
491 | deepEqual(result, [
492 | {x: 35, y: 75, r: 65}
493 | ]);
494 | });
495 |
496 | test('LLL solver with flat vertex', function () {
497 | const flatVertex = p(50, 10);
498 | const v1 = p(10, 10);
499 | const v2 = p(100, 10);
500 | const v3 = p(100, 140);
501 | const s1 = [v1, flatVertex];
502 | const s2 = [flatVertex, v2];
503 | const s3 = [v2, v3];
504 | const result = solver.solveEquations(new solver.EquationSystemCreator().addSegment(s1).addSegment(s2).addSegment(s3));
505 | svgDisplayTable([
506 | {
507 | label: 'edges and circle', content: pathList2svg([
508 | {cssClass: 'blue', d: polylines2path([s1, s2, s3]) + pointArray2path([v1, flatVertex, v2, v3])},
509 | {cssClass: 'red', d: pointArray2path(result, result[0].r)}
510 | ])
511 | }
512 | ]);
513 | deepEqual(result, [
514 | {x: 50, y: 60, r: 50}
515 | ]);
516 | });
517 | test('medial axis1, 3 reflex points', function () {
518 | createSkeletonWithDisplay([
519 | p(10, 10),
520 | p(100, 10),
521 | p(50, 65),
522 | p(100, 140),
523 | p(40, 100),
524 | p(10, 140),
525 | p(20, 100)
526 | ]);
527 | });
528 | test('medial axis2, 1 reflex point', function () {
529 | createSkeletonWithDisplay([
530 | p(10, 10),
531 | p(100, 10),
532 | p(100, 140),
533 | p(40, 100),
534 | p(10, 140)
535 | ]);
536 | });
537 |
538 |
539 | test('medial axis3, convex polygon', function () {
540 | createSkeletonWithDisplay([
541 | p(10, 10),
542 | p(100, 10),
543 | p(150, 60),
544 | p(150, 100),
545 | p(100, 140),
546 | p(20, 150),
547 | p(10, 140)
548 | ]);
549 | });
550 |
551 | test('medial axis4, rectangle', function () {
552 | createSkeletonWithDisplay([
553 | p(10, 10),
554 | p(100, 10),
555 | p(100, 140),
556 | p(10, 140)
557 | ]);
558 | });
559 |
560 | test('medial axis5, convex polygon', function () {
561 |
562 | const p2 = [
563 | [326, 361],
564 | [361, 300],
565 | [397, 258],
566 | [457, 225],
567 | [490, 235],
568 | [522, 255],
569 | [564, 308],
570 | [606, 373],
571 | [575, 426],
572 | [540, 464],
573 | [465, 503],
574 | [439, 510],
575 | [367, 475],
576 | [348, 438]
577 | ];
578 | const polygon2 = [];
579 | for (let i = 0; i < p2.length; i++) {
580 | polygon2.push(p((p2[i][0] - 326) / 2, (p2[i][1] - 220) / 2));
581 | }
582 | createSkeletonWithDisplay(polygon2);
583 | });
584 |
585 | test('medial axis6, rectangle with flat vertices', function () {
586 | createSkeletonWithDisplay([
587 | p(10, 10),
588 | p(100, 10),
589 | p(100, 20),
590 | p(100, 30),
591 | p(100, 40),
592 | p(100, 50),
593 | p(100, 60),
594 | p(100, 100),
595 | p(100, 140),
596 | p(10, 140),
597 | p(10, 60),
598 | p(10, 50),
599 | p(10, 40),
600 | p(10, 30),
601 | p(10, 20)
602 | ]);
603 | });
604 |
605 | test('snake', function () {
606 | const poly = [p(458, 39),
607 | p(458, 39),
608 | p(395, 46),
609 | p(308, 76),
610 | p(141, 64),
611 | p(100, 80),
612 | p(234, 89),
613 | p(343, 99),
614 | p(400, 115),
615 | p(405, 66),
616 | p(423, 50),
617 | p(419, 135),
618 | p(378, 205),
619 | p(337, 201),
620 | p(72, 185),
621 | p(73, 205),
622 | p(306, 213),
623 | p(375, 226),
624 | p(412, 261),
625 | p(343, 326),
626 | p(233, 330),
627 | p(74, 344),
628 | p(57, 316),
629 | p(133, 290),
630 | p(290, 291),
631 | p(366, 232),
632 | p(296, 222),
633 | p(172, 246),
634 | p(41, 214),
635 | p(35, 178),
636 | p(197, 171),
637 | p(350, 194),
638 | p(398, 140),
639 | p(326, 117),
640 | p(155, 92),
641 | p(28, 138),
642 | p(22, 71),
643 | p(113, 52),
644 | p(277, 64),
645 | p(326, 35),
646 | p(391, 25),
647 | p(456, 23),
648 | p(498, 15)
649 | ];
650 | for (let i = 0; i < poly.length; i++) {
651 | const point = poly[i];
652 | point.x /= 2.5;
653 | point.y /= 2.5;
654 | }
655 |
656 | createSkeletonWithDisplay(poly, {
657 | afterProcess: extractDCELAfterProcess
658 | });
659 | });
660 |
661 | test('cut square with hole', function () {
662 |
663 | const centerX = 75;
664 | const centerY = 75;
665 |
666 | function cp(xdiff, ydiff) {
667 | return p(centerX + xdiff, centerY + ydiff);
668 | }
669 |
670 | const outerSide = 130;
671 | const halfOuterSide = outerSide / 2;
672 | const wallThickness = 50;
673 |
674 | const inserted1 = cp(halfOuterSide, 0);
675 | const inserted2 = cp(halfOuterSide - wallThickness, 0);
676 | const inserted3 = cp(halfOuterSide - wallThickness, 0);
677 | const inserted4 = cp(halfOuterSide, 0);
678 |
679 | const polygon = [
680 | cp(-halfOuterSide, -halfOuterSide),
681 | cp(-halfOuterSide, halfOuterSide),
682 | cp(halfOuterSide, halfOuterSide),
683 | inserted1,
684 | inserted2,
685 | cp(halfOuterSide - wallThickness, halfOuterSide - wallThickness),
686 | cp(-halfOuterSide + wallThickness, halfOuterSide - wallThickness),
687 | cp(-halfOuterSide + wallThickness, -halfOuterSide + wallThickness),
688 | cp(halfOuterSide - wallThickness, -halfOuterSide + wallThickness),
689 | inserted3,
690 | inserted4,
691 | cp(halfOuterSide, -halfOuterSide)
692 | ];
693 | const root = createSkeletonWithDisplay(polygon, {afterProcess: extractDCELAfterProcess});
694 |
695 | });
696 |
--------------------------------------------------------------------------------
/polygons.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | //https://github.com/substack/point-in-polygon/blob/master/index.js
4 | // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
5 | //to be replaced by a vertex radius filter when there is a kd map.
6 | function pointInPolygon(point, polygon) {
7 | var x = point.x, y = point.y;
8 | var inside = false;
9 | for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
10 | var xi = polygon[i].x, yi = polygon[i].y;
11 | var xj = polygon[j].x, yj = polygon[j].y;
12 |
13 | var intersect = ((yi > y) != (yj > y))
14 | && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
15 | if (intersect) inside = !inside;
16 | }
17 | return inside;
18 | }
19 |
20 | function commonVertex(segment1, segment2) {
21 | if(pointEquals(segment1[0], segment2[0]) || pointEquals(segment1[0], segment2[1]))
22 | return segment1[0];
23 | if (pointEquals(segment1[1], segment2[0]) || pointEquals(segment1[1], segment2[1]))
24 | return segment1[1];
25 | return null
26 | }
27 |
28 | function vectLength(x, y) {
29 | return Math.sqrt(sqVectorLength(x, y));
30 | }
31 |
32 | function sqr(val) {
33 | return val * val;
34 | }
35 |
36 | function sqVectorLength(x, y) {
37 | return sqr(x) + sqr(y);
38 | }
39 |
40 | function segLength(segment) {
41 | return vectLength(segment[0].x - segment[1].x, segment[0].y - segment[1].y);
42 | }
43 |
44 | function sqSegLength(segment) {
45 | return sqVectorLength(segment[0].x - segment[1].x, segment[0].y - segment[1].y);
46 | }
47 |
48 | function unitVector(inVector) {
49 | var len = vectLength(inVector.x, inVector.y);
50 | return {x: inVector.x / len, y: inVector.y / len};
51 | }
52 |
53 | function bisectorVector(v1, v2) {
54 | var l1 = vectLength(v1.x, v1.y);
55 | var l2 = vectLength(v2.x, v2.y);
56 | var x = l2 * v1.x + l1 * v2.x;
57 | var y = l2 * v1.y + l1 * v2.y;
58 | if (x == 0 && y == 0)
59 | return {x: v1.y, y: -v1.x};
60 | return {x: x, y: y};
61 | }
62 |
63 | function segmentToVector(segment) {
64 | return {x: segment[1].x - segment[0].x, y: segment[1].y - segment[0].y};
65 | }
66 |
67 | function perpendicularPoint(vertex, segment) {
68 | var vector = unitVector(segmentToVector(segment));
69 | //noinspection JSSuspiciousNameCombination
70 | var v = {x: -vector.y, y: vector.x};
71 | return {x: vertex.x + v.x * 100, y: vertex.y + v.y * 100};
72 | }
73 |
74 | function bisectorVectorFromSegments(s1, s2) {
75 | return bisectorVector({x: s1[1].x - s1[0].x, y: s1[1].y - s1[0].y},
76 | {x: s2[1].x - s2[0].x, y: s2[1].y - s2[0].y});
77 | }
78 |
79 | function pointEquals(p1, p2) {
80 | return p1.x == p2.x && p1.y == p2.y;
81 | }
82 |
83 | function pointProjectedOnSegment(point, segment) {
84 | function dist2(v, w) {
85 | return sqr(v.x - w.x) + sqr(v.y - w.y)
86 | }
87 |
88 | var v = segment[0];
89 | var w = segment[1];
90 | var l2 = dist2(v, w);
91 | if (l2 == 0) return dist2(point, v);
92 | var t = ((point.x - v.x) * (w.x - v.x) + (point.y - v.y) * (w.y - v.y)) / l2;
93 | if (t >= 0 && t <= 1)
94 | return { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) };
95 | return null;
96 | }
97 |
98 | function distToSegmentSquared(point, segment) {
99 | function dist2(v, w) {
100 | return sqr(v.x - w.x) + sqr(v.y - w.y)
101 | }
102 |
103 | var v = segment[0];
104 | var w = segment[1];
105 | var l2 = dist2(v, w);
106 | if (l2 == 0) return dist2(point, v);
107 | var t = ((point.x - v.x) * (w.x - v.x) + (point.y - v.y) * (w.y - v.y)) / l2;
108 | if (t < 0) return dist2(point, v);
109 | if (t > 1) return dist2(point, w);
110 | return dist2(point, { x: v.x + t * (w.x - v.x),
111 | y: v.y + t * (w.y - v.y) });
112 | }
113 |
114 | function signedArea(polygon) {
115 | var area = 0;
116 | for (var i = 0; i < polygon.length; i++) {
117 | var next = polygon[(i + 1) % polygon.length];
118 | area += polygon[i].x * next.y - polygon[i].y * next.x;
119 | }
120 | return area / 2;
121 | }
122 |
123 | //http://www.kevlindev.com/gui/math/intersection/Intersection.js
124 | function intersectionAbscissa(seg1, seg2) {
125 | var a1 = seg1[0], a2 = seg1[1], b1 = seg2[0], b2 = seg2[1];
126 | var divisor = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
127 | var ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x);
128 | var ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x);
129 | var ua = ua_t / divisor;
130 | var ub = ub_t / divisor;
131 | return [ua, ub];
132 | }
133 |
134 | //http://www.kevlindev.com/gui/math/intersection/Intersection.js
135 | function intersectionSegments(seg1, seg2, allowOutside) {
136 | //order the segments so that the results are always the same
137 | if (comparePoint(seg1[0], seg2[0]) > 0) {
138 | var tmp = seg1;
139 | seg1 = seg2;
140 | seg2 = tmp;
141 | }
142 | var a1 = seg1[0], a2 = seg1[1];
143 | var u = intersectionAbscissa(seg1, seg2);
144 | var ua = u[0];
145 | var ub = u[1];
146 | if (allowOutside && isFinite(ua) && isFinite(ub) || (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1))
147 | return {x: a1.x + ua * (a2.x - a1.x), y: a1.y + ua * (a2.y - a1.y)};
148 | return null;
149 | }
150 |
151 | function createIntersections(segments) {
152 | var intersections = [];
153 | for (var i = 0; i < segments.length; i++)
154 | if (comparePoint(segments[i][0], segments[i][1]) > 0)
155 | segments[i].reverse();
156 | for (i = 0; i < segments.length; i++) {
157 | for (var j = i + 1; j < segments.length; j++) {
158 | var intersection = intersectionSegments(segments[i], segments[j]);
159 | if (intersection)
160 | intersections.push([intersection, [segments[i], segments[j]]]);
161 | }
162 | }
163 | return intersections;
164 | }
165 |
166 | function comparePoint(p1, p2) {
167 | if (p1.x < p2.x)
168 | return -1;
169 | if (p1.x == p2.x)
170 | return p1.y - p2.y;
171 | return 1;
172 | }
173 |
174 | function eventsForSegment(segment) {
175 | var s = [segment[0], segment[1]];
176 | if (comparePoint(segment[0], segment[1]) > 0)
177 | s.reverse();
178 | return [
179 | {type: 'left', point: s[0], segment: s, action: function (scanBeam, eventQueue) {
180 | scanBeam.leftPoint(s[0], s, eventQueue);
181 | }},
182 | {type: 'right', point: s[1], segment: s, action: function (scanBeam, eventQueue) {
183 | scanBeam.rightPoint(s[1], s, eventQueue);
184 | }}
185 | ];
186 | }
187 |
188 | function initialPopulationOfQueue(segments) {
189 | var initialEvents = [];
190 | for (var i = 0; i < segments.length; i++) {
191 | var events = eventsForSegment(segments[i]);
192 | initialEvents.push(events[0]);
193 | initialEvents.push(events[1]);
194 | }
195 | return createMockEventQueue(initialEvents);
196 | }
197 |
198 | function bentleyOttmann(segments) {
199 | var queue = initialPopulationOfQueue(segments);
200 | var scanBeam = createMockScanBeam();
201 | while (!queue.isEmpty())
202 | queue.fetchFirst().action(scanBeam, queue);
203 | return scanBeam.getResult();
204 | }
205 |
206 | function createMockEventQueue(initialEvents) {
207 | var queue = [];
208 |
209 | function segmentEqual(s1, s2) {
210 | return s1 == s2
211 | || pointEquals(s1[0], s2[0]) && pointEquals(s1[1], s2[1])
212 | || pointEquals(s1[1], s2[0]) && pointEquals(s1[0], s2[1]);
213 | }
214 |
215 | function eventExists(event) {
216 | for (var i = 0; i < queue.length; i++)
217 | if (queue[i].type == 'intersection'
218 | && (segmentEqual(event.segments[0], queue[i].segments[0]) && segmentEqual(event.segments[1], queue[i].segments[1])
219 | || segmentEqual(event.segments[1], queue[i].segments[0]) && segmentEqual(event.segments[0], queue[i].segments[1])))
220 | return true;
221 | return false
222 | }
223 |
224 | function compareEvents(e1, e2) {
225 | return comparePoint(e1.point, e2.point);
226 | }
227 |
228 | function sort() {
229 | queue.sort(compareEvents);
230 | }
231 |
232 | Array.prototype.push.apply(queue, initialEvents);
233 | sort();
234 | return {
235 | pushIntersectionEvent: function (event) {
236 | if (eventExists(event))
237 | return;
238 | queue.push(event);
239 | sort();
240 | },
241 | fetchFirst: function () {
242 | return queue.shift();
243 | },
244 | isEmpty: function () {
245 | return queue.length == 0;
246 | },
247 | dumpQueue: function () {
248 | var res = [];
249 | for (var i = 0; i < queue.length; i++)
250 | res.push(queue[i]);
251 | return res;
252 | }
253 | }
254 | }
255 |
256 | function createMockScanBeam() {
257 | var beam = [];
258 | var result = [];
259 |
260 | function findIndex(point) {
261 | function segmentHeight(segment, x) {
262 | if (segment[0].x > segment[1].x)
263 | throw 'backwards segment';
264 | var t = (x - segment[0].x) / (segment[1].x - segment[0].x);
265 | return segment[0].y + t * (segment[1].y - segment[0].y);
266 | }
267 |
268 | for (var i = 0; i < beam.length; i++) {
269 | var segmentH = segmentHeight(beam[i], point.x);
270 | if (segmentH > point.y)
271 | return i;
272 | }
273 | return beam.length;
274 | }
275 |
276 | function findSegmentIndex(segment) {
277 | for (var i = 0; i < beam.length; i++)
278 | if (beam[i] == segment)
279 | return i;
280 | throw 'oops segment not in beam???';
281 | }
282 |
283 | function swap(i1, i2) {
284 | var tmp = beam[i1];
285 | beam[i1] = beam[i2];
286 | beam[i2] = tmp;
287 | }
288 |
289 | function pushIfIntersectsOnRight(currentPoint, previousSegment, nextSegment, eventQueue) {
290 | var intersection = intersectionSegments(nextSegment, previousSegment);
291 | if (intersection != null && comparePoint(currentPoint, intersection) < 0) {
292 | var segments = [previousSegment, nextSegment];
293 | eventQueue.pushIntersectionEvent({type: 'intersection', point: intersection, segments: segments,
294 | action: function (scanBeam, eventQueue) {
295 | scanBeam.intersectionPoint(intersection, segments, eventQueue);
296 | }});
297 | }
298 | }
299 |
300 | return {
301 | leftPoint: function (point, segment, eventQueue) {
302 | var insertionIndex = findIndex(point);
303 | if (insertionIndex > 0)
304 | pushIfIntersectsOnRight(point, beam[insertionIndex - 1], segment, eventQueue);
305 | if (insertionIndex < beam.length)
306 | pushIfIntersectsOnRight(point, segment, beam[insertionIndex], eventQueue);
307 | beam.splice(insertionIndex, 0, segment);
308 | },
309 | rightPoint: function (point, segment, eventQueue) {
310 | var segmentIndex = findSegmentIndex(segment);
311 | if (segmentIndex > 0 && segmentIndex < beam.length - 1)
312 | pushIfIntersectsOnRight(point, beam[segmentIndex - 1], beam[segmentIndex + 1], eventQueue);
313 | beam.splice(segmentIndex, 1);
314 | },
315 | intersectionPoint: function (point, segments, eventQueue) {
316 | result.push([point, segments]);
317 | var before = findSegmentIndex(segments[0]);
318 | var after = findSegmentIndex(segments[1]);
319 | swap(before, after);
320 | if (before > 0)
321 | pushIfIntersectsOnRight(point, beam[before - 1], segments[1], eventQueue);
322 | if (after < beam.length - 1)
323 | pushIfIntersectsOnRight(point, segments[0], beam[after + 1], eventQueue);
324 | },
325 | getResult: function () {
326 | return result;
327 | },
328 | dumpBeam: function () {
329 | var res = [];
330 | for (var i = 0; i < beam.length; i++)
331 | res.push(beam[i]);
332 | return res;
333 | }
334 | };
335 | }
336 |
--------------------------------------------------------------------------------
/qunit-1.12.0.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework
3 | *
4 | * http://qunitjs.com
5 | *
6 | * Copyright 2012 jQuery Foundation and other contributors
7 | * Released under the MIT license.
8 | * http://jquery.org/license
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 5px 5px 0 0;
42 | -moz-border-radius: 5px 5px 0 0;
43 | -webkit-border-top-right-radius: 5px;
44 | -webkit-border-top-left-radius: 5px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-testrunner-toolbar label {
58 | display: inline-block;
59 | padding: 0 .5em 0 .1em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | overflow: hidden;
71 | }
72 |
73 | #qunit-userAgent {
74 | padding: 0.5em 0 0.5em 2.5em;
75 | background-color: #2b81af;
76 | color: #fff;
77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
78 | }
79 |
80 | #qunit-modulefilter-container {
81 | float: right;
82 | }
83 |
84 | /** Tests: Pass/Fail */
85 |
86 | #qunit-tests {
87 | list-style-position: inside;
88 | }
89 |
90 | #qunit-tests li {
91 | padding: 0.4em 0.5em 0.4em 2.5em;
92 | border-bottom: 1px solid #fff;
93 | list-style-position: inside;
94 | }
95 |
96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
97 | display: none;
98 | }
99 |
100 | #qunit-tests li strong {
101 | cursor: pointer;
102 | }
103 |
104 | #qunit-tests li a {
105 | padding: 0.5em;
106 | color: #c2ccd1;
107 | text-decoration: none;
108 | }
109 | #qunit-tests li a:hover,
110 | #qunit-tests li a:focus {
111 | color: #000;
112 | }
113 |
114 | #qunit-tests li .runtime {
115 | float: right;
116 | font-size: smaller;
117 | }
118 |
119 | .qunit-assert-list {
120 | margin-top: 0.5em;
121 | padding: 0.5em;
122 |
123 | background-color: #fff;
124 |
125 | border-radius: 5px;
126 | -moz-border-radius: 5px;
127 | -webkit-border-radius: 5px;
128 | }
129 |
130 | .qunit-collapsed {
131 | display: none;
132 | }
133 |
134 | #qunit-tests table {
135 | border-collapse: collapse;
136 | margin-top: .2em;
137 | }
138 |
139 | #qunit-tests th {
140 | text-align: right;
141 | vertical-align: top;
142 | padding: 0 .5em 0 0;
143 | }
144 |
145 | #qunit-tests td {
146 | vertical-align: top;
147 | }
148 |
149 | #qunit-tests pre {
150 | margin: 0;
151 | white-space: pre-wrap;
152 | word-wrap: break-word;
153 | }
154 |
155 | #qunit-tests del {
156 | background-color: #e0f2be;
157 | color: #374e0c;
158 | text-decoration: none;
159 | }
160 |
161 | #qunit-tests ins {
162 | background-color: #ffcaca;
163 | color: #500;
164 | text-decoration: none;
165 | }
166 |
167 | /*** Test Counts */
168 |
169 | #qunit-tests b.counts { color: black; }
170 | #qunit-tests b.passed { color: #5E740B; }
171 | #qunit-tests b.failed { color: #710909; }
172 |
173 | #qunit-tests li li {
174 | padding: 5px;
175 | background-color: #fff;
176 | border-bottom: none;
177 | list-style-position: inside;
178 | }
179 |
180 | /*** Passing Styles */
181 |
182 | #qunit-tests li li.pass {
183 | color: #3c510c;
184 | background-color: #fff;
185 | border-left: 10px solid #C6E746;
186 | }
187 |
188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
189 | #qunit-tests .pass .test-name { color: #366097; }
190 |
191 | #qunit-tests .pass .test-actual,
192 | #qunit-tests .pass .test-expected { color: #999999; }
193 |
194 | #qunit-banner.qunit-pass { background-color: #C6E746; }
195 |
196 | /*** Failing Styles */
197 |
198 | #qunit-tests li li.fail {
199 | color: #710909;
200 | background-color: #fff;
201 | border-left: 10px solid #EE5757;
202 | white-space: pre;
203 | }
204 |
205 | #qunit-tests > li:last-child {
206 | border-radius: 0 0 5px 5px;
207 | -moz-border-radius: 0 0 5px 5px;
208 | -webkit-border-bottom-right-radius: 5px;
209 | -webkit-border-bottom-left-radius: 5px;
210 | }
211 |
212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
213 | #qunit-tests .fail .test-name,
214 | #qunit-tests .fail .module-name { color: #000000; }
215 |
216 | #qunit-tests .fail .test-actual { color: #EE5757; }
217 | #qunit-tests .fail .test-expected { color: green; }
218 |
219 | #qunit-banner.qunit-fail { background-color: #EE5757; }
220 |
221 |
222 | /** Result */
223 |
224 | #qunit-testresult {
225 | padding: 0.5em 0.5em 0.5em 2.5em;
226 |
227 | color: #2b81af;
228 | background-color: #D2E0E6;
229 |
230 | border-bottom: 1px solid white;
231 | }
232 | #qunit-testresult .module-name {
233 | font-weight: bold;
234 | }
235 |
236 | /** Fixture */
237 |
238 | #qunit-fixture {
239 | position: absolute;
240 | top: -10000px;
241 | left: -10000px;
242 | width: 1000px;
243 | height: 1000px;
244 | }
245 |
--------------------------------------------------------------------------------
/rhill-voronoi-core.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Copyright (C) 2010-2013 Raymond Hill: https://github.com/gorhill/Javascript-Voronoi
3 | MIT License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md
4 | */
5 | /*
6 | Author: Raymond Hill (rhill@raymondhill.net)
7 | Contributor: Jesse Morgan (morgajel@gmail.com)
8 | File: rhill-voronoi-core.js
9 | Version: 0.98
10 | Date: January 21, 2013
11 | Description: This is my personal Javascript implementation of
12 | Steven Fortune's algorithm to compute Voronoi diagrams.
13 |
14 | License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md
15 | Credits: See https://github.com/gorhill/Javascript-Voronoi/CREDITS.md
16 | History: See https://github.com/gorhill/Javascript-Voronoi/CHANGELOG.md
17 |
18 | ## Usage:
19 |
20 | var sites = [{x:300,y:300}, {x:100,y:100}, {x:200,y:500}, {x:250,y:450}, {x:600,y:150}];
21 | // xl, xr means x left, x right
22 | // yt, yb means y top, y bottom
23 | var bbox = {xl:0, xr:800, yt:0, yb:600};
24 | var voronoi = new Voronoi();
25 | // pass an object which exhibits xl, xr, yt, yb properties. The bounding
26 | // box will be used to connect unbound edges, and to close open cells
27 | result = voronoi.compute(sites, bbox);
28 | // render, further analyze, etc.
29 |
30 | Return value:
31 | An object with the following properties:
32 |
33 | result.vertices = an array of unordered, unique Voronoi.Vertex objects making
34 | up the Voronoi diagram.
35 | result.edges = an array of unordered, unique Voronoi.Edge objects making up
36 | the Voronoi diagram.
37 | result.cells = an array of Voronoi.Cell object making up the Voronoi diagram.
38 | A Cell object might have an empty array of halfedges, meaning no Voronoi
39 | cell could be computed for a particular cell.
40 | result.execTime = the time it took to compute the Voronoi diagram, in
41 | milliseconds.
42 |
43 | Voronoi.Vertex object:
44 | x: The x position of the vertex.
45 | y: The y position of the vertex.
46 |
47 | Voronoi.Edge object:
48 | lSite: the Voronoi site object at the left of this Voronoi.Edge object.
49 | rSite: the Voronoi site object at the right of this Voronoi.Edge object (can
50 | be null).
51 | va: an object with an 'x' and a 'y' property defining the start point
52 | (relative to the Voronoi site on the left) of this Voronoi.Edge object.
53 | vb: an object with an 'x' and a 'y' property defining the end point
54 | (relative to Voronoi site on the left) of this Voronoi.Edge object.
55 |
56 | For edges which are used to close open cells (using the supplied bounding
57 | box), the rSite property will be null.
58 |
59 | Voronoi.Cell object:
60 | site: the Voronoi site object associated with the Voronoi cell.
61 | halfedges: an array of Voronoi.Halfedge objects, ordered counterclockwise,
62 | defining the polygon for this Voronoi cell.
63 |
64 | Voronoi.Halfedge object:
65 | site: the Voronoi site object owning this Voronoi.Halfedge object.
66 | edge: a reference to the unique Voronoi.Edge object underlying this
67 | Voronoi.Halfedge object.
68 | getStartpoint(): a method returning an object with an 'x' and a 'y' property
69 | for the start point of this halfedge. Keep in mind halfedges are always
70 | countercockwise.
71 | getEndpoint(): a method returning an object with an 'x' and a 'y' property
72 | for the end point of this halfedge. Keep in mind halfedges are always
73 | countercockwise.
74 |
75 | TODO: Identify opportunities for performance improvement.
76 |
77 | TODO: Let the user close the Voronoi cells, do not do it automatically. Not only let
78 | him close the cells, but also allow him to close more than once using a different
79 | bounding box for the same Voronoi diagram.
80 | */
81 |
82 | /*global Math */
83 |
84 | // ---------------------------------------------------------------------------
85 |
86 | function Voronoi() {
87 | this.vertices = null;
88 | this.edges = null;
89 | this.cells = null;
90 | this.toRecycle = null;
91 | this.beachsectionJunkyard = [];
92 | this.circleEventJunkyard = [];
93 | this.vertexJunkyard = [];
94 | this.edgeJunkyard = [];
95 | this.cellJunkyard = [];
96 | }
97 |
98 | // ---------------------------------------------------------------------------
99 |
100 | Voronoi.prototype.reset = function () {
101 | if (!this.beachline) {
102 | this.beachline = new this.RBTree();
103 | }
104 | // Move leftover beachsections to the beachsection junkyard.
105 | if (this.beachline.root) {
106 | var beachsection = this.beachline.getFirst(this.beachline.root);
107 | while (beachsection) {
108 | this.beachsectionJunkyard.push(beachsection); // mark for reuse
109 | beachsection = beachsection.rbNext;
110 | }
111 | }
112 | this.beachline.root = null;
113 | if (!this.circleEvents) {
114 | this.circleEvents = new this.RBTree();
115 | }
116 | this.circleEvents.root = this.firstCircleEvent = null;
117 | this.vertices = [];
118 | this.edges = [];
119 | this.cells = [];
120 | };
121 |
122 | Voronoi.prototype.sqrt = Math.sqrt;
123 | Voronoi.prototype.abs = Math.abs;
124 | Voronoi.prototype.ε = Voronoi.ε = 1e-9;
125 | Voronoi.prototype.invε = Voronoi.invε = 1.0 / Voronoi.ε;
126 | Voronoi.prototype.equalWithEpsilon = function (a, b) {
127 | return this.abs(a - b) < 1e-9;
128 | };
129 | Voronoi.prototype.greaterThanWithEpsilon = function (a, b) {
130 | return a - b > 1e-9;
131 | };
132 | Voronoi.prototype.greaterThanOrEqualWithEpsilon = function (a, b) {
133 | return b - a < 1e-9;
134 | };
135 | Voronoi.prototype.lessThanWithEpsilon = function (a, b) {
136 | return b - a > 1e-9;
137 | };
138 | Voronoi.prototype.lessThanOrEqualWithEpsilon = function (a, b) {
139 | return a - b < 1e-9;
140 | };
141 |
142 | // ---------------------------------------------------------------------------
143 | // Red-Black tree code (based on C version of "rbtree" by Franck Bui-Huu
144 | // https://github.com/fbuihuu/libtree/blob/master/rb.c
145 |
146 | Voronoi.prototype.RBTree = function () {
147 | this.root = null;
148 | };
149 |
150 | Voronoi.prototype.RBTree.prototype.rbInsertSuccessor = function (node, successor) {
151 | var parent;
152 | if (node) {
153 | // >>> rhill 2011-05-27: Performance: cache previous/next nodes
154 | successor.rbPrevious = node;
155 | successor.rbNext = node.rbNext;
156 | if (node.rbNext) {
157 | node.rbNext.rbPrevious = successor;
158 | }
159 | node.rbNext = successor;
160 | // <<<
161 | if (node.rbRight) {
162 | // in-place expansion of node.rbRight.getFirst();
163 | node = node.rbRight;
164 | while (node.rbLeft) {
165 | node = node.rbLeft;
166 | }
167 | node.rbLeft = successor;
168 | }
169 | else {
170 | node.rbRight = successor;
171 | }
172 | parent = node;
173 | }
174 | // rhill 2011-06-07: if node is null, successor must be inserted
175 | // to the left-most part of the tree
176 | else if (this.root) {
177 | node = this.getFirst(this.root);
178 | // >>> Performance: cache previous/next nodes
179 | successor.rbPrevious = null;
180 | successor.rbNext = node;
181 | node.rbPrevious = successor;
182 | // <<<
183 | node.rbLeft = successor;
184 | parent = node;
185 | }
186 | else {
187 | // >>> Performance: cache previous/next nodes
188 | successor.rbPrevious = successor.rbNext = null;
189 | // <<<
190 | this.root = successor;
191 | parent = null;
192 | }
193 | successor.rbLeft = successor.rbRight = null;
194 | successor.rbParent = parent;
195 | successor.rbRed = true;
196 | // Fixup the modified tree by recoloring nodes and performing
197 | // rotations (2 at most) hence the red-black tree properties are
198 | // preserved.
199 | var grandpa, uncle;
200 | node = successor;
201 | while (parent && parent.rbRed) {
202 | grandpa = parent.rbParent;
203 | if (parent === grandpa.rbLeft) {
204 | uncle = grandpa.rbRight;
205 | if (uncle && uncle.rbRed) {
206 | parent.rbRed = uncle.rbRed = false;
207 | grandpa.rbRed = true;
208 | node = grandpa;
209 | }
210 | else {
211 | if (node === parent.rbRight) {
212 | this.rbRotateLeft(parent);
213 | node = parent;
214 | parent = node.rbParent;
215 | }
216 | parent.rbRed = false;
217 | grandpa.rbRed = true;
218 | this.rbRotateRight(grandpa);
219 | }
220 | }
221 | else {
222 | uncle = grandpa.rbLeft;
223 | if (uncle && uncle.rbRed) {
224 | parent.rbRed = uncle.rbRed = false;
225 | grandpa.rbRed = true;
226 | node = grandpa;
227 | }
228 | else {
229 | if (node === parent.rbLeft) {
230 | this.rbRotateRight(parent);
231 | node = parent;
232 | parent = node.rbParent;
233 | }
234 | parent.rbRed = false;
235 | grandpa.rbRed = true;
236 | this.rbRotateLeft(grandpa);
237 | }
238 | }
239 | parent = node.rbParent;
240 | }
241 | this.root.rbRed = false;
242 | };
243 |
244 | Voronoi.prototype.RBTree.prototype.rbRemoveNode = function (node) {
245 | // >>> rhill 2011-05-27: Performance: cache previous/next nodes
246 | if (node.rbNext) {
247 | node.rbNext.rbPrevious = node.rbPrevious;
248 | }
249 | if (node.rbPrevious) {
250 | node.rbPrevious.rbNext = node.rbNext;
251 | }
252 | node.rbNext = node.rbPrevious = null;
253 | // <<<
254 | var parent = node.rbParent,
255 | left = node.rbLeft,
256 | right = node.rbRight,
257 | next;
258 | if (!left) {
259 | next = right;
260 | }
261 | else if (!right) {
262 | next = left;
263 | }
264 | else {
265 | next = this.getFirst(right);
266 | }
267 | if (parent) {
268 | if (parent.rbLeft === node) {
269 | parent.rbLeft = next;
270 | }
271 | else {
272 | parent.rbRight = next;
273 | }
274 | }
275 | else {
276 | this.root = next;
277 | }
278 | // enforce red-black rules
279 | var isRed;
280 | if (left && right) {
281 | isRed = next.rbRed;
282 | next.rbRed = node.rbRed;
283 | next.rbLeft = left;
284 | left.rbParent = next;
285 | if (next !== right) {
286 | parent = next.rbParent;
287 | next.rbParent = node.rbParent;
288 | node = next.rbRight;
289 | parent.rbLeft = node;
290 | next.rbRight = right;
291 | right.rbParent = next;
292 | }
293 | else {
294 | next.rbParent = parent;
295 | parent = next;
296 | node = next.rbRight;
297 | }
298 | }
299 | else {
300 | isRed = node.rbRed;
301 | node = next;
302 | }
303 | // 'node' is now the sole successor's child and 'parent' its
304 | // new parent (since the successor can have been moved)
305 | if (node) {
306 | node.rbParent = parent;
307 | }
308 | // the 'easy' cases
309 | if (isRed) {
310 | return;
311 | }
312 | if (node && node.rbRed) {
313 | node.rbRed = false;
314 | return;
315 | }
316 | // the other cases
317 | var sibling;
318 | do {
319 | if (node === this.root) {
320 | break;
321 | }
322 | if (node === parent.rbLeft) {
323 | sibling = parent.rbRight;
324 | if (sibling.rbRed) {
325 | sibling.rbRed = false;
326 | parent.rbRed = true;
327 | this.rbRotateLeft(parent);
328 | sibling = parent.rbRight;
329 | }
330 | if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) {
331 | if (!sibling.rbRight || !sibling.rbRight.rbRed) {
332 | sibling.rbLeft.rbRed = false;
333 | sibling.rbRed = true;
334 | this.rbRotateRight(sibling);
335 | sibling = parent.rbRight;
336 | }
337 | sibling.rbRed = parent.rbRed;
338 | parent.rbRed = sibling.rbRight.rbRed = false;
339 | this.rbRotateLeft(parent);
340 | node = this.root;
341 | break;
342 | }
343 | }
344 | else {
345 | sibling = parent.rbLeft;
346 | if (sibling.rbRed) {
347 | sibling.rbRed = false;
348 | parent.rbRed = true;
349 | this.rbRotateRight(parent);
350 | sibling = parent.rbLeft;
351 | }
352 | if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) {
353 | if (!sibling.rbLeft || !sibling.rbLeft.rbRed) {
354 | sibling.rbRight.rbRed = false;
355 | sibling.rbRed = true;
356 | this.rbRotateLeft(sibling);
357 | sibling = parent.rbLeft;
358 | }
359 | sibling.rbRed = parent.rbRed;
360 | parent.rbRed = sibling.rbLeft.rbRed = false;
361 | this.rbRotateRight(parent);
362 | node = this.root;
363 | break;
364 | }
365 | }
366 | sibling.rbRed = true;
367 | node = parent;
368 | parent = parent.rbParent;
369 | } while (!node.rbRed);
370 | if (node) {
371 | node.rbRed = false;
372 | }
373 | };
374 |
375 | Voronoi.prototype.RBTree.prototype.rbRotateLeft = function (node) {
376 | var p = node,
377 | q = node.rbRight, // can't be null
378 | parent = p.rbParent;
379 | if (parent) {
380 | if (parent.rbLeft === p) {
381 | parent.rbLeft = q;
382 | }
383 | else {
384 | parent.rbRight = q;
385 | }
386 | }
387 | else {
388 | this.root = q;
389 | }
390 | q.rbParent = parent;
391 | p.rbParent = q;
392 | p.rbRight = q.rbLeft;
393 | if (p.rbRight) {
394 | p.rbRight.rbParent = p;
395 | }
396 | q.rbLeft = p;
397 | };
398 |
399 | Voronoi.prototype.RBTree.prototype.rbRotateRight = function (node) {
400 | var p = node,
401 | q = node.rbLeft, // can't be null
402 | parent = p.rbParent;
403 | if (parent) {
404 | if (parent.rbLeft === p) {
405 | parent.rbLeft = q;
406 | }
407 | else {
408 | parent.rbRight = q;
409 | }
410 | }
411 | else {
412 | this.root = q;
413 | }
414 | q.rbParent = parent;
415 | p.rbParent = q;
416 | p.rbLeft = q.rbRight;
417 | if (p.rbLeft) {
418 | p.rbLeft.rbParent = p;
419 | }
420 | q.rbRight = p;
421 | };
422 |
423 | Voronoi.prototype.RBTree.prototype.getFirst = function (node) {
424 | while (node.rbLeft) {
425 | node = node.rbLeft;
426 | }
427 | return node;
428 | };
429 |
430 | Voronoi.prototype.RBTree.prototype.getLast = function (node) {
431 | while (node.rbRight) {
432 | node = node.rbRight;
433 | }
434 | return node;
435 | };
436 |
437 | // ---------------------------------------------------------------------------
438 | // Diagram methods
439 |
440 | Voronoi.prototype.Diagram = function (site) {
441 | this.site = site;
442 | };
443 |
444 | // ---------------------------------------------------------------------------
445 | // Cell methods
446 |
447 | Voronoi.prototype.Cell = function (site) {
448 | this.site = site;
449 | this.halfedges = [];
450 | this.closeMe = false;
451 | };
452 |
453 | Voronoi.prototype.Cell.prototype.init = function (site) {
454 | this.site = site;
455 | this.halfedges = [];
456 | this.closeMe = false;
457 | return this;
458 | };
459 |
460 | Voronoi.prototype.createCell = function (site) {
461 | var cell = this.cellJunkyard.pop();
462 | if (cell) {
463 | return cell.init(site);
464 | }
465 | return new this.Cell(site);
466 | };
467 |
468 | Voronoi.prototype.Cell.prototype.prepareHalfedges = function () {
469 | var halfedges = this.halfedges,
470 | iHalfedge = halfedges.length,
471 | edge;
472 | // get rid of unused halfedges
473 | // rhill 2011-05-27: Keep it simple, no point here in trying
474 | // to be fancy: dangling edges are a typically a minority.
475 | while (iHalfedge--) {
476 | edge = halfedges[iHalfedge].edge;
477 | if (!edge.vb || !edge.va) {
478 | halfedges.splice(iHalfedge, 1);
479 | }
480 | }
481 |
482 | // rhill 2011-05-26: I tried to use a binary search at insertion
483 | // time to keep the array sorted on-the-fly (in Cell.addHalfedge()).
484 | // There was no real benefits in doing so, performance on
485 | // Firefox 3.6 was improved marginally, while performance on
486 | // Opera 11 was penalized marginally.
487 | halfedges.sort(function (a, b) {
488 | return b.angle - a.angle;
489 | });
490 | return halfedges.length;
491 | };
492 |
493 | // Return a list of the neighbor Ids
494 | Voronoi.prototype.Cell.prototype.getNeighborIds = function () {
495 | var neighbors = [],
496 | iHalfedge = this.halfedges.length,
497 | edge;
498 | while (iHalfedge--) {
499 | edge = this.halfedges[iHalfedge].edge;
500 | if (edge.lSite !== null && edge.lSite.voronoiId != this.site.voronoiId) {
501 | neighbors.push(edge.lSite.voronoiId);
502 | }
503 | else if (edge.rSite !== null && edge.rSite.voronoiId != this.site.voronoiId) {
504 | neighbors.push(edge.rSite.voronoiId);
505 | }
506 | }
507 | return neighbors;
508 | };
509 |
510 | // Compute bounding box
511 | //
512 | Voronoi.prototype.Cell.prototype.getBbox = function () {
513 | var halfedges = this.halfedges,
514 | iHalfedge = halfedges.length,
515 | xmin = Infinity,
516 | ymin = Infinity,
517 | xmax = -Infinity,
518 | ymax = -Infinity,
519 | v, vx, vy;
520 | while (iHalfedge--) {
521 | v = halfedges[iHalfedge].getStartpoint();
522 | vx = v.x;
523 | vy = v.y;
524 | if (vx < xmin) {
525 | xmin = vx;
526 | }
527 | if (vy < ymin) {
528 | ymin = vy;
529 | }
530 | if (vx > xmax) {
531 | xmax = vx;
532 | }
533 | if (vy > ymax) {
534 | ymax = vy;
535 | }
536 | // we dont need to take into account end point,
537 | // since each end point matches a start point
538 | }
539 | return {
540 | x: xmin,
541 | y: ymin,
542 | width: xmax - xmin,
543 | height: ymax - ymin
544 | };
545 | };
546 |
547 | // Return whether a point is inside, on, or outside the cell:
548 | // -1: point is outside the perimeter of the cell
549 | // 0: point is on the perimeter of the cell
550 | // 1: point is inside the perimeter of the cell
551 | //
552 | Voronoi.prototype.Cell.prototype.pointIntersection = function (x, y) {
553 | // Check if point in polygon. Since all polygons of a Voronoi
554 | // diagram are convex, then:
555 | // http://paulbourke.net/geometry/polygonmesh/
556 | // Solution 3 (2D):
557 | // "If the polygon is convex then one can consider the polygon
558 | // "as a 'path' from the first vertex. A point is on the interior
559 | // "of this polygons if it is always on the same side of all the
560 | // "line segments making up the path. ...
561 | // "(y - y0) (x1 - x0) - (x - x0) (y1 - y0)
562 | // "if it is less than 0 then P is to the right of the line segment,
563 | // "if greater than 0 it is to the left, if equal to 0 then it lies
564 | // "on the line segment"
565 | var halfedges = this.halfedges,
566 | iHalfedge = halfedges.length,
567 | halfedge,
568 | p0, p1, r;
569 | while (iHalfedge--) {
570 | halfedge = halfedges[iHalfedge];
571 | p0 = halfedge.getStartpoint();
572 | p1 = halfedge.getEndpoint();
573 | r = (y - p0.y) * (p1.x - p0.x) - (x - p0.x) * (p1.y - p0.y);
574 | if (!r) {
575 | return 0;
576 | }
577 | if (r > 0) {
578 | return -1;
579 | }
580 | }
581 | return 1;
582 | };
583 |
584 | // ---------------------------------------------------------------------------
585 | // Edge methods
586 | //
587 |
588 | Voronoi.prototype.Vertex = function (x, y) {
589 | this.x = x;
590 | this.y = y;
591 | };
592 |
593 | Voronoi.prototype.Edge = function (lSite, rSite) {
594 | this.lSite = lSite;
595 | this.rSite = rSite;
596 | this.va = this.vb = null;
597 | };
598 |
599 | Voronoi.prototype.Halfedge = function (edge, lSite, rSite) {
600 | this.site = lSite;
601 | this.edge = edge;
602 | // 'angle' is a value to be used for properly sorting the
603 | // halfsegments counterclockwise. By convention, we will
604 | // use the angle of the line defined by the 'site to the left'
605 | // to the 'site to the right'.
606 | // However, border edges have no 'site to the right': thus we
607 | // use the angle of line perpendicular to the halfsegment (the
608 | // edge should have both end points defined in such case.)
609 | if (rSite) {
610 | this.angle = Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x);
611 | }
612 | else {
613 | var va = edge.va,
614 | vb = edge.vb;
615 | // rhill 2011-05-31: used to call getStartpoint()/getEndpoint(),
616 | // but for performance purpose, these are expanded in place here.
617 | this.angle = edge.lSite === lSite ?
618 | Math.atan2(vb.x - va.x, va.y - vb.y) :
619 | Math.atan2(va.x - vb.x, vb.y - va.y);
620 | }
621 | };
622 |
623 | Voronoi.prototype.createHalfedge = function (edge, lSite, rSite) {
624 | return new this.Halfedge(edge, lSite, rSite);
625 | };
626 |
627 | Voronoi.prototype.Halfedge.prototype.getStartpoint = function () {
628 | return this.edge.lSite === this.site ? this.edge.va : this.edge.vb;
629 | };
630 |
631 | Voronoi.prototype.Halfedge.prototype.getEndpoint = function () {
632 | return this.edge.lSite === this.site ? this.edge.vb : this.edge.va;
633 | };
634 |
635 |
636 | // this create and add a vertex to the internal collection
637 |
638 | Voronoi.prototype.createVertex = function (x, y) {
639 | var v = this.vertexJunkyard.pop();
640 | if (!v) {
641 | v = new this.Vertex(x, y);
642 | }
643 | else {
644 | v.x = x;
645 | v.y = y;
646 | }
647 | this.vertices.push(v);
648 | return v;
649 | };
650 |
651 | // this create and add an edge to internal collection, and also create
652 | // two halfedges which are added to each site's counterclockwise array
653 | // of halfedges.
654 |
655 | Voronoi.prototype.createEdge = function (lSite, rSite, va, vb) {
656 | var edge = this.edgeJunkyard.pop();
657 | if (!edge) {
658 | edge = new this.Edge(lSite, rSite);
659 | }
660 | else {
661 | edge.lSite = lSite;
662 | edge.rSite = rSite;
663 | edge.va = edge.vb = null;
664 | }
665 |
666 | this.edges.push(edge);
667 | if (va) {
668 | this.setEdgeStartpoint(edge, lSite, rSite, va);
669 | }
670 | if (vb) {
671 | this.setEdgeEndpoint(edge, lSite, rSite, vb);
672 | }
673 | this.cells[lSite.voronoiId].halfedges.push(this.createHalfedge(edge, lSite, rSite));
674 | this.cells[rSite.voronoiId].halfedges.push(this.createHalfedge(edge, rSite, lSite));
675 | return edge;
676 | };
677 |
678 | Voronoi.prototype.createBorderEdge = function (lSite, va, vb) {
679 | var edge = this.edgeJunkyard.pop();
680 | if (!edge) {
681 | edge = new this.Edge(lSite, null);
682 | }
683 | else {
684 | edge.lSite = lSite;
685 | edge.rSite = null;
686 | }
687 | edge.va = va;
688 | edge.vb = vb;
689 | this.edges.push(edge);
690 | return edge;
691 | };
692 |
693 | Voronoi.prototype.setEdgeStartpoint = function (edge, lSite, rSite, vertex) {
694 | if (!edge.va && !edge.vb) {
695 | edge.va = vertex;
696 | edge.lSite = lSite;
697 | edge.rSite = rSite;
698 | }
699 | else if (edge.lSite === rSite) {
700 | edge.vb = vertex;
701 | }
702 | else {
703 | edge.va = vertex;
704 | }
705 | };
706 |
707 | Voronoi.prototype.setEdgeEndpoint = function (edge, lSite, rSite, vertex) {
708 | this.setEdgeStartpoint(edge, rSite, lSite, vertex);
709 | };
710 |
711 | // ---------------------------------------------------------------------------
712 | // Beachline methods
713 |
714 | // rhill 2011-06-07: For some reasons, performance suffers significantly
715 | // when instanciating a literal object instead of an empty ctor
716 | Voronoi.prototype.Beachsection = function () {
717 | };
718 |
719 | // rhill 2011-06-02: A lot of Beachsection instanciations
720 | // occur during the computation of the Voronoi diagram,
721 | // somewhere between the number of sites and twice the
722 | // number of sites, while the number of Beachsections on the
723 | // beachline at any given time is comparatively low. For this
724 | // reason, we reuse already created Beachsections, in order
725 | // to avoid new memory allocation. This resulted in a measurable
726 | // performance gain.
727 |
728 | Voronoi.prototype.createBeachsection = function (site) {
729 | var beachsection = this.beachsectionJunkyard.pop();
730 | if (!beachsection) {
731 | beachsection = new this.Beachsection();
732 | }
733 | beachsection.site = site;
734 | return beachsection;
735 | };
736 |
737 | // calculate the left break point of a particular beach section,
738 | // given a particular sweep line
739 | Voronoi.prototype.leftBreakPoint = function (arc, directrix) {
740 | // http://en.wikipedia.org/wiki/Parabola
741 | // http://en.wikipedia.org/wiki/Quadratic_equation
742 | // h1 = x1,
743 | // k1 = (y1+directrix)/2,
744 | // h2 = x2,
745 | // k2 = (y2+directrix)/2,
746 | // p1 = k1-directrix,
747 | // a1 = 1/(4*p1),
748 | // b1 = -h1/(2*p1),
749 | // c1 = h1*h1/(4*p1)+k1,
750 | // p2 = k2-directrix,
751 | // a2 = 1/(4*p2),
752 | // b2 = -h2/(2*p2),
753 | // c2 = h2*h2/(4*p2)+k2,
754 | // x = (-(b2-b1) + Math.sqrt((b2-b1)*(b2-b1) - 4*(a2-a1)*(c2-c1))) / (2*(a2-a1))
755 | // When x1 become the x-origin:
756 | // h1 = 0,
757 | // k1 = (y1+directrix)/2,
758 | // h2 = x2-x1,
759 | // k2 = (y2+directrix)/2,
760 | // p1 = k1-directrix,
761 | // a1 = 1/(4*p1),
762 | // b1 = 0,
763 | // c1 = k1,
764 | // p2 = k2-directrix,
765 | // a2 = 1/(4*p2),
766 | // b2 = -h2/(2*p2),
767 | // c2 = h2*h2/(4*p2)+k2,
768 | // x = (-b2 + Math.sqrt(b2*b2 - 4*(a2-a1)*(c2-k1))) / (2*(a2-a1)) + x1
769 |
770 | // change code below at your own risk: care has been taken to
771 | // reduce errors due to computers' finite arithmetic precision.
772 | // Maybe can still be improved, will see if any more of this
773 | // kind of errors pop up again.
774 | var site = arc.site,
775 | rfocx = site.x,
776 | rfocy = site.y,
777 | pby2 = rfocy - directrix;
778 | // parabola in degenerate case where focus is on directrix
779 | if (!pby2) {
780 | return rfocx;
781 | }
782 | var lArc = arc.rbPrevious;
783 | if (!lArc) {
784 | return -Infinity;
785 | }
786 | site = lArc.site;
787 | var lfocx = site.x,
788 | lfocy = site.y,
789 | plby2 = lfocy - directrix;
790 | // parabola in degenerate case where focus is on directrix
791 | if (!plby2) {
792 | return lfocx;
793 | }
794 | var hl = lfocx - rfocx,
795 | aby2 = 1 / pby2 - 1 / plby2,
796 | b = hl / plby2;
797 | if (aby2) {
798 | return (-b + this.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx;
799 | }
800 | // both parabolas have same distance to directrix, thus break point is midway
801 | return (rfocx + lfocx) / 2;
802 | };
803 |
804 | // calculate the right break point of a particular beach section,
805 | // given a particular directrix
806 | Voronoi.prototype.rightBreakPoint = function (arc, directrix) {
807 | var rArc = arc.rbNext;
808 | if (rArc) {
809 | return this.leftBreakPoint(rArc, directrix);
810 | }
811 | var site = arc.site;
812 | return site.y === directrix ? site.x : Infinity;
813 | };
814 |
815 | Voronoi.prototype.detachBeachsection = function (beachsection) {
816 | this.detachCircleEvent(beachsection); // detach potentially attached circle event
817 | this.beachline.rbRemoveNode(beachsection); // remove from RB-tree
818 | this.beachsectionJunkyard.push(beachsection); // mark for reuse
819 | };
820 |
821 | Voronoi.prototype.removeBeachsection = function (beachsection) {
822 | var circle = beachsection.circleEvent,
823 | x = circle.x,
824 | y = circle.ycenter,
825 | vertex = this.createVertex(x, y),
826 | previous = beachsection.rbPrevious,
827 | next = beachsection.rbNext,
828 | disappearingTransitions = [beachsection],
829 | abs_fn = Math.abs;
830 |
831 | // remove collapsed beachsection from beachline
832 | this.detachBeachsection(beachsection);
833 |
834 | // there could be more than one empty arc at the deletion point, this
835 | // happens when more than two edges are linked by the same vertex,
836 | // so we will collect all those edges by looking up both sides of
837 | // the deletion point.
838 | // by the way, there is *always* a predecessor/successor to any collapsed
839 | // beach section, it's just impossible to have a collapsing first/last
840 | // beach sections on the beachline, since they obviously are unconstrained
841 | // on their left/right side.
842 |
843 | // look left
844 | var lArc = previous;
845 | while (lArc.circleEvent && abs_fn(x - lArc.circleEvent.x) < 1e-9 && abs_fn(y - lArc.circleEvent.ycenter) < 1e-9) {
846 | previous = lArc.rbPrevious;
847 | disappearingTransitions.unshift(lArc);
848 | this.detachBeachsection(lArc); // mark for reuse
849 | lArc = previous;
850 | }
851 | // even though it is not disappearing, I will also add the beach section
852 | // immediately to the left of the left-most collapsed beach section, for
853 | // convenience, since we need to refer to it later as this beach section
854 | // is the 'left' site of an edge for which a start point is set.
855 | disappearingTransitions.unshift(lArc);
856 | this.detachCircleEvent(lArc);
857 |
858 | // look right
859 | var rArc = next;
860 | while (rArc.circleEvent && abs_fn(x - rArc.circleEvent.x) < 1e-9 && abs_fn(y - rArc.circleEvent.ycenter) < 1e-9) {
861 | next = rArc.rbNext;
862 | disappearingTransitions.push(rArc);
863 | this.detachBeachsection(rArc); // mark for reuse
864 | rArc = next;
865 | }
866 | // we also have to add the beach section immediately to the right of the
867 | // right-most collapsed beach section, since there is also a disappearing
868 | // transition representing an edge's start point on its left.
869 | disappearingTransitions.push(rArc);
870 | this.detachCircleEvent(rArc);
871 |
872 | // walk through all the disappearing transitions between beach sections and
873 | // set the start point of their (implied) edge.
874 | var nArcs = disappearingTransitions.length,
875 | iArc;
876 | for (iArc = 1; iArc < nArcs; iArc++) {
877 | rArc = disappearingTransitions[iArc];
878 | lArc = disappearingTransitions[iArc - 1];
879 | this.setEdgeStartpoint(rArc.edge, lArc.site, rArc.site, vertex);
880 | }
881 |
882 | // create a new edge as we have now a new transition between
883 | // two beach sections which were previously not adjacent.
884 | // since this edge appears as a new vertex is defined, the vertex
885 | // actually define an end point of the edge (relative to the site
886 | // on the left)
887 | lArc = disappearingTransitions[0];
888 | rArc = disappearingTransitions[nArcs - 1];
889 | rArc.edge = this.createEdge(lArc.site, rArc.site, undefined, vertex);
890 |
891 | // create circle events if any for beach sections left in the beachline
892 | // adjacent to collapsed sections
893 | this.attachCircleEvent(lArc);
894 | this.attachCircleEvent(rArc);
895 | };
896 |
897 | Voronoi.prototype.addBeachsection = function (site) {
898 | var x = site.x,
899 | directrix = site.y;
900 |
901 | // find the left and right beach sections which will surround the newly
902 | // created beach section.
903 | // rhill 2011-06-01: This loop is one of the most often executed,
904 | // hence we expand in-place the comparison-against-epsilon calls.
905 | var lArc, rArc,
906 | dxl, dxr,
907 | node = this.beachline.root;
908 |
909 | while (node) {
910 | dxl = this.leftBreakPoint(node, directrix) - x;
911 | // x lessThanWithEpsilon xl => falls somewhere before the left edge of the beachsection
912 | if (dxl > 1e-9) {
913 | // this case should never happen
914 | // if (!node.rbLeft) {
915 | // rArc = node.rbLeft;
916 | // break;
917 | // }
918 | node = node.rbLeft;
919 | }
920 | else {
921 | dxr = x - this.rightBreakPoint(node, directrix);
922 | // x greaterThanWithEpsilon xr => falls somewhere after the right edge of the beachsection
923 | if (dxr > 1e-9) {
924 | if (!node.rbRight) {
925 | lArc = node;
926 | break;
927 | }
928 | node = node.rbRight;
929 | }
930 | else {
931 | // x equalWithEpsilon xl => falls exactly on the left edge of the beachsection
932 | if (dxl > -1e-9) {
933 | lArc = node.rbPrevious;
934 | rArc = node;
935 | }
936 | // x equalWithEpsilon xr => falls exactly on the right edge of the beachsection
937 | else if (dxr > -1e-9) {
938 | lArc = node;
939 | rArc = node.rbNext;
940 | }
941 | // falls exactly somewhere in the middle of the beachsection
942 | else {
943 | lArc = rArc = node;
944 | }
945 | break;
946 | }
947 | }
948 | }
949 | // at this point, keep in mind that lArc and/or rArc could be
950 | // undefined or null.
951 |
952 | // create a new beach section object for the site and add it to RB-tree
953 | var newArc = this.createBeachsection(site);
954 | this.beachline.rbInsertSuccessor(lArc, newArc);
955 |
956 | // cases:
957 | //
958 |
959 | // [null,null]
960 | // least likely case: new beach section is the first beach section on the
961 | // beachline.
962 | // This case means:
963 | // no new transition appears
964 | // no collapsing beach section
965 | // new beachsection become root of the RB-tree
966 | if (!lArc && !rArc) {
967 | return;
968 | }
969 |
970 | // [lArc,rArc] where lArc == rArc
971 | // most likely case: new beach section split an existing beach
972 | // section.
973 | // This case means:
974 | // one new transition appears
975 | // the left and right beach section might be collapsing as a result
976 | // two new nodes added to the RB-tree
977 | if (lArc === rArc) {
978 | // invalidate circle event of split beach section
979 | this.detachCircleEvent(lArc);
980 |
981 | // split the beach section into two separate beach sections
982 | rArc = this.createBeachsection(lArc.site);
983 | this.beachline.rbInsertSuccessor(newArc, rArc);
984 |
985 | // since we have a new transition between two beach sections,
986 | // a new edge is born
987 | newArc.edge = rArc.edge = this.createEdge(lArc.site, newArc.site);
988 |
989 | // check whether the left and right beach sections are collapsing
990 | // and if so create circle events, to be notified when the point of
991 | // collapse is reached.
992 | this.attachCircleEvent(lArc);
993 | this.attachCircleEvent(rArc);
994 | return;
995 | }
996 |
997 | // [lArc,null]
998 | // even less likely case: new beach section is the *last* beach section
999 | // on the beachline -- this can happen *only* if *all* the previous beach
1000 | // sections currently on the beachline share the same y value as
1001 | // the new beach section.
1002 | // This case means:
1003 | // one new transition appears
1004 | // no collapsing beach section as a result
1005 | // new beach section become right-most node of the RB-tree
1006 | if (lArc && !rArc) {
1007 | newArc.edge = this.createEdge(lArc.site, newArc.site);
1008 | return;
1009 | }
1010 |
1011 | // [null,rArc]
1012 | // impossible case: because sites are strictly processed from top to bottom,
1013 | // and left to right, which guarantees that there will always be a beach section
1014 | // on the left -- except of course when there are no beach section at all on
1015 | // the beach line, which case was handled above.
1016 | // rhill 2011-06-02: No point testing in non-debug version
1017 | //if (!lArc && rArc) {
1018 | // throw "Voronoi.addBeachsection(): What is this I don't even";
1019 | // }
1020 |
1021 | // [lArc,rArc] where lArc != rArc
1022 | // somewhat less likely case: new beach section falls *exactly* in between two
1023 | // existing beach sections
1024 | // This case means:
1025 | // one transition disappears
1026 | // two new transitions appear
1027 | // the left and right beach section might be collapsing as a result
1028 | // only one new node added to the RB-tree
1029 | if (lArc !== rArc) {
1030 | // invalidate circle events of left and right sites
1031 | this.detachCircleEvent(lArc);
1032 | this.detachCircleEvent(rArc);
1033 |
1034 | // an existing transition disappears, meaning a vertex is defined at
1035 | // the disappearance point.
1036 | // since the disappearance is caused by the new beachsection, the
1037 | // vertex is at the center of the circumscribed circle of the left,
1038 | // new and right beachsections.
1039 | // http://mathforum.org/library/drmath/view/55002.html
1040 | // Except that I bring the origin at A to simplify
1041 | // calculation
1042 | var lSite = lArc.site,
1043 | ax = lSite.x,
1044 | ay = lSite.y,
1045 | bx = site.x - ax,
1046 | by = site.y - ay,
1047 | rSite = rArc.site,
1048 | cx = rSite.x - ax,
1049 | cy = rSite.y - ay,
1050 | d = 2 * (bx * cy - by * cx),
1051 | hb = bx * bx + by * by,
1052 | hc = cx * cx + cy * cy,
1053 | vertex = this.createVertex((cy * hb - by * hc) / d + ax, (bx * hc - cx * hb) / d + ay);
1054 |
1055 | // one transition disappear
1056 | this.setEdgeStartpoint(rArc.edge, lSite, rSite, vertex);
1057 |
1058 | // two new transitions appear at the new vertex location
1059 | newArc.edge = this.createEdge(lSite, site, undefined, vertex);
1060 | rArc.edge = this.createEdge(site, rSite, undefined, vertex);
1061 |
1062 | // check whether the left and right beach sections are collapsing
1063 | // and if so create circle events, to handle the point of collapse.
1064 | this.attachCircleEvent(lArc);
1065 | this.attachCircleEvent(rArc);
1066 | return;
1067 | }
1068 | };
1069 |
1070 | // ---------------------------------------------------------------------------
1071 | // Circle event methods
1072 |
1073 | // rhill 2011-06-07: For some reasons, performance suffers significantly
1074 | // when instanciating a literal object instead of an empty ctor
1075 | Voronoi.prototype.CircleEvent = function () {
1076 | // rhill 2013-10-12: it helps to state exactly what we are at ctor time.
1077 | this.arc = null;
1078 | this.rbLeft = null;
1079 | this.rbNext = null;
1080 | this.rbParent = null;
1081 | this.rbPrevious = null;
1082 | this.rbRed = false;
1083 | this.rbRight = null;
1084 | this.site = null;
1085 | this.x = this.y = this.ycenter = 0;
1086 | };
1087 |
1088 | Voronoi.prototype.attachCircleEvent = function (arc) {
1089 | var lArc = arc.rbPrevious,
1090 | rArc = arc.rbNext;
1091 | if (!lArc || !rArc) {
1092 | return;
1093 | } // does that ever happen?
1094 | var lSite = lArc.site,
1095 | cSite = arc.site,
1096 | rSite = rArc.site;
1097 |
1098 | // If site of left beachsection is same as site of
1099 | // right beachsection, there can't be convergence
1100 | if (lSite === rSite) {
1101 | return;
1102 | }
1103 |
1104 | // Find the circumscribed circle for the three sites associated
1105 | // with the beachsection triplet.
1106 | // rhill 2011-05-26: It is more efficient to calculate in-place
1107 | // rather than getting the resulting circumscribed circle from an
1108 | // object returned by calling Voronoi.circumcircle()
1109 | // http://mathforum.org/library/drmath/view/55002.html
1110 | // Except that I bring the origin at cSite to simplify calculations.
1111 | // The bottom-most part of the circumcircle is our Fortune 'circle
1112 | // event', and its center is a vertex potentially part of the final
1113 | // Voronoi diagram.
1114 | var bx = cSite.x,
1115 | by = cSite.y,
1116 | ax = lSite.x - bx,
1117 | ay = lSite.y - by,
1118 | cx = rSite.x - bx,
1119 | cy = rSite.y - by;
1120 |
1121 | // If points l->c->r are clockwise, then center beach section does not
1122 | // collapse, hence it can't end up as a vertex (we reuse 'd' here, which
1123 | // sign is reverse of the orientation, hence we reverse the test.
1124 | // http://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon
1125 | // rhill 2011-05-21: Nasty finite precision error which caused circumcircle() to
1126 | // return infinites: 1e-12 seems to fix the problem.
1127 | var d = 2 * (ax * cy - ay * cx);
1128 | if (d >= -2e-12) {
1129 | return;
1130 | }
1131 |
1132 | var ha = ax * ax + ay * ay,
1133 | hc = cx * cx + cy * cy,
1134 | x = (cy * ha - ay * hc) / d,
1135 | y = (ax * hc - cx * ha) / d,
1136 | ycenter = y + by;
1137 |
1138 | // Important: ybottom should always be under or at sweep, so no need
1139 | // to waste CPU cycles by checking
1140 |
1141 | // recycle circle event object if possible
1142 | var circleEvent = this.circleEventJunkyard.pop();
1143 | if (!circleEvent) {
1144 | circleEvent = new this.CircleEvent();
1145 | }
1146 | circleEvent.arc = arc;
1147 | circleEvent.site = cSite;
1148 | circleEvent.x = x + bx;
1149 | circleEvent.y = ycenter + this.sqrt(x * x + y * y); // y bottom
1150 | circleEvent.ycenter = ycenter;
1151 | arc.circleEvent = circleEvent;
1152 |
1153 | // find insertion point in RB-tree: circle events are ordered from
1154 | // smallest to largest
1155 | var predecessor = null,
1156 | node = this.circleEvents.root;
1157 | while (node) {
1158 | if (circleEvent.y < node.y || (circleEvent.y === node.y && circleEvent.x <= node.x)) {
1159 | if (node.rbLeft) {
1160 | node = node.rbLeft;
1161 | }
1162 | else {
1163 | predecessor = node.rbPrevious;
1164 | break;
1165 | }
1166 | }
1167 | else {
1168 | if (node.rbRight) {
1169 | node = node.rbRight;
1170 | }
1171 | else {
1172 | predecessor = node;
1173 | break;
1174 | }
1175 | }
1176 | }
1177 | this.circleEvents.rbInsertSuccessor(predecessor, circleEvent);
1178 | if (!predecessor) {
1179 | this.firstCircleEvent = circleEvent;
1180 | }
1181 | };
1182 |
1183 | Voronoi.prototype.detachCircleEvent = function (arc) {
1184 | var circleEvent = arc.circleEvent;
1185 | if (circleEvent) {
1186 | if (!circleEvent.rbPrevious) {
1187 | this.firstCircleEvent = circleEvent.rbNext;
1188 | }
1189 | this.circleEvents.rbRemoveNode(circleEvent); // remove from RB-tree
1190 | this.circleEventJunkyard.push(circleEvent);
1191 | arc.circleEvent = null;
1192 | }
1193 | };
1194 |
1195 | // ---------------------------------------------------------------------------
1196 | // Diagram completion methods
1197 |
1198 | // connect dangling edges (not if a cursory test tells us
1199 | // it is not going to be visible.
1200 | // return value:
1201 | // false: the dangling endpoint couldn't be connected
1202 | // true: the dangling endpoint could be connected
1203 | Voronoi.prototype.connectEdge = function (edge, bbox) {
1204 | // skip if end point already connected
1205 | var vb = edge.vb;
1206 | if (!!vb) {
1207 | return true;
1208 | }
1209 |
1210 | // make local copy for performance purpose
1211 | var va = edge.va,
1212 | xl = bbox.xl,
1213 | xr = bbox.xr,
1214 | yt = bbox.yt,
1215 | yb = bbox.yb,
1216 | lSite = edge.lSite,
1217 | rSite = edge.rSite,
1218 | lx = lSite.x,
1219 | ly = lSite.y,
1220 | rx = rSite.x,
1221 | ry = rSite.y,
1222 | fx = (lx + rx) / 2,
1223 | fy = (ly + ry) / 2,
1224 | fm, fb;
1225 |
1226 | // if we reach here, this means cells which use this edge will need
1227 | // to be closed, whether because the edge was removed, or because it
1228 | // was connected to the bounding box.
1229 | this.cells[lSite.voronoiId].closeMe = true;
1230 | this.cells[rSite.voronoiId].closeMe = true;
1231 |
1232 | // get the line equation of the bisector if line is not vertical
1233 | if (ry !== ly) {
1234 | fm = (lx - rx) / (ry - ly);
1235 | fb = fy - fm * fx;
1236 | }
1237 |
1238 | // remember, direction of line (relative to left site):
1239 | // upward: left.x < right.x
1240 | // downward: left.x > right.x
1241 | // horizontal: left.x == right.x
1242 | // upward: left.x < right.x
1243 | // rightward: left.y < right.y
1244 | // leftward: left.y > right.y
1245 | // vertical: left.y == right.y
1246 |
1247 | // depending on the direction, find the best side of the
1248 | // bounding box to use to determine a reasonable start point
1249 |
1250 | // rhill 2013-12-02:
1251 | // While at it, since we have the values which define the line,
1252 | // clip the end of va if it is outside the bbox.
1253 | // https://github.com/gorhill/Javascript-Voronoi/issues/15
1254 | // TODO: Do all the clipping here rather than rely on Liang-Barsky
1255 | // which does not do well sometimes due to loss of arithmetic
1256 | // precision. The code here doesn't degrade if one of the vertex is
1257 | // at a huge distance.
1258 |
1259 | // special case: vertical line
1260 | if (fm === undefined) {
1261 | // doesn't intersect with viewport
1262 | if (fx < xl || fx >= xr) {
1263 | return false;
1264 | }
1265 | // downward
1266 | if (lx > rx) {
1267 | if (!va || va.y < yt) {
1268 | va = this.createVertex(fx, yt);
1269 | }
1270 | else if (va.y >= yb) {
1271 | return false;
1272 | }
1273 | vb = this.createVertex(fx, yb);
1274 | }
1275 | // upward
1276 | else {
1277 | if (!va || va.y > yb) {
1278 | va = this.createVertex(fx, yb);
1279 | }
1280 | else if (va.y < yt) {
1281 | return false;
1282 | }
1283 | vb = this.createVertex(fx, yt);
1284 | }
1285 | }
1286 | // closer to vertical than horizontal, connect start point to the
1287 | // top or bottom side of the bounding box
1288 | else if (fm < -1 || fm > 1) {
1289 | // downward
1290 | if (lx > rx) {
1291 | if (!va || va.y < yt) {
1292 | va = this.createVertex((yt - fb) / fm, yt);
1293 | }
1294 | else if (va.y >= yb) {
1295 | return false;
1296 | }
1297 | vb = this.createVertex((yb - fb) / fm, yb);
1298 | }
1299 | // upward
1300 | else {
1301 | if (!va || va.y > yb) {
1302 | va = this.createVertex((yb - fb) / fm, yb);
1303 | }
1304 | else if (va.y < yt) {
1305 | return false;
1306 | }
1307 | vb = this.createVertex((yt - fb) / fm, yt);
1308 | }
1309 | }
1310 | // closer to horizontal than vertical, connect start point to the
1311 | // left or right side of the bounding box
1312 | else {
1313 | // rightward
1314 | if (ly < ry) {
1315 | if (!va || va.x < xl) {
1316 | va = this.createVertex(xl, fm * xl + fb);
1317 | }
1318 | else if (va.x >= xr) {
1319 | return false;
1320 | }
1321 | vb = this.createVertex(xr, fm * xr + fb);
1322 | }
1323 | // leftward
1324 | else {
1325 | if (!va || va.x > xr) {
1326 | va = this.createVertex(xr, fm * xr + fb);
1327 | }
1328 | else if (va.x < xl) {
1329 | return false;
1330 | }
1331 | vb = this.createVertex(xl, fm * xl + fb);
1332 | }
1333 | }
1334 | edge.va = va;
1335 | edge.vb = vb;
1336 |
1337 | return true;
1338 | };
1339 |
1340 | // line-clipping code taken from:
1341 | // Liang-Barsky function by Daniel White
1342 | // http://www.skytopia.com/project/articles/compsci/clipping.html
1343 | // Thanks!
1344 | // A bit modified to minimize code paths
1345 | Voronoi.prototype.clipEdge = function (edge, bbox) {
1346 | var ax = edge.va.x,
1347 | ay = edge.va.y,
1348 | bx = edge.vb.x,
1349 | by = edge.vb.y,
1350 | t0 = 0,
1351 | t1 = 1,
1352 | dx = bx - ax,
1353 | dy = by - ay;
1354 | // left
1355 | var q = ax - bbox.xl;
1356 | if (dx === 0 && q < 0) {
1357 | return false;
1358 | }
1359 | var r = -q / dx;
1360 | if (dx < 0) {
1361 | if (r < t0) {
1362 | return false;
1363 | }
1364 | if (r < t1) {
1365 | t1 = r;
1366 | }
1367 | }
1368 | else if (dx > 0) {
1369 | if (r > t1) {
1370 | return false;
1371 | }
1372 | if (r > t0) {
1373 | t0 = r;
1374 | }
1375 | }
1376 | // right
1377 | q = bbox.xr - ax;
1378 | if (dx === 0 && q < 0) {
1379 | return false;
1380 | }
1381 | r = q / dx;
1382 | if (dx < 0) {
1383 | if (r > t1) {
1384 | return false;
1385 | }
1386 | if (r > t0) {
1387 | t0 = r;
1388 | }
1389 | }
1390 | else if (dx > 0) {
1391 | if (r < t0) {
1392 | return false;
1393 | }
1394 | if (r < t1) {
1395 | t1 = r;
1396 | }
1397 | }
1398 | // top
1399 | q = ay - bbox.yt;
1400 | if (dy === 0 && q < 0) {
1401 | return false;
1402 | }
1403 | r = -q / dy;
1404 | if (dy < 0) {
1405 | if (r < t0) {
1406 | return false;
1407 | }
1408 | if (r < t1) {
1409 | t1 = r;
1410 | }
1411 | }
1412 | else if (dy > 0) {
1413 | if (r > t1) {
1414 | return false;
1415 | }
1416 | if (r > t0) {
1417 | t0 = r;
1418 | }
1419 | }
1420 | // bottom
1421 | q = bbox.yb - ay;
1422 | if (dy === 0 && q < 0) {
1423 | return false;
1424 | }
1425 | r = q / dy;
1426 | if (dy < 0) {
1427 | if (r > t1) {
1428 | return false;
1429 | }
1430 | if (r > t0) {
1431 | t0 = r;
1432 | }
1433 | }
1434 | else if (dy > 0) {
1435 | if (r < t0) {
1436 | return false;
1437 | }
1438 | if (r < t1) {
1439 | t1 = r;
1440 | }
1441 | }
1442 |
1443 | // if we reach this point, Voronoi edge is within bbox
1444 |
1445 | // if t0 > 0, va needs to change
1446 | // rhill 2011-06-03: we need to create a new vertex rather
1447 | // than modifying the existing one, since the existing
1448 | // one is likely shared with at least another edge
1449 | if (t0 > 0) {
1450 | edge.va = this.createVertex(ax + t0 * dx, ay + t0 * dy);
1451 | }
1452 |
1453 | // if t1 < 1, vb needs to change
1454 | // rhill 2011-06-03: we need to create a new vertex rather
1455 | // than modifying the existing one, since the existing
1456 | // one is likely shared with at least another edge
1457 | if (t1 < 1) {
1458 | edge.vb = this.createVertex(ax + t1 * dx, ay + t1 * dy);
1459 | }
1460 |
1461 | // va and/or vb were clipped, thus we will need to close
1462 | // cells which use this edge.
1463 | if (t0 > 0 || t1 < 1) {
1464 | this.cells[edge.lSite.voronoiId].closeMe = true;
1465 | this.cells[edge.rSite.voronoiId].closeMe = true;
1466 | }
1467 |
1468 | return true;
1469 | };
1470 |
1471 | // Connect/cut edges at bounding box
1472 | Voronoi.prototype.clipEdges = function (bbox) {
1473 | // connect all dangling edges to bounding box
1474 | // or get rid of them if it can't be done
1475 | var edges = this.edges,
1476 | iEdge = edges.length,
1477 | edge,
1478 | abs_fn = Math.abs;
1479 |
1480 | // iterate backward so we can splice safely
1481 | while (iEdge--) {
1482 | edge = edges[iEdge];
1483 | // edge is removed if:
1484 | // it is wholly outside the bounding box
1485 | // it is looking more like a point than a line
1486 | if (!this.connectEdge(edge, bbox) || !this.clipEdge(edge, bbox) ||
1487 | (abs_fn(edge.va.x - edge.vb.x) < 1e-9 && abs_fn(edge.va.y - edge.vb.y) < 1e-9)) {
1488 | edge.va = edge.vb = null;
1489 | edges.splice(iEdge, 1);
1490 | }
1491 | }
1492 | };
1493 |
1494 | // Close the cells.
1495 | // The cells are bound by the supplied bounding box.
1496 | // Each cell refers to its associated site, and a list
1497 | // of halfedges ordered counterclockwise.
1498 | Voronoi.prototype.closeCells = function (bbox) {
1499 | var xl = bbox.xl,
1500 | xr = bbox.xr,
1501 | yt = bbox.yt,
1502 | yb = bbox.yb,
1503 | cells = this.cells,
1504 | iCell = cells.length,
1505 | cell,
1506 | iLeft,
1507 | halfedges, nHalfedges,
1508 | edge,
1509 | va, vb, vz,
1510 | lastBorderSegment,
1511 | abs_fn = Math.abs;
1512 |
1513 | while (iCell--) {
1514 | cell = cells[iCell];
1515 | // prune, order halfedges counterclockwise, then add missing ones
1516 | // required to close cells
1517 | if (!cell.prepareHalfedges()) {
1518 | continue;
1519 | }
1520 | if (!cell.closeMe) {
1521 | continue;
1522 | }
1523 | // find first 'unclosed' point.
1524 | // an 'unclosed' point will be the end point of a halfedge which
1525 | // does not match the start point of the following halfedge
1526 | halfedges = cell.halfedges;
1527 | nHalfedges = halfedges.length;
1528 | // special case: only one site, in which case, the viewport is the cell
1529 | // ...
1530 |
1531 | // all other cases
1532 | iLeft = 0;
1533 | while (iLeft < nHalfedges) {
1534 | va = halfedges[iLeft].getEndpoint();
1535 | vz = halfedges[(iLeft + 1) % nHalfedges].getStartpoint();
1536 | // if end point is not equal to start point, we need to add the missing
1537 | // halfedge(s) up to vz
1538 | if (abs_fn(va.x - vz.x) >= 1e-9 || abs_fn(va.y - vz.y) >= 1e-9) {
1539 |
1540 | // rhill 2013-12-02:
1541 | // "Holes" in the halfedges are not necessarily always adjacent.
1542 | // https://github.com/gorhill/Javascript-Voronoi/issues/16
1543 |
1544 | // find entry point:
1545 | switch (true) {
1546 |
1547 | // walk downward along left side
1548 | case this.equalWithEpsilon(va.x, xl) && this.lessThanWithEpsilon(va.y, yb):
1549 | lastBorderSegment = this.equalWithEpsilon(vz.x, xl);
1550 | vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb);
1551 | edge = this.createBorderEdge(cell.site, va, vb);
1552 | iLeft++;
1553 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1554 | nHalfedges++;
1555 | if (lastBorderSegment) {
1556 | break;
1557 | }
1558 | va = vb;
1559 | // fall through
1560 |
1561 | // walk rightward along bottom side
1562 | case this.equalWithEpsilon(va.y, yb) && this.lessThanWithEpsilon(va.x, xr):
1563 | lastBorderSegment = this.equalWithEpsilon(vz.y, yb);
1564 | vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb);
1565 | edge = this.createBorderEdge(cell.site, va, vb);
1566 | iLeft++;
1567 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1568 | nHalfedges++;
1569 | if (lastBorderSegment) {
1570 | break;
1571 | }
1572 | va = vb;
1573 | // fall through
1574 |
1575 | // walk upward along right side
1576 | case this.equalWithEpsilon(va.x, xr) && this.greaterThanWithEpsilon(va.y, yt):
1577 | lastBorderSegment = this.equalWithEpsilon(vz.x, xr);
1578 | vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt);
1579 | edge = this.createBorderEdge(cell.site, va, vb);
1580 | iLeft++;
1581 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1582 | nHalfedges++;
1583 | if (lastBorderSegment) {
1584 | break;
1585 | }
1586 | va = vb;
1587 | // fall through
1588 |
1589 | // walk leftward along top side
1590 | case this.equalWithEpsilon(va.y, yt) && this.greaterThanWithEpsilon(va.x, xl):
1591 | lastBorderSegment = this.equalWithEpsilon(vz.y, yt);
1592 | vb = this.createVertex(lastBorderSegment ? vz.x : xl, yt);
1593 | edge = this.createBorderEdge(cell.site, va, vb);
1594 | iLeft++;
1595 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1596 | nHalfedges++;
1597 | if (lastBorderSegment) {
1598 | break;
1599 | }
1600 | va = vb;
1601 | // fall through
1602 |
1603 | // walk downward along left side
1604 | lastBorderSegment = this.equalWithEpsilon(vz.x, xl);
1605 | vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb);
1606 | edge = this.createBorderEdge(cell.site, va, vb);
1607 | iLeft++;
1608 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1609 | nHalfedges++;
1610 | if (lastBorderSegment) {
1611 | break;
1612 | }
1613 | va = vb;
1614 | // fall through
1615 |
1616 | // walk rightward along bottom side
1617 | lastBorderSegment = this.equalWithEpsilon(vz.y, yb);
1618 | vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb);
1619 | edge = this.createBorderEdge(cell.site, va, vb);
1620 | iLeft++;
1621 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1622 | nHalfedges++;
1623 | if (lastBorderSegment) {
1624 | break;
1625 | }
1626 | va = vb;
1627 | // fall through
1628 |
1629 | // walk upward along right side
1630 | lastBorderSegment = this.equalWithEpsilon(vz.x, xr);
1631 | vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt);
1632 | edge = this.createBorderEdge(cell.site, va, vb);
1633 | iLeft++;
1634 | halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null));
1635 | nHalfedges++;
1636 | if (lastBorderSegment) {
1637 | break;
1638 | }
1639 | // fall through
1640 |
1641 | default:
1642 | throw "Voronoi.closeCells() > this makes no sense!";
1643 | }
1644 | }
1645 | iLeft++;
1646 | }
1647 | cell.closeMe = false;
1648 | }
1649 | };
1650 |
1651 | // ---------------------------------------------------------------------------
1652 | // Debugging helper
1653 | /*
1654 | Voronoi.prototype.dumpBeachline = function(y) {
1655 | console.log('Voronoi.dumpBeachline(%f) > Beachsections, from left to right:', y);
1656 | if ( !this.beachline ) {
1657 | console.log(' None');
1658 | }
1659 | else {
1660 | var bs = this.beachline.getFirst(this.beachline.root);
1661 | while ( bs ) {
1662 | console.log(' site %d: xl: %f, xr: %f', bs.site.voronoiId, this.leftBreakPoint(bs, y), this.rightBreakPoint(bs, y));
1663 | bs = bs.rbNext;
1664 | }
1665 | }
1666 | };
1667 | */
1668 |
1669 | // ---------------------------------------------------------------------------
1670 | // Helper: Quantize sites
1671 |
1672 | // rhill 2013-10-12:
1673 | // This is to solve https://github.com/gorhill/Javascript-Voronoi/issues/15
1674 | // Since not all users will end up using the kind of coord values which would
1675 | // cause the issue to arise, I chose to let the user decide whether or not
1676 | // he should sanitize his coord values through this helper. This way, for
1677 | // those users who uses coord values which are known to be fine, no overhead is
1678 | // added.
1679 |
1680 | Voronoi.prototype.quantizeSites = function (sites) {
1681 | var ε = this.ε,
1682 | n = sites.length,
1683 | site;
1684 | while (n--) {
1685 | site = sites[n];
1686 | site.x = Math.floor(site.x / ε) * ε;
1687 | site.y = Math.floor(site.y / ε) * ε;
1688 | }
1689 | };
1690 |
1691 | // ---------------------------------------------------------------------------
1692 | // Helper: Recycle diagram: all vertex, edge and cell objects are
1693 | // "surrendered" to the Voronoi object for reuse.
1694 | // TODO: rhill-voronoi-core v2: more performance to be gained
1695 | // when I change the semantic of what is returned.
1696 |
1697 | Voronoi.prototype.recycle = function (diagram) {
1698 | if (diagram) {
1699 | if (diagram instanceof this.Diagram) {
1700 | this.toRecycle = diagram;
1701 | }
1702 | else {
1703 | throw 'Voronoi.recycleDiagram() > Need a Diagram object.';
1704 | }
1705 | }
1706 | };
1707 |
1708 | // ---------------------------------------------------------------------------
1709 | // Top-level Fortune loop
1710 |
1711 | // rhill 2011-05-19:
1712 | // Voronoi sites are kept client-side now, to allow
1713 | // user to freely modify content. At compute time,
1714 | // *references* to sites are copied locally.
1715 |
1716 | Voronoi.prototype.compute = function (sites, bbox) {
1717 | // to measure execution time
1718 | var startTime = new Date();
1719 |
1720 | // init internal state
1721 | this.reset();
1722 |
1723 | // any diagram data available for recycling?
1724 | // I do that here so that this is included in execution time
1725 | if (this.toRecycle) {
1726 | this.vertexJunkyard = this.vertexJunkyard.concat(this.toRecycle.vertices);
1727 | this.edgeJunkyard = this.edgeJunkyard.concat(this.toRecycle.edges);
1728 | this.cellJunkyard = this.cellJunkyard.concat(this.toRecycle.cells);
1729 | this.toRecycle = null;
1730 | }
1731 |
1732 | // Initialize site event queue
1733 | var siteEvents = sites.slice(0);
1734 | siteEvents.sort(function (a, b) {
1735 | var r = b.y - a.y;
1736 | if (r) {
1737 | return r;
1738 | }
1739 | return b.x - a.x;
1740 | });
1741 |
1742 | // process queue
1743 | var site = siteEvents.pop(),
1744 | siteid = 0,
1745 | xsitex, // to avoid duplicate sites
1746 | xsitey,
1747 | cells = this.cells,
1748 | circle;
1749 |
1750 | // main loop
1751 | for (; ;) {
1752 | // we need to figure whether we handle a site or circle event
1753 | // for this we find out if there is a site event and it is
1754 | // 'earlier' than the circle event
1755 | circle = this.firstCircleEvent;
1756 |
1757 | // add beach section
1758 | if (site && (!circle || site.y < circle.y || (site.y === circle.y && site.x < circle.x))) {
1759 | // only if site is not a duplicate
1760 | if (site.x !== xsitex || site.y !== xsitey) {
1761 | // first create cell for new site
1762 | cells[siteid] = this.createCell(site);
1763 | site.voronoiId = siteid++;
1764 | // then create a beachsection for that site
1765 | this.addBeachsection(site);
1766 | // remember last site coords to detect duplicate
1767 | xsitey = site.y;
1768 | xsitex = site.x;
1769 | }
1770 | site = siteEvents.pop();
1771 | }
1772 |
1773 | // remove beach section
1774 | else if (circle) {
1775 | this.removeBeachsection(circle.arc);
1776 | }
1777 |
1778 | // all done, quit
1779 | else {
1780 | break;
1781 | }
1782 | }
1783 |
1784 | // wrapping-up:
1785 | // connect dangling edges to bounding box
1786 | // cut edges as per bounding box
1787 | // discard edges completely outside bounding box
1788 | // discard edges which are point-like
1789 | this.clipEdges(bbox);
1790 |
1791 | // add missing edges in order to close opened cells
1792 | this.closeCells(bbox);
1793 |
1794 | // to measure execution time
1795 | var stopTime = new Date();
1796 |
1797 | // prepare return values
1798 | var diagram = new this.Diagram();
1799 | diagram.cells = this.cells;
1800 | diagram.edges = this.edges;
1801 | diagram.vertices = this.vertices;
1802 | diagram.execTime = stopTime.getTime() - startTime.getTime();
1803 |
1804 | // clean up
1805 | this.reset();
1806 |
1807 | return diagram;
1808 | };
1809 |
--------------------------------------------------------------------------------
/simplify.js:
--------------------------------------------------------------------------------
1 | /*
2 | (c) 2013, Vladimir Agafonkin
3 | Simplify.js, a high-performance JS polyline simplification library
4 | mourner.github.io/simplify-js
5 | */
6 |
7 | (function () {
8 | "use strict";
9 |
10 | // to suit your point format, run search/replace for '.x' and '.y';
11 | // for 3D version, see 3d branch
12 |
13 | function getX(p) {
14 | return p.x;
15 | }
16 |
17 | function getY(p) {
18 | return p.y;
19 | }
20 |
21 | // square distance between 2 points
22 | function getSqDist(p1, p2) {
23 |
24 | var dx = getX(p1) - getX(p2),
25 | dy = getY(p1) - getY(p2);
26 |
27 | return dx * dx + dy * dy;
28 | }
29 |
30 | // square distance from a point to a segment
31 | function getSqSegDist(p, p1, p2) {
32 |
33 | var x = getX(p1),
34 | y = getY(p1),
35 | dx = getX(p2) - x,
36 | dy = getY(p2) - y;
37 |
38 | if (dx !== 0 || dy !== 0) {
39 |
40 | var t = ((getX(p) - x) * dx + (getY(p) - y) * dy) / (dx * dx + dy * dy);
41 |
42 | if (t > 1) {
43 | x = getX(p2);
44 | y = getY(p2);
45 |
46 | } else if (t > 0) {
47 | x += dx * t;
48 | y += dy * t;
49 | }
50 | }
51 |
52 | dx = getX(p) - x;
53 | dy = getY(p) - y;
54 |
55 | return dx * dx + dy * dy;
56 | }
57 |
58 | // rest of the code doesn't care about point format
59 |
60 | // basic distance-based simplification
61 | function simplifyRadialDist(points, sqTolerance) {
62 |
63 | var prevPoint = points[0],
64 | newPoints = [prevPoint],
65 | point;
66 |
67 | for (var i = 1, len = points.length; i < len; i++) {
68 | point = points[i];
69 |
70 | if (getSqDist(point, prevPoint) > sqTolerance) {
71 | newPoints.push(point);
72 | prevPoint = point;
73 | }
74 | }
75 |
76 | if (prevPoint !== point) {
77 | newPoints.push(point);
78 | }
79 |
80 | return newPoints;
81 | }
82 |
83 | // simplification using optimized Douglas-Peucker algorithm with recursion elimination
84 | function simplifyDouglasPeucker(points, sqTolerance) {
85 |
86 | var len = points.length,
87 | MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array,
88 | markers = new MarkerArray(len),
89 | first = 0,
90 | last = len - 1,
91 | stack = [],
92 | newPoints = [],
93 | i, maxSqDist, sqDist, index;
94 |
95 | markers[first] = markers[last] = 1;
96 |
97 | while (last) {
98 |
99 | maxSqDist = 0;
100 |
101 | for (i = first + 1; i < last; i++) {
102 | sqDist = getSqSegDist(points[i], points[first], points[last]);
103 |
104 | if (sqDist > maxSqDist) {
105 | index = i;
106 | maxSqDist = sqDist;
107 | }
108 | }
109 |
110 | if (maxSqDist > sqTolerance) {
111 | markers[index] = 1;
112 | stack.push(first, index, index, last);
113 | }
114 |
115 | last = stack.pop();
116 | first = stack.pop();
117 | }
118 |
119 | for (i = 0; i < len; i++) {
120 | if (markers[i]) {
121 | newPoints.push(points[i]);
122 | }
123 | }
124 |
125 | return newPoints;
126 | }
127 |
128 | // both algorithms combined for awesome performance
129 | function simplify(points, tolerance, highestQuality) {
130 |
131 | var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;
132 |
133 | points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
134 | points = simplifyDouglasPeucker(points, sqTolerance);
135 |
136 | return points;
137 | }
138 |
139 | // export as AMD module / Node module / browser variable
140 | if (typeof define === 'function' && define.amd) {
141 | define(function () {
142 | return simplify;
143 | });
144 | } else if (typeof module !== 'undefined') {
145 | module.exports = simplify;
146 | } else {
147 | window.simplify = simplify;
148 | }
149 |
150 | })();
--------------------------------------------------------------------------------
/solver.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var solver = (function () {
3 | function det2x2(a, b, c, d) {
4 | var C = Math.pow(2, 27) + 1;
5 |
6 | function veltkamp(val) {
7 | var p = val * C;
8 | var q = val - p;
9 | var val1 = p + q;
10 | return [val1, val - val1];
11 | }
12 |
13 | function mult(a, b) {
14 | var va = veltkamp(a);
15 | var vb = veltkamp(b);
16 | var r1 = a * b;
17 | var t1 = -r1 + va[0] * vb[0];
18 | var t2 = t1 + va[0] * vb[1];
19 | var t3 = t2 + va[1] * vb[0];
20 | var r2 = t3 + va[1] * vb[1];
21 | return [r1, r2];
22 | }
23 |
24 | function twoDiff(a, b) {
25 | var s = a - b;
26 | var bb = s - a;
27 | var err = (a - (s - bb)) - (b + bb);
28 | return [s, err];
29 | }
30 |
31 | function quickTwoSum(a, b) {
32 | var res = a + b;
33 | var e = b - (res - a);
34 | return [res, e];
35 | }
36 |
37 | function subtraction(a, b) {
38 | var s = twoDiff(a[0], b[0]);
39 | var t = twoDiff(a[1], b[1]);
40 | var s2 = s[1] + t[1];
41 | var g = quickTwoSum(s[0], s2);
42 | s2 = g[1] + t[1];
43 | return quickTwoSum(g[0], s2);
44 | }
45 |
46 | var v1 = mult(a, d);
47 | var v2 = mult(b, c);
48 | var res = subtraction(v1, v2);
49 | return res[0];
50 | }
51 |
52 | function EquationSystemCreator() {
53 | this.quadraticEquations = [];
54 | this.linearEquations = [];
55 | this.segments = [];
56 | }
57 |
58 | EquationSystemCreator.prototype = {
59 | _addEquation: function (quadratic, a, b, k, c) {
60 | var equation = {a: a, b: b, k: k, c: c};
61 | if (quadratic)
62 | this.quadraticEquations.push(equation);
63 | else
64 | this.linearEquations.push(equation);
65 | },
66 | addVertex: function (vertex) {
67 | this._addEquation(1, -2 * vertex.x, -2 * vertex.y, 0, vertex.x * vertex.x + vertex.y * vertex.y);
68 | return this;
69 | },
70 | addSegment: function (segment) {
71 | //here is some cheating.
72 | // if we receive 2 collinear segments having a common point, we substitute the second segment by the common point
73 | // this case arises with collinear edges.
74 | for (var i = 0; i < this.segments.length; i++) {
75 | var other = this.segments[i];
76 | var commonPoint = commonVertex(segment, other);
77 | var v1 = segmentToVector(segment);
78 | var v2 = segmentToVector(other);
79 | if (commonPoint && det2x2(v1.x, v1.y, v2.x, v2.y) == 0)
80 | return this.addVertex(commonPoint);
81 | }
82 | this.segments.push(segment);
83 | var length = segLength(segment);
84 | var slope = det2x2(segment[1].x, segment[0].x, segment[1].y, segment[0].y) / length;
85 | var a = (segment[1].y - segment[0].y) / length;
86 | var b = -(segment[1].x - segment[0].x) / length;
87 | this._addEquation(0, a, b, 1, slope);
88 | return this;
89 | }
90 | };
91 |
92 | // https://github.com/aewallin/openvoronoi/blob/master/src/solvers/solver_qll.hpp
93 | // http://www.payne.org/index.php/Calculating_Voronoi_Nodes
94 | function solveEquations(creator, solutionsFilter) {
95 | function subtractEquations(eLeft, eRight) {
96 | var newEquation = {};
97 | for (var key in eLeft)
98 | if (eLeft.hasOwnProperty(key))
99 | newEquation[key] = eLeft[key] - eRight[key];
100 | return newEquation;
101 | }
102 |
103 | if (!solutionsFilter)
104 | solutionsFilter = function () {
105 | return true;
106 | };
107 | function solve(linearEquations, xi, yi, ti, quadraticEquation) {
108 | var firstEq = linearEquations[0];
109 | var secondEq = linearEquations[1];
110 | var determinant = det2x2(firstEq[xi], secondEq[xi], firstEq[yi], secondEq[yi]);
111 | if (determinant == 0)
112 | return [];
113 | var a0 = det2x2(firstEq[yi], secondEq[yi], firstEq[ti], secondEq[ti]) / determinant;
114 | var a1 = -det2x2(firstEq[xi], secondEq[xi], firstEq[ti], secondEq[ti]) / determinant;
115 | var b0 = det2x2(firstEq[yi], secondEq[yi], firstEq.c, secondEq.c) / determinant;
116 | var b1 = -det2x2(firstEq[xi], secondEq[xi], firstEq.c, secondEq.c) / determinant;
117 |
118 | var aargs = {
119 | a: [1, quadraticEquation.a],
120 | b: [1, quadraticEquation.b],
121 | k: [-1, quadraticEquation.k]
122 | };
123 | var solutions = qll_solve(aargs[xi][0], aargs[xi][1],
124 | aargs[yi][0], aargs[yi][1],
125 | aargs[ti][0], aargs[ti][1],
126 | quadraticEquation.c, // xk*xk + yk*yk - rk*rk,
127 | a0, b0, a1, b1);
128 | var realSolutions = [];
129 | for (var i = 0; i < solutions.length; i++) {
130 | var o = {};
131 | o[xi] = solutions[i].a;
132 | o[yi] = solutions[i].b;
133 | o[ti] = solutions[i].k;
134 | var point = {x: o.a, y: o.b, r: o.k};
135 | if (solutionsFilter(point))
136 | realSolutions.push(point);
137 | }
138 | return realSolutions;
139 | }
140 |
141 | function qll_solve(a0, b0, c0, d0, e0, f0, g0, a1, b1, a2, b2) {
142 | var a = a0 * (a1 * a1) + c0 * (a2 * a2) + e0;
143 | var b = 2 * a0 * a1 * b1 + 2 * a2 * b2 * c0 + a1 * b0 + a2 * d0 + f0;
144 | var c = a0 * (b1 * b1) + c0 * (b2 * b2) + b0 * b1 + b2 * d0 + g0;
145 | var roots = quadratic_roots(a, b, c);
146 |
147 | if (roots.length == 0)
148 | return [];
149 | var solutions = [];
150 | for (var i = 0; i < roots.length; i++) {
151 | var w = roots[i];
152 | solutions.push({a: a1 * w + b1, b: a2 * w + b2, k: w});
153 | }
154 | return solutions;
155 | }
156 |
157 | function quadratic_roots(a, b, c) {
158 | if (!a && !b)
159 | return [];
160 | if (!a)
161 | return [-c / b];
162 | if (!b) {
163 | var sqr = -c / a;
164 | if (sqr > 0)
165 | return [Math.sqrt(sqr), -Math.sqrt(sqr)];
166 | else if (sqr == 0)
167 | return [0];
168 | return [];
169 | }
170 | var discriminant = det2x2(b, 4 * a, c, b); // b * b - 4 * a * c;
171 | if (discriminant > 0) {
172 | var q = b > 0 ? (b + Math.sqrt(discriminant)) / -2 : (b - Math.sqrt(discriminant)) / -2;
173 | return [q / a, c / q];
174 | //not really proud of that one, but I found cases where I couldn't get a good result
175 | } else if (discriminant == 0 || Math.abs(discriminant / (b * b)) < Math.pow(2, -50))
176 | return [-b / (2 * a)];
177 | return [];
178 | }
179 |
180 | function clone(array) {
181 | return array.slice(0);
182 | }
183 |
184 | function solveLinear(equations) {
185 | var matrix = [];
186 | var rhs = [];
187 | for (var i = 0; i < equations.length; i++) {
188 | var equation = equations[i];
189 | matrix.push([equation.a, equation.b, equation.k]);
190 | rhs.push(-equation.c);
191 | }
192 | var result = numeric.solve(matrix, rhs);
193 | if (isFinite(result[0]) && isFinite(result[1]) && isFinite(result[2])) {
194 | var res = {x: result[0], y: result[1], r: result[2]};
195 | if (solutionsFilter(res))
196 | return [res];
197 | }
198 | return [];
199 | }
200 |
201 | var quadraticEquations = clone(creator.quadraticEquations);
202 | var linearEquations = clone(creator.linearEquations);
203 | var quadLength = quadraticEquations.length;
204 | var linLength = linearEquations.length;
205 | if (quadLength + linLength != 3)
206 | throw new Error('should have 3 equations, had ' + quadLength + ' quadratic equations and ' + linLength + ' linear equations.');
207 |
208 | if (quadraticEquations.length == 0)
209 | return solveLinear(linearEquations);
210 |
211 | var firstQuad = quadraticEquations[0];
212 | if (quadraticEquations.length > 1)
213 | for (var i = 1; i < quadLength; i++)
214 | linearEquations.push(subtractEquations(quadraticEquations[i], firstQuad));
215 | var solutions = [];
216 | solutions = solutions.concat(solve(linearEquations, 'a', 'b', 'k', firstQuad));
217 | if (!solutions.length)
218 | solutions = solutions.concat(solve(linearEquations, 'k', 'a', 'b', firstQuad));
219 | if (!solutions.length)
220 | solutions = solutions.concat(solve(linearEquations, 'b', 'k', 'a', firstQuad));
221 | return solutions;
222 | }
223 |
224 | return {
225 | solveEquations: solveEquations,
226 | EquationSystemCreator: EquationSystemCreator
227 | };
228 | })();
229 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | function cleanupIntersectionsForDisplay(intersections) {
5 | var res = [];
6 | for (var i = 0; i < intersections.length; i++)
7 | res.push(intersections[i][0]);
8 | return res;
9 | }
10 |
11 | test('2 segments intersections', function () {
12 | var s1 = [p(0, 0), p(20, 20)];
13 | var s2 = [p(20, 0), p(0, 20)];
14 | var res = intersectionSegments(s1, s2);
15 | var expected = {x: 10, y: 10};
16 | deepEqual(res, expected);
17 | displaySegmentsAndPoint([s1, s2], [expected], [res]);
18 | });
19 |
20 | test('horizontal-vertical intersection', function () {
21 | var s1 = [p(0, 30), p(60, 30)];
22 | var s2 = [p(30, 0), p(30, 60)];
23 | var res = [intersectionSegments(s1, s2)];
24 | var expected = [p(30, 30)];
25 | deepEqual(res, expected, 'direct computation');
26 | var segments = [s1, s2];
27 | displaySegmentsAndPoint(segments, expected, res);
28 | res = cleanupIntersectionsForDisplay(bentleyOttmann(segments));
29 | deepEqual(res, expected, 'in Bentley-Ottmann');
30 | displaySegmentsAndPoint(segments, expected, res);
31 | });
32 |
33 | test('2 almost horizontal segments intersections', function () {
34 | var epsilon = Math.pow(2, -50);
35 | var big = Math.pow(2, 52);
36 | var s1 = [p(1 - big, 1 - epsilon), p(1 + big, 1 + epsilon)];
37 | var s2 = [p(1 - big, 1 + epsilon), p(1 + big, 1 - epsilon)];
38 | var res = intersectionSegments(s1, s2);
39 | var expected = {x: 1, y: 1};
40 | deepEqual(res, expected);
41 | });
42 |
43 | test('2 almost vertical segments intersections', function () {
44 | var epsilon = Math.pow(2, -50);
45 | var big = Math.pow(2, 52);
46 | var s1 = [p(1 - epsilon, 1 - big), p(1 + epsilon, 1 + big)];
47 | var s2 = [p(1 + epsilon, 1 - big), p(1 - epsilon, 1 + big)];
48 | var res = intersectionSegments(s1, s2);
49 | var expected = {x: 1, y: 1};
50 | deepEqual(res, expected);
51 | });
52 |
53 | test('3 segments array intersections', function () {
54 | var s1 = [p(0, 0), p(20, 20)];
55 | var s2 = [p(20, 0), p(0, 20)];
56 | var s3 = [p(0, 10), p(10, 20)];
57 | var segments = [s1, s2, s3];
58 | var res = createIntersections(segments);
59 | var expected = [
60 | [p(10, 10), [s1, s2]],
61 | [p(5, 15), [s2, s3]]
62 | ];
63 | deepEqual(res, expected);
64 | expected = cleanupIntersectionsForDisplay(expected);
65 | res = cleanupIntersectionsForDisplay(res);
66 | displaySegmentsAndPoint(segments, res, expected);
67 | });
68 |
69 | test('initialization of event queue', function () {
70 | var expected = [p(0, 0), p(0, 10), p(0, 20), p(10, 20), p(20, 0), p(20, 20)];
71 | var s1 = [expected[0], expected[5]];
72 | var s2 = [expected[4], expected[2]];
73 | var s3 = [expected[1], expected[3]];
74 | var segments = [s1, s2, s3];
75 | var queue = initialPopulationOfQueue(segments);
76 | var res = [];
77 | ok(!queue.isEmpty(), 'queue is not empty');
78 | while (!queue.isEmpty())
79 | res.push(queue.fetchFirst().point);
80 | deepEqual(res, expected, 'events in correct order');
81 | ok(queue.isEmpty(), 'queue is empty after dumping the events');
82 | displaySegmentsAndPoint(segments, res, expected);
83 | });
84 |
85 | test('adding event to queue', function () {
86 | var expected = [p(0, 0), p(0, 20), p(10, 20), p(20, 20)];
87 | var s1 = [expected[0], expected[3]];
88 | var segments = [s1];
89 | var queue = initialPopulationOfQueue(segments);
90 | queue.pushIntersectionEvent({point: expected[2]});
91 | queue.pushIntersectionEvent({point: expected[1]});
92 | var res = [];
93 | ok(!queue.isEmpty(), 'queue is not empty');
94 | while (!queue.isEmpty())
95 | res.push(queue.fetchFirst().point);
96 | deepEqual(res, expected, 'events in correct order');
97 | ok(queue.isEmpty(), 'queue is empty after dumping the events');
98 | displaySegmentsAndPoint(segments, res, expected);
99 | });
100 |
101 | test('adding left events to beam', function () {
102 | var s1 = [p(0, 0), p(20, 20)];
103 | var s2 = [p(20, 0), p(0, 20)];
104 | var s3 = [p(0, 10), p(5, 10)];
105 | var segments = [s1, s2, s3];
106 | var res = bentleyOttmann(segments);
107 | var expected = [p(10, 10)];
108 | res = cleanupIntersectionsForDisplay(res);
109 | deepEqual(res, expected);
110 | displaySegmentsAndPoint(segments, expected, res);
111 | });
112 |
113 | test('beam 2 segments step by step checking', function () {
114 | var s1 = [p(0, 0), p(20, 20)];
115 | var s2 = [p(20, 0), p(0, 20)];
116 | var queue = createMockEventQueue([]);
117 | var beam = createMockScanBeam();
118 | beam.leftPoint(s1[0], s1, queue);
119 | beam.leftPoint(s2[1], s2, queue);
120 | var segments = [s1, s2];
121 | deepEqual(beam.dumpBeam(), segments, 'beam contains [s1, s2]');
122 | deepEqual(queue.dumpQueue()[0].point, p(10, 10), 'queue event point is correct');
123 | deepEqual(queue.dumpQueue()[0].type, 'intersection', 'queue event type is correct');
124 | deepEqual(queue.dumpQueue()[0].segments, segments, 'queue intersection event segments is correct');
125 | beam.intersectionPoint(p(10, 10), segments, queue);
126 | deepEqual(beam.dumpBeam(), [s2, s1], 'beam contains [s1, s2]');
127 | var expected = [
128 | [
129 | p(10, 10),
130 | segments
131 | ]
132 | ];
133 | deepEqual(beam.getResult(), expected, 'correct beam reults');
134 | beam.rightPoint(s2[0], s2, queue);
135 | deepEqual(beam.dumpBeam(), [s1], 'beam contains [s1]');
136 | beam.rightPoint(s1[1], s1, queue);
137 | deepEqual(beam.dumpBeam(), [], 'beam is empty');
138 | var result = beam.getResult();
139 | deepEqual(result, expected, 'correct beam reults');
140 | displaySegmentsAndPoint(segments, cleanupIntersectionsForDisplay(expected), cleanupIntersectionsForDisplay(result));
141 | });
142 |
143 | test('beam 4 segments; intersection masked by intersection', function () {
144 | var s1 = [p(0, 0), p(20, 20)];
145 | var s2 = [p(20, 0), p(0, 20)];
146 | var s3 = [p(7.5, 5), p(20, 5)];
147 | var s4 = [p(7.5, 15), p(20, 15)];
148 | var segments = [s1, s2, s3, s4];
149 | var res = bentleyOttmann(segments);
150 | var expected = [p(10, 10), p(15, 5), p(15, 15)];
151 | res = cleanupIntersectionsForDisplay(res);
152 | deepEqual(res, expected, 'same intersections as expected');
153 | displaySegmentsAndPoint(segments, expected, res);
154 | });
155 |
156 | test('beam 3 segments; intersection masked by right segment', function () {
157 | var s1 = [p(5, 0), p(20, 20)];
158 | var s2 = [p(20, 0), p(5, 20)];
159 | var s3 = [p(0, 10), p(10, 10)];
160 | var segments = [s1, s2, s3];
161 | var res = bentleyOttmann(segments);
162 | var expected = [p(12.5, 10)];
163 | res = cleanupIntersectionsForDisplay(res);
164 | deepEqual(res, expected, 'same intersection as expected');
165 | displaySegmentsAndPoint(segments, expected, res);
166 | });
167 |
168 | test('beam 3 segments; intersecting beam comes late', function () {
169 | var s1 = [p(0, 0), p(200, 110)];
170 | var s2 = [p(0, 150), p(150, 90)];
171 | var s3 = [p(50, 100), p(200, 100)];
172 | var segments = [s1, s2, s3];
173 | var res = bentleyOttmann(segments);
174 | var expected = [p(125, 100), p(181.8181818181818, 100)];
175 | res = cleanupIntersectionsForDisplay(res);
176 | deepEqual(res, expected, 'same intersections as expected');
177 | displaySegmentsAndPoint(segments, expected, res);
178 | });
179 |
180 | test('eventQueue prevents double insertion', function () {
181 | var s1 = [p(0, 0), p(20, 20)];
182 | var s2 = [p(20, 0), p(0, 20)];
183 | var queue = createMockEventQueue([]);
184 | var event = {type: 'intersection', point: p(10, 10), segments: [s1, s2]};
185 | queue.pushIntersectionEvent(event);
186 | deepEqual(queue.dumpQueue(), [event]);
187 | queue.pushIntersectionEvent({type: 'intersection', point: p(10, 10), segments: [s1, s2]});
188 | deepEqual(queue.dumpQueue(), [event]);
189 | });
190 |
191 | test('small random segment batch O(n^2) intersections against mock Bentley Ottmann', function () {
192 | var segments = [];
193 | for (var i = 0; i < 30; i++)
194 | segments.push([randomPoint(), randomPoint()]);
195 | var expected = cleanupIntersectionsForDisplay(createIntersections(segments));
196 | var res = cleanupIntersectionsForDisplay(bentleyOttmann(segments));
197 | expected.sort(comparePoint);
198 | res.sort(comparePoint);
199 | displaySegmentsAndPoint(segments, expected, res);
200 | deepEqual(res.length, expected.length, 'same number of points as expected');
201 | deepEqual(res, expected, 'intersections are equal');
202 | });
203 |
204 | test('big random segment batch O(n^2) intersections against mock Bentley Ottmann', function () {
205 | var segments = [];
206 | for (var i = 0; i < 300; i++)
207 | segments.push([randomPoint(), randomPoint()]);
208 | var expected = cleanupIntersectionsForDisplay(createIntersections(segments));
209 | var res = cleanupIntersectionsForDisplay(bentleyOttmann(segments));
210 | expected.sort(comparePoint);
211 | res.sort(comparePoint);
212 | deepEqual(res.length, expected.length, 'same number of points as expected');
213 | deepEqual(res, expected, 'intersections are equal');
214 | });
215 |
216 | test('rectangle', function () {
217 | var polygon = [p(10, 10), p(100, 10), p(100, 150), p(10, 150), p(10, 10)];
218 | displayMessage(svgAssertTable(polygon2path(polygon), '', ''));
219 | });
220 |
221 | test('point In Polygon', function () {
222 | var poly = [p(458, 39),
223 | p(458, 39),
224 | p(395, 46),
225 | p(308, 76),
226 | p(141, 64),
227 | p(100, 80),
228 | p(234, 89),
229 | p(343, 99),
230 | p(400, 115),
231 | p(405, 66),
232 | p(423, 50),
233 | p(419, 135),
234 | p(378, 205),
235 | p(337, 201),
236 | p(72, 185),
237 | p(73, 205),
238 | p(306, 213),
239 | p(375, 226),
240 | p(412, 261),
241 | p(343, 326),
242 | p(233, 330),
243 | p(74, 344),
244 | p(57, 316),
245 | p(133, 290),
246 | p(290, 291),
247 | p(366, 232),
248 | p(296, 222),
249 | p(172, 246),
250 | p(41, 214),
251 | p(35, 178),
252 | p(197, 171),
253 | p(350, 194),
254 | p(398, 140),
255 | p(326, 117),
256 | p(155, 92),
257 | p(28, 138),
258 | p(22, 71),
259 | p(113, 52),
260 | p(277, 64),
261 | p(326, 35),
262 | p(391, 25),
263 | p(456, 23),
264 | p(498, 15)
265 | ];
266 | for (var i = 0; i < poly.length; i++) {
267 | var point = poly[i];
268 | point.x /= 2.5;
269 | point.y /= 2.5;
270 | }
271 |
272 | var testPoint = {"x": 114.01865740403706, "y": 160.8977133216299};
273 | equal(pointInPolygon(testPoint, poly), false);
274 | });
--------------------------------------------------------------------------------
/testImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nraynaud/polygonsJS/379e73d9b421b7454e548f32e1d3ee14a127cae4/testImage.png
--------------------------------------------------------------------------------
/test_contour.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Testing Contour Extraction
6 |
7 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
258 |
259 |
--------------------------------------------------------------------------------
/test_display.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function pathList2svg(paths, test_display_viewBox) {
4 | let content = '';
5 | for (let i = 0; i < paths.length; i++)
6 | content += '\n';
7 | const viewBox = test_display_viewBox ? ' viewBox="' + test_display_viewBox + '"' : '';
8 | return '';
9 | }
10 |
11 | function path2svg(path, cssClass) {
12 | return pathList2svg([
13 | {d: path, cssClass: cssClass}
14 | ]);
15 | }
16 |
17 | function inTd(element) {
18 | return '
' + element + '
';
19 | }
20 |
21 | function svgDisplayTable(table) {
22 | let div = '
Geometry:
';
23 | for (let i = 0; i < table.length; i++)
24 | div += '
' + table[i].label + '
';
25 | div += '
';
26 | for (let i = 0; i < table.length; i++)
27 | div += '