UNPKG

d3-geo-polygon

Version:

Clipping and geometric operations for spherical polygons.

1,730 lines (1,520 loc) 97.3 kB
// 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