d3-geo-polygon
Version:
Clipping and geometric operations for spherical polygons.
153 lines (138 loc) • 5.33 kB
JavaScript
import {geoArea, geoBounds as bounds, geoCentroid as centroid, geoInterpolate as interpolate, geoProjection as projection} from "d3-geo";
import clipPolygon from "../clip/polygon.js";
import {abs, degrees, epsilon, radians} from "../math.js";
import matrix, {multiply, inverse} from "./matrix.js";
import pointEqual from "../pointEqual.js";
// 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).
export default function(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 = projection(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(clipPolygon(geometry).clipPoint(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 = bounds({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]
: centroid(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 = interpolate(edge[0], node.centroid)(epsilon))[0], point[1]);
inside = true;
}
stream.point((point = interpolate(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);
}