d3-geo-voronoi
Version:
Spherical Voronoi Diagram and Delaunay Triangulation
668 lines (598 loc) • 17.3 kB
JavaScript
// https://github.com/Fil/d3-geo-voronoi v2.1.0 Copyright 2024 Philippe Rivière
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-delaunay'), require('d3-geo'), require('d3-array'), require('d3-tricontour')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-delaunay', 'd3-geo', 'd3-array', 'd3-tricontour'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3, global.d3));
})(this, (function (exports, d3Delaunay, d3Geo, d3Array, d3Tricontour) { 'use strict';
const pi = Math.PI;
const halfPi = pi / 2;
const degrees = 180 / pi;
const radians = pi / 180;
const atan2 = Math.atan2;
const cos = Math.cos;
const max = Math.max;
const min = Math.min;
const sin = Math.sin;
const sign =
Math.sign ||
function (x) {
return x > 0 ? 1 : x < 0 ? -1 : 0;
};
const sqrt = Math.sqrt;
function asin(x) {
return x > 1 ? halfPi : x < -1 ? -halfPi : Math.asin(x);
}
function cartesianDot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
function cartesianCross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
function cartesianAdd(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}
function cartesianNormalize(d) {
var l = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
return [d[0] / l, d[1] / l, d[2] / l];
}
// Converts 3D Cartesian to spherical coordinates (degrees).
function spherical(cartesian) {
return [
atan2(cartesian[1], cartesian[0]) * degrees,
asin(max(-1, min(1, cartesian[2]))) * degrees,
];
}
// Converts spherical coordinates (degrees) to 3D Cartesian.
function cartesian(coordinates) {
const lambda = coordinates[0] * radians,
phi = coordinates[1] * radians,
cosphi = cos(phi);
return [cosphi * cos(lambda), cosphi * sin(lambda), sin(phi)];
}
// Spherical excess of a triangle (in spherical coordinates)
function excess(triangle) {
triangle = triangle.map((p) => cartesian(p));
return cartesianDot(triangle[0], cartesianCross(triangle[2], triangle[1]));
}
function geoDelaunay(points) {
const delaunay = geo_delaunay_from(points),
triangles = geo_triangles(delaunay),
edges = geo_edges(triangles, points),
neighbors = geo_neighbors(triangles, points.length),
find = geo_find(neighbors, points),
// Voronoi ; could take a center function as an argument
circumcenters = geo_circumcenters(triangles, points),
{ polygons, centers } = geo_polygons(circumcenters, triangles, points),
mesh = geo_mesh(polygons),
hull = geo_hull(triangles, points),
// Urquhart ; returns a function that takes a distance array as argument.
urquhart = geo_urquhart(edges, triangles);
return {
delaunay,
edges,
triangles,
centers,
neighbors,
polygons,
mesh,
hull,
urquhart,
find,
};
}
function geo_find(neighbors, points) {
function distance2(a, b) {
let x = a[0] - b[0],
y = a[1] - b[1],
z = a[2] - b[2];
return x * x + y * y + z * z;
}
return function find(x, y, next) {
if (next === undefined) next = 0;
let cell,
dist,
found = next;
const xyz = cartesian([x, y]);
do {
cell = next;
next = null;
dist = distance2(xyz, cartesian(points[cell]));
neighbors[cell].forEach((i) => {
let ndist = distance2(xyz, cartesian(points[i]));
if (ndist < dist) {
dist = ndist;
next = i;
found = i;
return;
}
});
} while (next !== null);
return found;
};
}
function geo_delaunay_from(points) {
if (points.length < 2) return {};
// find a valid point to send to infinity
let pivot = 0;
while (isNaN(points[pivot][0] + points[pivot][1]) && pivot++ < points.length);
const r = d3Geo.geoRotation(points[pivot]),
projection = d3Geo.geoStereographic()
.translate([0, 0])
.scale(1)
.rotate(r.invert([180, 0]));
points = points.map(projection);
const zeros = [];
let max2 = 1;
for (let i = 0, n = points.length; i < n; i++) {
let m = points[i][0] ** 2 + points[i][1] ** 2;
if (!isFinite(m) || m > 1e32) zeros.push(i);
else if (m > max2) max2 = m;
}
const FAR = 1e6 * sqrt(max2);
zeros.forEach((i) => (points[i] = [FAR, 0]));
// Add infinite horizon points
points.push([0, FAR]);
points.push([-FAR, 0]);
points.push([0, -FAR]);
const delaunay = d3Delaunay.Delaunay.from(points);
delaunay.projection = projection;
// clean up the triangulation
const { triangles, halfedges, inedges } = delaunay;
for (let i = 0, l = halfedges.length; i < l; i++) {
if (halfedges[i] < 0) {
const j = i % 3 == 2 ? i - 2 : i + 1;
const k = i % 3 == 0 ? i + 2 : i - 1;
const a = halfedges[j];
const b = halfedges[k];
halfedges[a] = b;
halfedges[b] = a;
halfedges[j] = halfedges[k] = -1;
triangles[i] = triangles[j] = triangles[k] = pivot;
inedges[triangles[a]] = a % 3 == 0 ? a + 2 : a - 1;
inedges[triangles[b]] = b % 3 == 0 ? b + 2 : b - 1;
i += 2 - (i % 3);
} else if (triangles[i] > points.length - 3 - 1) {
triangles[i] = pivot;
}
}
// there should always be 4 degenerate triangles
// console.warn(degenerate);
return delaunay;
}
function geo_edges(triangles, points) {
const _index = new Set();
if (points.length === 2) return [[0, 1]];
triangles.forEach((tri) => {
if (tri[0] === tri[1]) return;
if (excess(tri.map((i) => points[i])) < 0) return;
for (let i = 0, j; i < 3; i++) {
j = (i + 1) % 3;
_index.add(d3Array.extent([tri[i], tri[j]]).join("-"));
}
});
return Array.from(_index, (d) => d.split("-").map(Number));
}
function geo_triangles(delaunay) {
const { triangles } = delaunay;
if (!triangles) return [];
const geo_triangles = [];
for (let i = 0, n = triangles.length / 3; i < n; i++) {
const a = triangles[3 * i],
b = triangles[3 * i + 1],
c = triangles[3 * i + 2];
if (a !== b && b !== c) {
geo_triangles.push([a, c, b]);
}
}
return geo_triangles;
}
function geo_circumcenters(triangles, points) {
// if (!use_centroids) {
return triangles.map((tri) => {
const c = tri.map((i) => points[i]).map(cartesian),
V = cartesianAdd(
cartesianAdd(cartesianCross(c[1], c[0]), cartesianCross(c[2], c[1])),
cartesianCross(c[0], c[2])
);
return spherical(cartesianNormalize(V));
});
/*} else {
return triangles.map(tri => {
return d3.geoCentroid({
type: "MultiPoint",
coordinates: tri.map(i => points[i])
});
});
}*/
}
function geo_neighbors(triangles, npoints) {
const neighbors = [];
triangles.forEach((tri) => {
for (let j = 0; j < 3; j++) {
const a = tri[j],
b = tri[(j + 1) % 3];
neighbors[a] = neighbors[a] || [];
neighbors[a].push(b);
}
});
// degenerate cases
if (triangles.length === 0) {
if (npoints === 2) (neighbors[0] = [1]), (neighbors[1] = [0]);
else if (npoints === 1) neighbors[0] = [];
}
return neighbors;
}
function geo_polygons(circumcenters, triangles, points) {
const polygons = [];
const centers = circumcenters.slice();
if (triangles.length === 0) {
if (points.length < 2) return { polygons, centers };
if (points.length === 2) {
// two hemispheres
const a = cartesian(points[0]),
b = cartesian(points[1]),
m = cartesianNormalize(cartesianAdd(a, b)),
d = cartesianNormalize(cartesianCross(a, b)),
c = cartesianCross(m, d);
const poly = [
m,
cartesianCross(m, c),
cartesianCross(cartesianCross(m, c), c),
cartesianCross(cartesianCross(cartesianCross(m, c), c), c),
]
.map(spherical)
.map(supplement);
return (
polygons.push(poly),
polygons.push(poly.slice().reverse()),
{ polygons, centers }
);
}
}
triangles.forEach((tri, t) => {
for (let j = 0; j < 3; j++) {
const a = tri[j],
b = tri[(j + 1) % 3],
c = tri[(j + 2) % 3];
polygons[a] = polygons[a] || [];
polygons[a].push([b, c, t, [a, b, c]]);
}
});
// reorder each polygon
const reordered = polygons.map((poly) => {
const p = [poly[0][2]]; // t
let k = poly[0][1]; // k = c
for (let i = 1; i < poly.length; i++) {
// look for b = k
for (let j = 0; j < poly.length; j++) {
if (poly[j][0] == k) {
k = poly[j][1];
p.push(poly[j][2]);
break;
}
}
}
if (p.length > 2) {
return p;
} else if (p.length == 2) {
const R0 = o_midpoint(
points[poly[0][3][0]],
points[poly[0][3][1]],
centers[p[0]]
),
R1 = o_midpoint(
points[poly[0][3][2]],
points[poly[0][3][0]],
centers[p[0]]
);
const i0 = supplement(R0),
i1 = supplement(R1);
return [p[0], i1, p[1], i0];
}
});
function supplement(point) {
let f = -1;
centers.slice(triangles.length, Infinity).forEach((p, i) => {
if (p[0] === point[0] && p[1] === point[1]) f = i + triangles.length;
});
if (f < 0) (f = centers.length), centers.push(point);
return f;
}
return { polygons: reordered, centers };
}
function o_midpoint(a, b, c) {
a = cartesian(a);
b = cartesian(b);
c = cartesian(c);
const s = sign(cartesianDot(cartesianCross(b, a), c));
return spherical(cartesianNormalize(cartesianAdd(a, b)).map((d) => s * d));
}
function geo_mesh(polygons) {
const mesh = [];
polygons.forEach((poly) => {
if (!poly) return;
let p = poly[poly.length - 1];
for (let q of poly) {
if (q > p) mesh.push([p, q]);
p = q;
}
});
return mesh;
}
function geo_urquhart(edges, triangles) {
return function (distances) {
const _lengths = new Map(),
_urquhart = new Map();
edges.forEach((edge, i) => {
const u = edge.join("-");
_lengths.set(u, distances[i]);
_urquhart.set(u, true);
});
triangles.forEach((tri) => {
let l = 0,
remove = -1;
for (let j = 0; j < 3; j++) {
let u = d3Array.extent([tri[j], tri[(j + 1) % 3]]).join("-");
if (_lengths.get(u) > l) {
l = _lengths.get(u);
remove = u;
}
}
_urquhart.set(remove, false);
});
return edges.map((edge) => _urquhart.get(edge.join("-")));
};
}
function geo_hull(triangles, points) {
const _hull = new Set(),
hull = [];
triangles.map((tri) => {
if (excess(tri.map((i) => points[i > points.length ? 0 : i])) > 1e-12)
return;
for (let i = 0; i < 3; i++) {
let e = [tri[i], tri[(i + 1) % 3]],
code = `${e[0]}-${e[1]}`;
if (_hull.has(code)) _hull.delete(code);
else _hull.add(`${e[1]}-${e[0]}`);
}
});
const _index = new Map();
let start;
_hull.forEach((e) => {
e = e.split("-").map(Number);
_index.set(e[0], e[1]);
start = e[0];
});
if (start === undefined) return hull;
let next = start;
do {
hull.push(next);
let n = _index.get(next);
_index.set(next, -1);
next = n;
} while (next > -1 && next !== start);
return hull;
}
function geoVoronoi(data) {
const v = function (data) {
v.delaunay = null;
v._data = data;
if (typeof v._data === "object" && v._data.type === "FeatureCollection") {
v._data = v._data.features;
}
if (typeof v._data === "object") {
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;
};
v._vx = function (d) {
if (typeof d == "object" && "type" in d) {
return d3Geo.geoCentroid(d)[0];
}
if (0 in d) return d[0];
};
v._vy = function (d) {
if (typeof d == "object" && "type" in d) {
return d3Geo.geoCentroid(d)[1];
}
if (1 in d) return d[1];
};
v.x = function (f) {
if (!f) return v._vx;
v._vx = f;
return v;
};
v.y = function (f) {
if (!f) return v._vy;
v._vy = f;
return v;
};
v.polygons = function (data) {
if (data !== undefined) {
v(data);
}
if (!v.delaunay) return false;
const coll = {
type: "FeatureCollection",
features: [],
};
if (v.valid.length === 0) return coll;
v.delaunay.polygons.forEach((poly, i) =>
coll.features.push({
type: "Feature",
geometry: !poly
? null
: {
type: "Polygon",
coordinates: [
[...poly, poly[0]].map((i) => v.delaunay.centers[i]),
],
},
properties: {
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) {
if (data !== undefined) {
v(data);
}
if (!v.delaunay) return false;
return {
type: "FeatureCollection",
features: v.delaunay.triangles
.map((tri, index) => {
tri = tri.map((i) => v.points[i]);
tri.center = v.delaunay.centers[index];
return tri;
})
.filter((tri) => excess(tri) > 0)
.map((tri) => ({
type: "Feature",
properties: {
circumcenter: tri.center,
},
geometry: {
type: "Polygon",
coordinates: [[...tri, tri[0]]],
},
})),
};
};
v.links = function (data) {
if (data !== undefined) {
v(data);
}
if (!v.delaunay) return false;
const _distances = v.delaunay.edges.map((e) =>
d3Geo.geoDistance(v.points[e[0]], v.points[e[1]])
),
_urquart = v.delaunay.urquhart(_distances);
return {
type: "FeatureCollection",
features: v.delaunay.edges.map((e, i) => ({
type: "Feature",
properties: {
source: v.valid[e[0]],
target: v.valid[e[1]],
length: _distances[i],
urquhart: !!_urquart[i],
},
geometry: {
type: "LineString",
coordinates: [v.points[e[0]], v.points[e[1]]],
},
})),
};
};
v.mesh = function (data) {
if (data !== undefined) {
v(data);
}
if (!v.delaunay) return false;
return {
type: "MultiLineString",
coordinates: v.delaunay.edges.map((e) => [
v.points[e[0]],
v.points[e[1]],
]),
};
};
v.cellMesh = function (data) {
if (data !== undefined) {
v(data);
}
if (!v.delaunay) return false;
const { centers, polygons } = v.delaunay;
const coordinates = [];
for (const p of polygons) {
if (!p) continue;
for (
let n = p.length, p0 = p[n - 1], p1 = p[0], i = 0;
i < n;
p0 = p1, p1 = p[++i]
) {
if (p1 > p0) {
coordinates.push([centers[p0], centers[p1]]);
}
}
}
return {
type: "MultiLineString",
coordinates,
};
};
v._found = undefined;
v.find = function (x, y, radius) {
v._found = v.delaunay.find(x, y, v._found);
if (!radius || d3Geo.geoDistance([x, y], v.points[v._found]) < radius)
return v._found;
};
v.hull = function (data) {
if (data !== undefined) {
v(data);
}
const hull = v.delaunay.hull,
points = v.points;
return hull.length === 0
? null
: {
type: "Polygon",
coordinates: [[...hull.map((i) => points[i]), points[hull[0]]]],
};
};
return data ? v(data) : v;
}
function geoContour() {
let v;
const contour = d3Tricontour.tricontour()
.triangulate((data, x, y) => {
v = geoDelaunay(data.map((d, i) => [x(d, i), y(d, i)]));
return v.delaunay;
})
.pointInterpolate((i, j, a) => {
const { points, projection } = v.delaunay;
const A = projection.invert([points[2 * i], points[2 * i + 1]]),
B = projection.invert([points[2 * j], points[2 * j + 1]]);
return d3Geo.geoInterpolate(A, B)(a);
})
.ringsort((rings) => {
// tricky thing: in isobands this function is called twice,
// we want to reverse the polygons’s winding order only in tricontour()
// not in isoband()
if (rings.length && !rings[0].reversed) {
rings.forEach((ring) => ring.reverse());
rings[0].reversed = true;
}
return [rings];
});
return contour;
}
exports.geoContour = geoContour;
exports.geoDelaunay = geoDelaunay;
exports.geoVoronoi = geoVoronoi;
Object.defineProperty(exports, '__esModule', { value: true });
}));