diff --git a/package.json b/package.json index 5e9a393..8e047ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "d3-geo-voronoi", - "version": "1.2.1", + "version": "1.3.0", "description": "Spherical Voronoi Diagram and Delaunay Triangulation", "keywords": [ "d3", @@ -9,9 +9,9 @@ "d3-delaunay" ], "license": "MIT", - "main": "build/d3-geo-voronoi.js", - "unpkg": "build/d3-geo-voronoi.min.js", - "jsdelivr": "build/d3-geo-voronoi.min.js", + "main": "dist/d3-geo-voronoi.js", + "unpkg": "dist/d3-geo-voronoi.min.js", + "jsdelivr": "dist/d3-geo-voronoi.min.js", "module": "index", "jsnext:main": "index", "homepage": "https://github.com/Fil/d3-geo-voronoi", @@ -24,10 +24,10 @@ "url": "https://github.com/Fil" }, "scripts": { - "pretest": "rm -rf build && mkdir build && rollup --banner \"$(preamble)\" -g d3-array:d3,d3-geo:d3,d3-delaunay:d3 -f umd -n d3 --extend d3 -o build/d3-geo-voronoi.js -- index.js", + "pretest": "rm -rf dist && mkdir dist && rollup --banner \"$(preamble)\" -g d3-array:d3,d3-geo:d3,d3-delaunay:d3 -f umd -n d3 --extend d3 -o dist/d3-geo-voronoi.js -- index.js", "test": "tape 'test/**/*-test.js'", - "prepublishOnly": "npm run test && terser --preamble \"$(preamble)\" build/d3-geo-voronoi.js -c passes=2 -c negate_iife=false -m -o build/d3-geo-voronoi.min.js", - "postpublish": "zip -j build/d3-geo-voronoi.zip -- LICENSE README.md build/d3-geo-voronoi.js build/d3-geo-voronoi.min.js" + "prepublishOnly": "npm run test && terser --preamble \"$(preamble)\" dist/d3-geo-voronoi.js -c passes=2 -c negate_iife=false -m -o dist/d3-geo-voronoi.min.js", + "postpublish": "zip -j dist/d3-geo-voronoi.zip -- LICENSE README.md dist/d3-geo-voronoi.js dist/d3-geo-voronoi.min.js" }, "dependencies": { "d3-array": "^2.0", diff --git a/src/delaunay.js b/src/delaunay.js index 75a9f1c..2d9eafc 100644 --- a/src/delaunay.js +++ b/src/delaunay.js @@ -113,20 +113,25 @@ function geo_find(neighbors, points) { function geo_delaunay_from(points) { if (points.length < 2) return {}; - const r = geoRotation(points[0]), + // find a valid point to send to infinity + let pivot = 0; + while (isNaN(points[pivot][0]+points[pivot][1]) && pivot++ < points.length) {} + + const r = geoRotation(points[pivot]), projection = geoStereographic() .translate([0, 0]) .scale(1) .rotate(r.invert([180, 0])); points = points.map(projection); - const zeros = [0]; + const zeros = []; let max2 = 1; - for (let i = 1, n = points.length; i < n; i++) { - let m = points[i][0] * points[i][0] + points[i][1] * points[i][1]; - if (isNaN(m)) zeros.push(i); - if (m > max2) max2 = m; + for (let i = 0, n = points.length; i < n; i++) { + let m = points[i][0] ** 2 + points[i][1] ** 2; + if (!isFinite(m)) zeros.push(i); + else if (m > max2) max2 = m; } + const FAR = 1e6 * sqrt(max2); zeros.forEach(i => (points[i] = [FAR / 2, i])); diff --git a/src/voronoi.js b/src/voronoi.js index c0f4dd7..0d68a30 100644 --- a/src/voronoi.js +++ b/src/voronoi.js @@ -19,7 +19,11 @@ export function geoVoronoi(data) { v._data = v._data.features; } if (typeof v._data === "object") { - v.points = v._data.map(i => [v._vx(i), v._vy(i)]); + const temp = v._data + .map(d => [v._vx(d), v._vy(d), d]) + .filter(d => isFinite(d[0] + d[1])); + v.points = temp.map(d => [d[0], d[1]]); + v.valid = temp.map(d => d[2]); v.delaunay = geoDelaunay(v.points); } return v; @@ -53,12 +57,15 @@ export function geoVoronoi(data) { if (data !== undefined) { v(data); } + if (!v.delaunay) return false; - if (v._data.length === 0) return null; - if (v._data.length === 1) return { type: "Sphere" }; - return { + const coll = { type: "FeatureCollection", - features: v.delaunay.polygons.map((poly, i) => ({ + features: [] + }; + if (v.valid.length === 0) return coll; + v.delaunay.polygons.forEach((poly, i) => + coll.features.push({ type: "Feature", geometry: !poly ? null @@ -67,12 +74,23 @@ export function geoVoronoi(data) { coordinates: [[...poly, poly[0]].map(i => v.delaunay.centers[i])] }, properties: { - site: v._data[i], + site: v.valid[i], sitecoordinates: v.points[i], neighbours: v.delaunay.neighbors[i] // not part of the public API } - })) - }; + }) + ); + if (v.valid.length === 1) + coll.features.push({ + type: "Feature", + geometry: { type: "Sphere" }, + properties: { + site: v.valid[0], + sitecoordinates: v.points[0], + neighbours: [] + } + }); + return coll; }; v.triangles = function(data) { @@ -117,8 +135,8 @@ export function geoVoronoi(data) { features: v.delaunay.edges.map((e, i) => ({ type: "Feature", properties: { - source: v._data[e[0]], - target: v._data[e[1]], + source: v.valid[e[0]], + target: v.valid[e[1]], length: _distances[i], urquhart: !!_urquart[i] }, diff --git a/test/geo-voronoi-test.js b/test/geo-voronoi-test.js index e4a9ade..8b4a371 100644 --- a/test/geo-voronoi-test.js +++ b/test/geo-voronoi-test.js @@ -32,10 +32,24 @@ tape("geoVoronoi.polygons(sites) tolerates NaN.", function(test) { //var u = geoVoronoi.geoVoronoi().polygons(sites)[0][0], v = [ 5, 4.981069 ]; //test.ok( (Math.abs(u[0]-v[0]) < 1e-6) && (Math.abs(u[1]-v[1]) < 1e-6) ); const sites = [[0, 0], [2, 1], [NaN, -1], [4, NaN], [5,10]]; - var u = geoVoronoi.geoVoronoi(sites).polygons() + var u = geoVoronoi.geoVoronoi(sites).polygons(); + test.end(); +}); + +tape("geoVoronoi.polygons([no valid site]) returns an empty collection.", function(test) { + const sites = [[NaN, -1], [4, NaN], [Infinity,10]]; + var u = geoVoronoi.geoVoronoi(sites).polygons(); + test.deepEqual(u.features, []); test.end(); }); +tape("geoVoronoi.polygons([1 site]) returns a Sphere.", function(test) { + const sites = [[NaN, -1], [4, NaN], [5,10]]; + var u = geoVoronoi.geoVoronoi(sites).polygons(); + test.equal(u.features[0].type, "Feature"); + test.equal(u.features[0].geometry.type, "Sphere"); + test.end(); +}); var sites = [[0,0], [10,0], [0,10]];