d3-geo-polygon
Version:
Clipping and geometric operations for spherical polygons.
1,730 lines (1,520 loc) • 97.3 kB
JavaScript
// https://github.com/d3/d3-geo-polygon v2.0.1 Copyright 2017-2024 Mike Bostock
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-array'), require('d3-geo'), require('d3-geo-projection')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-array', 'd3-geo', 'd3-geo-projection'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3));
})(this, (function (exports, d3Array, d3Geo, d3GeoProjection) { 'use strict';
function noop() {}
function clipBuffer() {
let lines = [];
let line;
return {
point: function(x, y, i, t) {
const point = [x, y];
// when called by clipPolygon, store index and t
if (arguments.length > 2) { point.index = i; point.t = t; }
line.push(point);
},
lineStart: function() {
lines.push(line = []);
},
lineEnd: noop,
rejoin: function() {
if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));
},
result: function() {
const result = lines;
lines = [];
line = null;
return result;
}
};
}
function pointEqual(a, b) {
return a && b && a[0] === b[0] && a[1] === b[1];
}
function Intersection(point, points, other, entry) {
this.x = point;
this.z = points;
this.o = other; // another intersection
this.e = entry; // is an entry?
this.v = false; // visited
this.n = this.p = null; // next & previous
}
// A generalized polygon clipping algorithm: given a polygon that has been cut
// into its visible line segments, and rejoins the segments by interpolating
// along the clip edge.
function clipRejoin(segments, compareIntersection, startInside, interpolate, stream) {
const subject = [];
const clip = [];
segments.forEach((segment) => {
let n;
if ((n = segment.length - 1) <= 0) return;
let p0 = segment[0];
const p1 = segment[n];
// If the first and last points of a segment are coincident, then treat as a
// closed ring. TODO if all rings are closed, then the winding order of the
// exterior ring should be checked.
if (pointEqual(p0, p1)) {
stream.lineStart();
for (let i = 0; i < n; ++i) stream.point((p0 = segment[i])[0], p0[1]);
stream.lineEnd();
return;
}
let x;
subject.push(x = new Intersection(p0, segment, null, true));
clip.push(x.o = new Intersection(p0, null, x, false));
subject.push(x = new Intersection(p1, segment, null, false));
clip.push(x.o = new Intersection(p1, null, x, true));
});
if (!subject.length) return;
clip.sort(compareIntersection);
link(subject);
link(clip);
for (let i = 0, n = clip.length; i < n; ++i) {
clip[i].e = startInside = !startInside;
}
let start = subject[0],
points,
point;
// eslint-disable-next-line no-constant-condition
while (1) {
// Find first unvisited intersection.
let current = start,
isSubject = true;
while (current.v) if ((current = current.n) === start) return;
points = current.z;
stream.lineStart();
do {
current.v = current.o.v = true;
if (current.e) {
if (isSubject) {
for (let i = 0, n = points.length; i < n; ++i) stream.point((point = points[i])[0], point[1]);
} else {
interpolate(current.x, current.n.x, 1, stream);
}
current = current.n;
} else {
if (isSubject) {
points = current.p.z;
for (let i = points.length - 1; i >= 0; --i) stream.point((point = points[i])[0], point[1]);
} else {
interpolate(current.x, current.p.x, -1, stream);
}
current = current.p;
}
current = current.o;
points = current.z;
isSubject = !isSubject;
} while (!current.v);
stream.lineEnd();
}
}
function link(array) {
const n = array.length;
if (!n) return;
let i = 0,
a = array[0],
b;
while (++i < n) {
a.n = b = array[i];
b.p = a;
a = b;
}
a.n = b = array[0];
b.p = a;
}
const abs = Math.abs;
const atan = Math.atan;
const atan2 = Math.atan2;
const cos = Math.cos;
const exp = Math.exp;
const floor = Math.floor;
const hypot = Math.hypot;
const log = Math.log;
const max = Math.max;
const min = Math.min;
const pow = Math.pow;
const sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; };
const sin = Math.sin;
const tan = Math.tan;
const epsilon = 1e-6;
const epsilon2 = 1e-12;
const pi = Math.PI;
const halfPi = pi / 2;
const quarterPi = pi / 4;
const sqrt1_2 = Math.SQRT1_2;
const sqrtPi = sqrt(pi);
const tau = pi * 2;
const degrees = 180 / pi;
const radians = pi / 180;
function asin(x) {
return x > 1 ? halfPi : x < -1 ? -halfPi : Math.asin(x);
}
function acos(x) {
return x > 1 ? 0 : x < -1 ? pi : Math.acos(x);
}
function sqrt(x) {
return x > 0 ? Math.sqrt(x) : 0;
}
function spherical$1(cartesian) {
return [atan2(cartesian[1], cartesian[0]), asin(cartesian[2])];
}
function sphericalDegrees(cartesian) {
const c = spherical$1(cartesian);
return [c[0] * degrees, c[1] * degrees];
}
function cartesian$1(spherical) {
const lambda = spherical[0], phi = spherical[1], cosPhi = cos(phi);
return [cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)];
}
function cartesianDegrees(spherical) {
return cartesian$1([spherical[0] * radians, spherical[1] * radians]);
}
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 cartesianNormalize(d) {
const l = hypot(d[0], d[1], d[2]);
return [d[0] / l, d[1] / l, d[2] / l];
}
function cartesianEqual(a, b) {
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const dz = b[2] - a[2];
return dx * dx + dy * dy + dz * dz < epsilon2 * epsilon2;
}
function polygonContains(polygon, point) {
const lambda = point[0];
const phi = point[1];
const normal = [sin(lambda), -cos(lambda), 0];
let angle = 0;
let winding = 0;
const sum = new d3Array.Adder();
for (let i = 0, n = polygon.length; i < n; ++i) {
if (!(m = (ring = polygon[i]).length)) continue;
var ring,
m,
point0 = ring[m - 1],
lambda0 = point0[0],
phi0 = point0[1] / 2 + quarterPi,
sinPhi0 = sin(phi0),
cosPhi0 = cos(phi0);
for (let j = 0; j < m; ++j, lambda0 = lambda1, sinPhi0 = sinPhi1, cosPhi0 = cosPhi1, point0 = point1) {
var point1 = ring[j],
lambda1 = point1[0],
phi1 = point1[1] / 2 + quarterPi,
sinPhi1 = sin(phi1),
cosPhi1 = cos(phi1),
delta = lambda1 - lambda0,
sign = delta >= 0 ? 1 : -1,
absDelta = sign * delta,
antimeridian = absDelta > pi,
k = sinPhi0 * sinPhi1;
sum.add(atan2(k * sign * sin(absDelta), cosPhi0 * cosPhi1 + k * cos(absDelta)));
angle += antimeridian ? delta + sign * tau : delta;
// Are the longitudes either side of the point’s meridian (lambda),
// and are the latitudes smaller than the parallel (phi)?
if (antimeridian ^ lambda0 >= lambda ^ lambda1 >= lambda) {
const arc = cartesianNormalize(cartesianCross(cartesian$1(point0), cartesian$1(point1)));
const intersection = cartesianNormalize(cartesianCross(normal, arc));
const phiArc = (antimeridian ^ delta >= 0 ? -1 : 1) * asin(intersection[2]);
if (phi > phiArc || phi === phiArc && (arc[0] || arc[1])) {
winding += antimeridian ^ delta >= 0 ? 1 : -1;
}
}
}
}
// First, determine whether the South pole is inside or outside:
//
// It is inside if:
// * the polygon winds around it in a clockwise direction.
// * the polygon does not (cumulatively) wind around it, but has a negative
// (counter-clockwise) area.
//
// Second, count the (signed) number of times a segment crosses a lambda
// from the point to the South pole. If it is zero, then the point is the
// same side as the South pole.
return (angle < -epsilon || angle < epsilon && +sum < -epsilon) ^ (winding & 1);
}
function clip(pointVisible, clipLine, interpolate, start, sort, {clipPoint = false} = {}) {
if (typeof sort === "undefined") sort = compareIntersection;
return function(sink) {
const line = clipLine(sink);
const ringBuffer = clipBuffer();
const ringSink = clipLine(ringBuffer);
let polygonStarted = false,
polygon,
segments,
ring;
const clip = {
point,
lineStart,
lineEnd,
polygonStart: function() {
clip.point = pointRing;
clip.lineStart = ringStart;
clip.lineEnd = ringEnd;
segments = [];
polygon = [];
},
polygonEnd: function() {
clip.point = point;
clip.lineStart = lineStart;
clip.lineEnd = lineEnd;
segments = d3Array.merge(segments);
const startInside = polygonContains(polygon, start);
if (segments.length) {
if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
clipRejoin(segments, sort, startInside, interpolate, sink);
} else if (startInside) {
if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
interpolate(null, null, 1, sink);
}
if (polygonStarted) sink.polygonEnd(), polygonStarted = false;
segments = polygon = null;
},
sphere: () => interpolate(null, null, 1, sink)
};
function point(lambda, phi) {
if ((!clipPoint && !ring) || pointVisible(lambda, phi)) sink.point(lambda, phi);
}
function pointLine(lambda, phi) {
line.point(lambda, phi);
}
function lineStart() {
clip.point = pointLine;
line.lineStart();
}
function lineEnd() {
clip.point = point;
line.lineEnd();
}
function pointRing(lambda, phi, close) {
ring.push([lambda, phi]);
ringSink.point(lambda, phi, close);
}
function ringStart() {
ringSink.lineStart();
ring = [];
}
function ringEnd() {
pointRing(ring[0][0], ring[0][1], true);
ringSink.lineEnd();
const clean = ringSink.clean();
const ringSegments = ringBuffer.result();
const n = ringSegments.length;
let m, segment, point;
ring.pop();
polygon.push(ring);
ring = null;
if (!n) return;
// No intersections.
if (clean & 1) {
segment = ringSegments[0];
if ((m = segment.length - 1) > 0) {
if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
sink.lineStart();
for (let i = 0; i < m; ++i) sink.point((point = segment[i])[0], point[1]);
sink.lineEnd();
}
return;
}
// Rejoin connected segments.
// TODO reuse ringBuffer.rejoin()?
if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));
segments.push(ringSegments.filter(validSegment));
}
return clip;
};
}
function validSegment(segment) {
return segment.length > 1;
}
// Intersections are sorted along the clip edge. For both antimeridian cutting
// and circle clipping, the same comparison is used.
function compareIntersection(a, b) {
return ((a = a.x)[0] < 0 ? a[1] - halfPi - epsilon : halfPi - a[1])
- ((b = b.x)[0] < 0 ? b[1] - halfPi - epsilon : halfPi - b[1]);
}
class intersectSegment {
constructor(from, to) {
this.from = from, this.to = to;
this.normal = cartesianCross(from, to);
this.fromNormal = cartesianCross(this.normal, from);
this.toNormal = cartesianCross(this.normal, to);
this.l = acos(cartesianDot(from, to));
}
}
// >> here a and b are segments processed by intersectSegment
function intersect(a, b) {
if (cartesianEqual(a.from, b.from) || cartesianEqual(a.from, b.to)) return a.from;
if (cartesianEqual(a.to, b.from) || cartesianEqual(a.to, b.to)) return a.to;
// Slightly faster lookup when there is no intersection
const lc = (a.l + b.l < pi) ? cos(a.l + b.l) - epsilon : -1;
if (cartesianDot(a.from, b.from) < lc
|| cartesianDot(a.from, b.to) < lc
|| cartesianDot(a.to, b.from) < lc
|| cartesianDot(a.to, b.to) < lc)
return;
const axb = cartesianNormalize(cartesianCross(a.normal, b.normal));
const a0 = cartesianDot(axb, a.fromNormal);
const a1 = cartesianDot(axb, a.toNormal);
const b0 = cartesianDot(axb, b.fromNormal);
const b1 = cartesianDot(axb, b.toNormal);
// check if the candidate lies on both segments
// or is almost equal to one of the four points
if (a0 >= 0 && a1 <= 0 && b0 >= 0 && b1 <= 0)
return axb;
// same test for the antipode
if (a0 <= 0 && a1 >= 0 && b0 <= 0 && b1 >= 0)
return axb.map(d => -d);
}
function intersectPointOnLine(p, a) {
const a0 = cartesianDot(p, a.fromNormal);
const a1 = cartesianDot(p, a.toNormal);
p = cartesianDot(p, a.normal);
return abs(p) < epsilon2 && (a0 > -epsilon2 && a1 < epsilon2 || a0 < epsilon2 && a1 > -epsilon2);
}
const intersectCoincident = {};
function intersect$1(a, b) {
const ca = a.map(p => cartesian$1(p.map(d => d * radians)));
const cb = b.map(p => cartesian$1(p.map(d => d * radians)));
const i = intersect(
new intersectSegment(ca[0], ca[1]),
new intersectSegment(cb[0], cb[1])
);
return i ? spherical$1(i).map((d) => d * degrees) : null;
}
const clipNone = (stream) => stream;
// clipPolygon
function geoClipPolygon (geometry) {
let clipPoint = true;
function clipGeometry(geometry) {
if (geometry.type === "Polygon") geometry = {type: "MultiPolygon", coordinates: [geometry.coordinates]};
if (geometry.type !== "MultiPolygon") return clipNone;
const clips = geometry.coordinates.map((polygon) => {
polygon = polygon.map(ringRadians);
const pointVisible = visible(polygon);
const segments = ringSegments(polygon[0]); // todo holes?
return clip(
pointVisible,
clipLine(segments, pointVisible),
interpolate(segments, polygon),
polygon[0][0],
clipPolygonSort,
{clipPoint}
);
});
function clipPolygon(stream) {
const clipstream = clips.map(clip => clip(stream));
return {
point(lambda, phi) {
clipstream.forEach((clip) => clip.point(lambda, phi));
},
lineStart() {
clipstream.forEach((clip) => clip.lineStart());
},
lineEnd() {
clipstream.forEach((clip) => clip.lineEnd());
},
polygonStart() {
clipstream.forEach((clip) => clip.polygonStart());
},
polygonEnd() {
clipstream.forEach((clip) => clip.polygonEnd());
},
sphere() {
clipstream.forEach((clip) => clip.sphere());
},
};
}
clipPolygon.polygon = (_) => _ !== undefined ? clipGeometry(geometry = _) : geometry;
clipPolygon.clipPoint = (_) => _ !== undefined ? ((clipPoint = !!_), clipGeometry(geometry)) : clipPoint;
return clipPolygon;
}
return clipGeometry(geometry);
}
function ringRadians(ring) {
return ring.map((point) => [point[0] * radians, point[1] * radians]);
}
function ringSegments(ring) {
const segments = [];
let c0;
ring.forEach((point, i) => {
const c = cartesian$1(point);
if (i) segments.push(new intersectSegment(c0, c));
c0 = c;
return point;
});
return segments;
}
function clipPolygonSort(a, b) {
(a = a.x), (b = b.x);
return a.index - b.index || a.t - b.t;
}
function interpolate(segments, polygon) {
return (from, to, direction, stream) => {
if (from == null) {
stream.polygonStart();
polygon.forEach((ring) => {
stream.lineStart();
ring.forEach((point) => stream.point(point[0], point[1]));
stream.lineEnd();
});
stream.polygonEnd();
} else if (
from.index !== to.index &&
from.index != null &&
to.index != null
) {
for (
let i = from.index;
i !== to.index;
i = (i + direction + segments.length) % segments.length
) {
const segment = segments[i];
const point = spherical$1(direction > 0 ? segment.to : segment.from);
stream.point(point[0], point[1]);
}
} else if (
from.index === to.index &&
from.t > to.t &&
from.index != null &&
to.index != null
) {
for (let i = 0; i < segments.length; ++i) {
const segment =
segments[
(from.index + i * direction + segments.length) % segments.length
];
const point = spherical$1(direction > 0 ? segment.to : segment.from);
stream.point(point[0], point[1]);
}
}
};
}
// Geodesic coordinates for two 3D points.
function clipPolygonDistance(a, b) {
const axb = cartesianCross(a, b);
return atan2(sqrt(cartesianDot(axb, axb)), cartesianDot(a, b));
}
function visible(polygon) {
return (lambda, phi) => polygonContains(polygon, [lambda, phi]);
}
function randsign(i, j) {
return sign(sin(100 * i + j));
}
function clipLine(segments, pointVisible) {
return function (stream) {
let point0, lambda00, phi00, v00, v0, clean, line, lines = [];
return {
lineStart() {
point0 = null;
clean = 1;
line = [];
},
lineEnd() {
if (v0) lines.push(line);
lines.forEach((line) => {
stream.lineStart();
line.forEach((point) => stream.point(...point)); // can have 4 dimensions
stream.lineEnd();
});
lines = [];
},
point(lambda, phi, close) {
if (cos(lambda) == -1) lambda -= sign(sin(lambda)) * 1e-5; // move away from -180/180 https://github.com/d3/d3-geo/pull/108#issuecomment-323798937
if (close) (lambda = lambda00), (phi = phi00);
let point = cartesian$1([lambda * 0.9999999999, phi + 1e-14]);
let v = v0;
if (point0) {
const intersections = [];
let segment = new intersectSegment(point0, point);
for (let i = 0, j = 100; i < segments.length && j > 0; ++i) {
const s = segments[i];
const intersection = intersect(segment, s);
if (intersection) {
if (
intersection === intersectCoincident ||
cartesianEqual(intersection, point0) ||
cartesianEqual(intersection, point) ||
cartesianEqual(intersection, s.from) ||
cartesianEqual(intersection, s.to)
) {
const t = 1e-4;
lambda = ((lambda + 3 * pi + randsign(i, j) * t) % (2 * pi)) - pi;
phi = min(pi / 2 - t, max(t - pi / 2, phi + randsign(i, j) * t));
segment = new intersectSegment(point0, (point = cartesian$1([lambda, phi])));
(i = -1), --j;
intersections.length = 0;
continue;
}
const sph = spherical$1(intersection);
intersection.distance = clipPolygonDistance(point0, intersection);
intersection.index = i;
intersection.t = clipPolygonDistance(s.from, intersection);
intersection[0] = sph[0];
intersection[1] = sph[1];
delete intersection[2];
intersections.push(intersection);
}
}
if (intersections.length) {
clean = 0;
intersections.sort((a, b) => a.distance - b.distance);
for (let i = 0; i < intersections.length; ++i) {
const intersection = intersections[i];
v = !v;
if (v) {
line = [];
line.push([
intersection[0],
intersection[1],
intersection.index,
intersection.t
]);
} else {
line.push([
intersection[0],
intersection[1],
intersection.index,
intersection.t
]);
lines.push(line);
}
}
}
if (v) line.push([lambda, phi]);
} else {
for (let i = 0, j = 100; i < segments.length && j > 0; ++i) {
const s = segments[i];
if (intersectPointOnLine(point, s)) {
const t = 1e-4;
lambda = ((lambda + 3 * pi + randsign(i, j) * t) % (2 * pi)) - pi;
phi = min(
pi / 2 - 1e-4,
max(1e-4 - pi / 2, phi + randsign(i, j) * t)
);
point = cartesian$1([lambda, phi]);
(i = -1), --j;
}
}
v00 = v = pointVisible((lambda00 = lambda), (phi00 = phi));
if (v) (line = []), line.push([lambda, phi]);
}
point0 = point;
v0 = v;
},
// Rejoin first and last segments if there were intersections and the first
// and last points were visible.
clean() {
return clean | ((v00 && v0) << 1);
}
};
};
}
// Note: 6-element arrays are used to denote the 3x3 affine transform matrix:
// [a, b, c,
// d, e, f,
// 0, 0, 1] - this redundant row is left out.
// Transform matrix for [a0, a1] -> [b0, b1].
function matrix(a, b) {
const u = subtract(a[1], a[0]);
const v = subtract(b[1], b[0]);
const phi = angle(u, v);
const s = length(u) / length(v);
return multiply([
1, 0, a[0][0],
0, 1, a[0][1]
], multiply([
s, 0, 0,
0, s, 0
], multiply([
cos(phi), sin(phi), 0,
-sin(phi), cos(phi), 0
], [
1, 0, -b[0][0],
0, 1, -b[0][1]
])));
}
// Inverts a transform matrix.
function inverse(m) {
const k = 1 / (m[0] * m[4] - m[1] * m[3]);
return [
k * m[4], -k * m[1], k * (m[1] * m[5] - m[2] * m[4]),
-k * m[3], k * m[0], k * (m[2] * m[3] - m[0] * m[5])
];
}
// Multiplies two 3x2 matrices.
function multiply(a, b) {
return [
a[0] * b[0] + a[1] * b[3],
a[0] * b[1] + a[1] * b[4],
a[0] * b[2] + a[1] * b[5] + a[2],
a[3] * b[0] + a[4] * b[3],
a[3] * b[1] + a[4] * b[4],
a[3] * b[2] + a[4] * b[5] + a[5]
];
}
// Subtracts 2D vectors.
function subtract(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
// Magnitude of a 2D vector.
function length(v) {
return sqrt(v[0] * v[0] + v[1] * v[1]);
}
// Angle between two 2D vectors.
function angle(a, b) {
return atan2(a[0] * b[1] - a[1] * b[0], a[0] * b[0] + a[1] * b[1]);
}
// Creates a polyhedral projection.
// * tree: a spanning tree of polygon faces. Nodes are automatically
// augmented with a transform matrix.
// * face: a function that returns the appropriate node for a given {lambda, phi}
// point (radians).
function polyhedral(tree, face) {
recurse(tree, {transform: null});
function recurse(node, parent) {
node.edges = faceEdges(node.face);
// Find shared edge.
if (parent.face) {
const shared = node.shared = sharedEdge(node.face, parent.face);
const m = matrix(shared.map(parent.project), shared.map(node.project));
node.transform = parent.transform ? multiply(parent.transform, m) : m;
// Replace shared edge in parent edges array.
let edges = parent.edges;
for (let i = 0, n = edges.length; i < n; ++i) {
if (pointEqual(shared[0], edges[i][1]) && pointEqual(shared[1], edges[i][0])) edges[i] = node;
if (pointEqual(shared[0], edges[i][0]) && pointEqual(shared[1], edges[i][1])) edges[i] = node;
}
edges = node.edges;
for (let i = 0, n = edges.length; i < n; ++i) {
if (pointEqual(shared[0], edges[i][0]) && pointEqual(shared[1], edges[i][1])) edges[i] = parent;
if (pointEqual(shared[0], edges[i][1]) && pointEqual(shared[1], edges[i][0])) edges[i] = parent;
}
} else {
node.transform = parent.transform;
}
if (node.children) node.children.forEach((child) => recurse(child, node));
return node;
}
function forward(lambda, phi) {
const node = face(lambda, phi);
const point = node.project([lambda * degrees, phi * degrees]);
const t = node.transform;
return t
? [t[0] * point[0] + t[1] * point[1] + t[2], -(t[3] * point[0] + t[4] * point[1] + t[5])]
: [point[0], -point[1]];
}
// Naive inverse! A faster solution would use bounding boxes, or even a
// polygonal quadtree.
if (hasInverse(tree)) forward.invert = function(x, y) {
const coordinates = faceInvert(tree, [x, -y]);
return coordinates && (coordinates[0] *= radians, coordinates[1] *= radians, coordinates);
};
function faceInvert(node, coordinates) {
const invert = node.project.invert;
let point = coordinates;
let p;
let t = node.transform;
if (t) {
t = inverse(t);
point = [t[0] * point[0] + t[1] * point[1] + t[2], (t[3] * point[0] + t[4] * point[1] + t[5])];
}
if (invert && node === faceDegrees(p = invert(point))) return p;
const children = node.children;
for (let i = 0, n = children && children.length; i < n; ++i) {
p = faceInvert(children[i], coordinates);
if (p) return p;
}
}
function faceDegrees(coordinates) {
return face(coordinates[0] * radians, coordinates[1] * radians);
}
const proj = d3Geo.geoProjection(forward);
// run around the mesh of faces and stream all vertices to create the clipping polygon
const p = [];
const geometry = {type: "MultiPolygon", coordinates: [[p]]};
outline({point: (lambda, phi) => p.push([lambda, phi])}, tree);
p.push(p[0]);
proj.preclip(geoClipPolygon(geometry).clipPoint(d3Geo.geoArea(geometry) < 4 * Math.PI - 0.1));
proj.tree = function() { return tree; };
return proj;
}
function outline(stream, node, parent) {
let point,
edges = node.edges,
n = edges.length,
edge,
multiPoint = {type: "MultiPoint", coordinates: node.face},
notPoles = node.face.filter(function(d) { return abs(d[1]) !== 90; }),
b = d3Geo.geoBounds({type: "MultiPoint", coordinates: notPoles}),
inside = false,
j = -1,
dx = b[1][0] - b[0][0];
// TODO
node.centroid = dx === 180 || dx === 360
? [(b[0][0] + b[1][0]) / 2, (b[0][1] + b[1][1]) / 2]
: d3Geo.geoCentroid(multiPoint);
// First find the shared edge…
if (parent) while (++j < n) {
if (edges[j] === parent) break;
}
++j;
for (let i = 0; i < n; ++i) {
edge = edges[(i + j) % n];
if (Array.isArray(edge)) {
if (!inside) {
stream.point((point = d3Geo.geoInterpolate(edge[0], node.centroid)(epsilon))[0], point[1]);
inside = true;
}
stream.point((point = d3Geo.geoInterpolate(edge[1], node.centroid)(epsilon))[0], point[1]);
} else {
inside = false;
if (edge !== parent) outline(stream, edge, node);
}
}
}
// Finds a shared edge given two clockwise polygons.
function sharedEdge(a, b) {
const n = a.length;
let x, y, found = null;
for (let i = 0; i < n; ++i) {
x = a[i];
for (let j = b.length; --j >= 0;) {
y = b[j];
if (x[0] === y[0] && x[1] === y[1]) {
if (found) return [found, x];
found = x;
}
}
}
}
// Converts an array of n face vertices to an array of n + 1 edges.
function faceEdges(face) {
const n = face.length;
const edges = [];
for (let i = 0, a = face[n - 1]; i < n; ++i) edges.push([a, a = face[i]]);
return edges;
}
function hasInverse(node) {
return node.project.invert || node.children && node.children.some(hasInverse);
}
// TODO generate on-the-fly to avoid external modification.
const octahedron = [
[0, 90],
[-90, 0], [0, 0], [90, 0], [180, 0],
[0, -90]
];
var octahedron$1 = [
[0, 2, 1],
[0, 3, 2],
[5, 1, 2],
[5, 2, 3],
[0, 1, 4],
[0, 4, 3],
[5, 4, 1],
[5, 3, 4]
].map((face) => face.map((i) => octahedron[i]));
function butterfly(faceProjection = ((face) => {
const c = d3Geo.geoCentroid({type: "MultiPoint", coordinates: face});
return d3Geo.geoGnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]);
})) {
const faces = octahedron$1.map((face) => ({face, project: faceProjection(face)}));
[-1, 0, 0, 1, 0, 1, 4, 5].forEach((d, i) => {
const node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});
return polyhedral(
faces[0],
(lambda, phi) => faces[
lambda < -pi / 2 ? phi < 0 ? 6 : 4
: lambda < 0 ? phi < 0 ? 2 : 0
: lambda < pi / 2 ? phi < 0 ? 3 : 1
: phi < 0 ? 7 : 5]
)
.angle(-30)
.scale(101.858)
.center([0, 45]);
}
// code duplicated from d3-geo-projection
function collignonRaw(lambda, phi) {
const alpha = sqrt(1 - sin(phi));
return [(2 / sqrtPi) * lambda * alpha, sqrtPi * (1 - alpha)];
}
collignonRaw.invert = function(x, y) {
const lambda = (y / sqrtPi - 1);
return [lambda ? x * sqrt(pi) / lambda / 2 : 0, asin(1 - lambda ** 2)];
};
const kx = 2 / sqrt(3);
function collignonK(a, b) {
const p = collignonRaw(a, b);
return [p[0] * kx, p[1]];
}
collignonK.invert = (x,y) => collignonRaw.invert(x / kx, y);
function collignon(faceProjection = (face) => {
const c = d3Geo.geoCentroid({type: "MultiPoint", coordinates: face});
return d3Geo.geoProjection(collignonK).translate([0, 0]).scale(1).rotate(c[1] > 0 ? [-c[0], 0] : [180 - c[0], 180]);
}) {
const faces = octahedron$1.map((face) => ({face, project: faceProjection(face)}));
[-1, 0, 0, 1, 0, 1, 4, 5].forEach((d, i) => {
const node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});
return polyhedral(
faces[0],
(lambda, phi) => faces[lambda < -pi / 2 ? phi < 0 ? 6 : 4
: lambda < 0 ? phi < 0 ? 2 : 0
: lambda < pi / 2 ? phi < 0 ? 3 : 1
: phi < 0 ? 7 : 5])
.angle(-30)
.scale(121.906)
.center([0, 48.5904]);
}
function waterman(faceProjection = ((face) => {
const c = face.length === 6 ? d3Geo.geoCentroid({type: "MultiPoint", coordinates: face}) : face[0];
return d3Geo.geoGnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]);
})) {
const w5 = octahedron$1.map((face) => {
const xyz = face.map(cartesian);
const n = xyz.length;
const hexagon = [];
let a = xyz[n - 1], b;
for (let i = 0; i < n; ++i) {
b = xyz[i];
hexagon.push(spherical([
a[0] * 0.9486832980505138 + b[0] * 0.31622776601683794,
a[1] * 0.9486832980505138 + b[1] * 0.31622776601683794,
a[2] * 0.9486832980505138 + b[2] * 0.31622776601683794
]), spherical([
b[0] * 0.9486832980505138 + a[0] * 0.31622776601683794,
b[1] * 0.9486832980505138 + a[1] * 0.31622776601683794,
b[2] * 0.9486832980505138 + a[2] * 0.31622776601683794
]));
a = b;
}
return hexagon;
});
const cornerNormals = [];
const parents = [-1, 0, 0, 1, 0, 1, 4, 5];
w5.forEach((hexagon, j) => {
const face = octahedron$1[j],
n = face.length,
normals = cornerNormals[j] = [];
for (let i = 0; i < n; ++i) {
w5.push([
face[i],
hexagon[(i * 2 + 2) % (2 * n)],
hexagon[(i * 2 + 1) % (2 * n)]
]);
parents.push(j);
normals.push(cross(
cartesian(hexagon[(i * 2 + 2) % (2 * n)]),
cartesian(hexagon[(i * 2 + 1) % (2 * n)])
));
}
});
const faces = w5.map((face) => ({
project: faceProjection(face),
face
}));
parents.forEach((d, i) => {
const parent = faces[d];
parent && (parent.children || (parent.children = [])).push(faces[i]);
});
function face(lambda, phi) {
const cosphi = cos(phi);
const p = [cosphi * cos(lambda), cosphi * sin(lambda), sin(phi)];
const hexagon = lambda < -pi / 2 ? phi < 0 ? 6 : 4
: lambda < 0 ? phi < 0 ? 2 : 0
: lambda < pi / 2 ? phi < 0 ? 3 : 1
: phi < 0 ? 7 : 5;
const n = cornerNormals[hexagon];
return faces[dot(n[0], p) < 0 ? 8 + 3 * hexagon
: dot(n[1], p) < 0 ? 8 + 3 * hexagon + 1
: dot(n[2], p) < 0 ? 8 + 3 * hexagon + 2
: hexagon];
}
return polyhedral(faces[0], face)
.angle(-30)
.scale(110.625)
.center([0, 45]);
}
function dot(a, b) {
let s = 0;
for (let i = 0; i < a.length; ++i) s += a[i] * b[i];
return s;
}
function cross(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]
];
}
// 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;
const phi = coordinates[1] * radians;
const cosphi = cos(phi);
return [
cosphi * cos(lambda),
cosphi * sin(lambda),
sin(phi)
];
}
// it is possible to pass a specific projection on each face
// by default is is a gnomonic projection centered on the face's centroid
// scale 1 by convention
const faceProjection0 = (face) => d3Geo.geoGnomonic()
.scale(1)
.translate([0, 0])
.rotate([
Math.abs(face.site[1]) > 89.99999999 ? 0 : -face.site[0],
-face.site[1]
]);
function voronoi(
parents = [],
polygons = { features: [] },
faceProjection = faceProjection0,
find
) {
if (find === undefined) find = find0;
let faces = [];
function build_tree() {
// the faces from the polyhedron each yield
// - face: its vertices
// - site: its voronoi site (default: centroid)
// - project: local projection on this face
faces = polygons.features.map((feature, i) => {
const polygon = feature.geometry.coordinates[0];
const face = polygon.slice(0, -1);
face.site =
feature.properties && feature.properties.sitecoordinates
? feature.properties.sitecoordinates
: d3Geo.geoCentroid(feature.geometry);
return {
face,
site: face.site,
id: i,
project: faceProjection(face)
};
});
// Build a tree of the faces, starting with face 0 (North Pole)
// which has no parent (-1)
parents.forEach((d, i) => {
const node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});
}
// a basic function to find the polygon that contains the point
function find0(lambda, phi) {
let d0 = Infinity;
let found = -1;
for (let i = 0; i < faces.length; i++) {
const d = d3Geo.geoDistance(faces[i].site, [lambda, phi]);
if (d < d0) {
d0 = d;
found = i;
}
}
return found;
}
function faceFind(lambda, phi) {
return faces[find(lambda * degrees, phi * degrees)];
}
let p = d3Geo.geoGnomonic();
function reset() {
let rotate = p.rotate(),
translate = p.translate(),
center = p.center(),
scale = p.scale(),
angle = p.angle();
if (faces.length) {
p = polyhedral(faces.find((face, i) => face && !faces[parents[i]]), faceFind);
}
p.parents = function(_) {
if (!arguments.length) return parents;
parents = _;
build_tree();
return reset();
};
p.polygons = function(_) {
if (!arguments.length) return polygons;
polygons = _;
build_tree();
return reset();
};
p.faceProjection = function(_) {
if (!arguments.length) return faceProjection;
faceProjection = _;
build_tree();
return reset();
};
p.faceFind = function(_) {
if (!arguments.length) return find;
find = _;
return reset();
};
return p
.rotate(rotate)
.translate(translate)
.center(center)
.scale(scale)
.angle(angle);
}
build_tree();
return reset();
}
function dodecahedral() {
const A0 = asin(1/sqrt(3)) * degrees;
const A1 = acos((sqrt(5) - 1) / sqrt(3) / 2) * degrees;
const A2 = 90 - A1;
const A3 = acos(-(1 + sqrt(5)) / sqrt(3) / 2) * degrees;
const dodecahedron = [
[[45,A0],[0,A1],[180,A1],[135,A0],[90,A2]],
[[45,A0],[A2,0],[-A2,0],[-45,A0],[0,A1]],
[[45,A0],[90,A2],[90,-A2],[45,-A0],[A2,0]],
[[0,A1],[-45,A0],[-90,A2],[-135,A0],[180,A1]],
[[A2,0],[45,-A0],[0,-A1],[-45,-A0],[-A2,0]],
[[90,A2],[135,A0],[A3,0],[135,-A0],[90,-A2]],
[[45,-A0],[90,-A2],[135,-A0],[180,-A1],[0,-A1]],
[[135,A0],[180,A1],[-135,A0],[-A3,0],[A3,0]],
[[-45,A0],[-A2,0],[-45,-A0],[-90,-A2],[-90,A2]],
[[-45,-A0],[0,-A1],[180,-A1],[-135,-A0],[-90,-A2]],
[[135,-A0],[A3,0],[-A3,0],[-135,-A0],[180,-A1]],
[[-135,A0],[-90,A2],[-90,-A2],[-135,-A0],[-A3,0]]
];
const polygons = {
type: "FeatureCollection",
features: dodecahedron.map((face) => ({
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [ [...face, face[0]] ]
}
}))
};
return voronoi()
.parents([-1,0,4,8,1,2,2,3,1,8,6,3])
.angle(72 * 1.5)
.polygons(polygons)
.scale(99.8)
.rotate([-8,0,-32]);
}
// code duplicated from d3-geo-projection
function lagrangeRaw(n) {
function forward(lambda, phi) {
if (abs(abs(phi) - halfPi) < epsilon) return [0, phi < 0 ? -2 : 2];
const sinPhi = sin(phi);
const v = pow((1 + sinPhi) / (1 - sinPhi), n / 2);
const c = 0.5 * (v + 1 / v) + cos(lambda *= n);
return [2 * sin(lambda) / c, (v - 1 / v) / c];
}
forward.invert = (x, y) => {
const y0 = abs(y);
if (abs(y0 - 2) < epsilon) return x ? null : [0, sign(y) * halfPi];
if (y0 > 2) return null;
x /= 2, y /= 2;
const x2 = x * x;
const y2 = y * y;
let t = 2 * y / (1 + x2 + y2); // tanh(nPhi)
t = pow((1 + t) / (1 - t), 1 / n);
return [
atan2(2 * x, 1 - x2 - y2) / n,
asin((t - 1) / (t + 1))
];
};
return forward;
}
function complexMul(a, b) {
return [a[0] * b[0] - a[1] * b[1], a[1] * b[0] + a[0] * b[1]];
}
function complexAdd(a, b) {
return [a[0] + b[0], a[1] + b[1]];
}
function complexSub(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
function complexNorm2(a) {
return a[0] * a[0] + a[1] * a[1];
}
function complexNorm(a) {
return sqrt(complexNorm2(a));
}
function complexLogHypot(a, b) {
const _a = abs(a);
const _b = abs(b);
if (a === 0) return log(_b);
if (b === 0) return log(_a);
if (_a < 3000 && _b < 3000) return log(a * a + b * b) * 0.5;
return log(a / cos(atan2(b, a)));
}
// adapted from https://github.com/infusion/Complex.js
function complexPow(a, n) {
let b = a[1],
arg,
loh;
a = a[0];
if (a === 0 && b === 0) return [0, 0];
if (typeof n === "number") n = [n, 0];
if (!n[1]) {
if (b === 0 && a >= 0) {
return [pow(a, n[0]), 0];
} else if (a === 0) {
switch ((n[1] % 4 + 4) % 4) {
case 0:
return [pow(b, n[0]), 0];
case 1:
return [0, pow(b, n[0])];
case 2:
return [-pow(b, n[0]), 0];
case 3:
return [0, -pow(b, n[0])];
}
}
}
arg = atan2(b, a);
loh = complexLogHypot(a, b);
a = exp(n[0] * loh - n[1] * arg);
b = n[1] * loh + n[0] * arg;
return [a * cos(b), a * sin(b)];
}
// w1 = gamma(1/n) * gamma(1 - 2/n) / n / gamma(1 - 1/n)
// https://blocks.roadtolarissa.com/Fil/852557838117687bbd985e4b38ff77d4
const w = [-1 / 2, sqrt(3) / 2],
w1 = [1.7666387502854533, 0],
m = 0.3 * 0.3;
// Approximate \int _0 ^sm(z) dt / (1 - t^3)^(2/3)
// sm maps a triangle to a disc, sm^-1 does the opposite
function sm_1(z) {
// rotate to have s ~= 1
const rot = complexPow(
w,
d3Array.scan(
[0, 1, 2].map(function(i) {
return -complexMul(z, complexPow(w, [i, 0]))[0];
})
)
);
let y = complexMul(rot, z);
y = [1 - y[0], -y[1]];
// McIlroy formula 5 p6 and table for F3 page 16
const F0 = [
1.44224957030741,
0.240374928384568,
0.0686785509670194,
0.0178055502507087,
0.00228276285265497,
-1.48379585422573e-3,
-1.64287728109203e-3,
-1.02583417082273e-3,
-4.83607537673571e-4,
-1.67030822094781e-4,
-2.45024395166263e-5,
2.14092375450951e-5,
2.55897270486771e-5,
1.73086854400834e-5,
8.72756299984649e-6,
3.18304486798473e-6,
4.79323894565283e-7,
-4.58968389565456e-7,
-5.62970586787826e-7,
-3.92135372833465e-7
];
let F = [0, 0];
for (let i = F0.length; i--; ) F = complexAdd([F0[i], 0], complexMul(F, y));
let k = complexMul(
complexAdd(w1, complexMul([-F[0], -F[1]], complexPow(y, 1 - 2 / 3))),
complexMul(rot, rot)
);
// when we are close to [0,0] we switch to another approximation:
// https://www.wolframalpha.com/input/?i=(-2%2F3+choose+k)++*+(-1)%5Ek++%2F+(k%2B1)+with+k%3D0,1,2,3,4
// the difference is _very_ tiny but necessary
// if we want projection(0,0) === [0,0]
const n = complexNorm2(z);
if (n < m) {
const H0 = [
1,
1 / 3,
5 / 27,
10 / 81,
22 / 243 //…
];
const z3 = complexPow(z, [3, 0]);
let h = [0, 0];
for (let i = H0.length; i--; ) h = complexAdd([H0[i], 0], complexMul(h, z3));
h = complexMul(h, z);
k = complexAdd(complexMul(k, [n / m, 0]), complexMul(h, [1 - n / m, 0]));
}
return k;
}
const lagrange1_2 = lagrangeRaw ? lagrangeRaw(0.5) : null;
function coxRaw(lambda, phi) {
const s = lagrange1_2(lambda, phi);
const t = sm_1([s[1] / 2, s[0] / 2]);
return [t[1], t[0]];
}
// the Sphere should go *exactly* to the vertices of the triangles
// because they are singular points
function sphere() {
const c = 2 * asin(1 / sqrt(5)) * degrees;
return {
type: "Polygon",
coordinates: [
[[0, 90], [-180, -c + epsilon], [0, -90], [180, -c + epsilon], [0, 90]]
]
};
}
function cox() {
const p = d3Geo.geoProjection(coxRaw);
const stream_ = p.stream;
p.stream = function(stream) {
const rotate = p.rotate(),
rotateStream = stream_(stream),
sphereStream = (p.rotate([0, 0]), stream_(stream));
p.rotate(rotate);
rotateStream.sphere = function() {
d3Geo.geoStream(sphere(), sphereStream);
};
return rotateStream;
};
return p
.scale(188.305)
.translate([480, 333.167]);
}
// Approximate Newton-Raphson
// Solve f(x) = y, start from x
function solve(f, y, x) {
let steps = 100, delta, f0, f1;
x = x === undefined ? 0 : +x;
y = +y;
do {
f0 = f(x);
f1 = f(x + epsilon);
if (f0 === f1) f1 = f0 + epsilon;
x -= delta = (-1 * epsilon * (f0 - y)) / (f0 - f1);
} while (steps-- > 0 && abs(delta) > epsilon);
return steps < 0 ? NaN : x;
}
// Approximate Newton-Raphson in 2D
// Solve f(a,b) = [x,y]
function solve2d(f, MAX_ITERATIONS = 40, eps = epsilon2) {
return (x, y, a = 0, b = 0) => {
let err2, da, db;
for (let i = 0; i < MAX_ITERATIONS; ++i) {
const p = f(a, b),
// diffs
tx = p[0] - x,
ty = p[1] - y;
if (abs(tx) < eps && abs(ty) < eps) break; // we're there!
// backtrack if we overshot
const h = tx * tx + ty * ty;
if (h > err2) {
a -= da /= 2;
b -= db /= 2;
continue;
}
err2 = h;
// partial derivatives
const ea = (a > 0 ? -1 : 1) * eps,
eb = (b > 0 ? -1 : 1) * eps,
pa = f(a + ea, b),
pb = f(a, b + eb),
dxa = (pa[0] - p[0]) / ea,
dya = (pa[1] - p[1]) / ea,
dxb = (pb[0] - p[0]) / eb,
dyb = (pb[1] - p[1]) / eb,
// determinant
D = dyb * dxa - dya * dxb,
// newton step — or half-step for small D
l = (abs(D) < 0.5 ? 0.5 : 1) / D;
da = (ty * dxb - tx * dyb) * l;
db = (tx * dya - ty * dxa) * l;
a += da;
b += db;
if (abs(da) < eps && abs(db) < eps) break; // we're crawling
}
return [a, b];
};
}
function leeRaw(lambda, phi) {
const w = [-1 / 2, sqrt(3) / 2];
let k = [0, 0],
h = [0, 0],
z = complexMul(d3Geo.geoStereographicRaw(lambda, phi), [sqrt(2), 0]);
// rotate to have s ~= 1
const sector = d3Array.greatest([0, 1, 2], (i) => complexMul(z, complexPow(w, [i, 0]))[0]);
const rot = complexPow(w, [sector, 0]);
const n = complexNorm(z);
if (n > 0.3) {
// if |z| > 0.5, use the approx based on y = (1-z)
// McIlroy formula 6 p6 and table for G page 16
const y = complexSub([1, 0], complexMul(rot, z));
// w1 = gamma(1/3) * gamma(1/2) / 3 / gamma(5/6);
// https://bl.ocks.org/Fil/1aeff1cfda7188e9fbf037d8e466c95c
const w1 = 1.4021821053254548;
const G0 = [
1.15470053837925, 0.192450089729875, 0.0481125224324687,
0.010309826235529, 3.34114739114366e-4, -1.50351632601465e-3,
-1.2304417796231e-3, -6.75190201960282e-4, -2.84084537293856e-4,
-8.21205120500051e-5, -1.59257630018706e-6, 1.91691805888369e-5,
1.73095888028726e-5, 1.03865580818367e-5, 4.70614523937179e-6,
1.4413500104181e-6, 1.92757960170179e-8, -3.82869799649063e-7,
-3.57526015225576e-7, -2.2175964844211e-7,
];
let G = [0, 0];
for (let i = G0.length; i--; ) {
G = complexAdd([G0[i], 0], complexMul(G, y));
}
k = complexSub([w1, 0], complexMul(complexPow(y, 1 / 2), G));
k = complexMul(k, rot);
k = complexMul(k, rot);
}
if (n < 0.5) {
// if |z| < 0.3
// https://www.wolframalpha.com/input/?i=series+of+((1-z%5E3))+%5E+(-1%2F2)+at+z%3D0 (and ask for "more terms")
// 1 + z^3/2 + (3 z^6)/8 + (5 z^9)/16 + (35 z^12)/128 + (63 z^15)/256 + (231 z^18)/1024 + O(z^21)
// https://www.wolframalpha.com/input/?i=integral+of+1+%2B+z%5E3%2F2+%2B+(3+z%5E6)%2F8+%2B+(5+z%5E9)%2F16+%2B+(35+z%5E12)%2F128+%2B+(63+z%5E15)%2F256+%2B+(231+z%5E18)%2F1024
// (231 z^19)/19456 + (63 z^16)/4096 + (35 z^13)/1664 + z^10/32 + (3 z^7)/56 + z^4/8 + z + constant
const H0 = [1, 1 / 8, 3 / 56, 1 / 32, 35 / 1664, 63 / 4096, 231 / 19456];
const z3 = complexPow(z, [3, 0]);
for (let i = H0.length; i--; ) h = complexAdd([H0[i], 0], complexMul(h, z3));
h = complexMul(h, z);
}
if (n < 0.3) return h;
if (n > 0.5) return k;
// in between 0.3 and 0.5, interpolate
const t = (n - 0.3) / (0.5 - 0.3);
return complexAdd(complexMul(k, [t, 0]), complexMul(h, [1 - t, 0]));
}
const leeSolver = solve2d(leeRaw);
leeRaw.invert = function (x, y) {
if (x > 1.5) return false; // immediately avoid using the wrong face
const p = leeSolver(x, y, x, y * 0.5);
const q = leeRaw(p[0], p[1]);
q[0] -= x;
q[1] -= y;
return (q[0] * q[0] + q[1] * q[1] < 1e-8)
? p
: [-10, 0]; // far out of the face
};
const asin1_3 = asin(1 / 3);
const centers = [
[0, 90],
[-180, -asin1_3 * degrees],
[-60, -asin1_3 * degrees],
[60, -asin1_3 * degrees],
];
const tetrahedron = [
[1, 2, 3],
[0, 2, 1],
[0, 3, 2],
[0, 1, 3],
].map((face) => face.map((i) => centers[i]));
function tetrahedralLee (
faceProjection = (face) => {
const c = d3Geo.geoCentroid({ type: "MultiPoint", coordinates: face });
const rotate = abs(c[1]) == 90 ? [0, -c[1], -30] : [-c[0], -c[1], 30];
return d3Geo.geoProjection(leeRaw).scale(1).translate([0, 0]).rotate(rotate);
}
) {
return voronoi([-1, 0, 0, 0], {
features: tetrahedron.map((t) => ({
type: "Feature",
geometry: {type: "Polygon", coordinates: [[...t, t[0]]]}
}))
}, faceProjection)
.rotate([30, 180]) // North Pole aspect
.angle(30)
.scale(118.662)
.translate([480, 195.47]);
}
/*
* Buckminster Fuller’s spherical triangle transformation procedure
*
* Based on Robert W. Gray’s formulae published in “Exact Transformation Equations
* For Fuller's World Map,” _Cartographica_, 32(3): 17-25 (1995).
*
* Implemented for D3.js by Philippe Rivière, 2018 (https://visionscarto.net/)
*
* To the extent possible under law, Philippe Rivière has waived all copyright
* and related or neighboring rights to this implementation. (Public Domain.)
*/
function GrayFullerRaw() {
const SQRT_3 = sqrt(3);
// Gray’s constants
const Z = sqrt(5 + 2 * sqrt(5)) / sqrt(15);
const el = sqrt(8) / sqrt(5 + sqrt(5));
const dve = sqrt(3 + sqrt(5)) / sqrt(5 + sqrt(5));
const grayfuller = function(lambda, phi) {
const cosPhi = cos(phi),
s = Z / (cosPhi * cos(lambda)),
x = cosPhi * sin(lambda) * s,
y = sin(phi) * s,
a1p = atan2(2 * y / SQRT_3 + el / 3 - el / 2, dve),
a2p = atan2(x - y / SQRT_3 + el / 3 - el / 2, dve),
a3p = atan2(el / 3 - x - y / SQRT_3 - el / 2, dve);
return [SQRT_3 * (a2p - a3p), 2 * a1p - a2p - a3p];
};
// Inverse approximation
grayfuller.invert = function(x, y) {
// if the point is out of the triangle, return
// something meaningless (but far away enough)
if (x * x + y * y > 5) return [0, 3];
const R = 2.9309936378128416;
const p = d3Geo.geoGnomonicRaw.invert(x / R, y / R);
let j = 0, dx, dy;
do {
const f = grayfuller(p[0], p[1]);
dx = x - f[0],
dy = y - f[1];
p[0] += 0.2 * dx;
p[1] += 0.2 * dy;
} while (j++ < 30 && abs(dx) + abs(dy) > epsilon);
return p;
};
return grayfuller;
}
/*
* Buckminster Fuller’s AirOcean arrangement of the icosahedron
*
* Implemented for D3.js by Jason Davies (2013),
* Enrico Spinielli (2017) and Philippe Rivière (2017, 2018)
*
*/
function airoceanRaw(faceProjection) {
const theta = atan(0.5) * degrees;
// construction inspired by
// https://en.wikipedia.org/wiki/Regular_icosahedron#Spherical_coordinates
const vertices = [[0, 90], [0, -90]].concat(
d3Array.range(10).map((i) => [(i * 36 + 180) % 360 - 180, i & 1 ? theta : -theta])
);
// icosahedron
const polyhedron = [
[0, 3, 11],
[0, 5, 3],
[0, 7, 5],
[0, 9, 7],
[0, 11, 9], // North
[2, 11, 3],
[3, 4, 2],
[4, 3, 5],
[5, 6, 4],
[6, 5, 7],
[7, 8, 6],
[8, 7, 9],
[9, 10, 8],
[10, 9