180 lines
5.4 KiB
JavaScript
180 lines
5.4 KiB
JavaScript
import {Adder} from "d3-array";
|
|
import {areaStream, areaRingSum} from "./area.js";
|
|
import {cartesian, cartesianCross, cartesianNormalizeInPlace, spherical} from "./cartesian.js";
|
|
import {abs, degrees, epsilon, radians} from "./math.js";
|
|
import stream from "./stream.js";
|
|
|
|
var lambda0, phi0, lambda1, phi1, // bounds
|
|
lambda2, // previous lambda-coordinate
|
|
lambda00, phi00, // first point
|
|
p0, // previous 3D point
|
|
deltaSum,
|
|
ranges,
|
|
range;
|
|
|
|
var boundsStream = {
|
|
point: boundsPoint,
|
|
lineStart: boundsLineStart,
|
|
lineEnd: boundsLineEnd,
|
|
polygonStart: function() {
|
|
boundsStream.point = boundsRingPoint;
|
|
boundsStream.lineStart = boundsRingStart;
|
|
boundsStream.lineEnd = boundsRingEnd;
|
|
deltaSum = new Adder();
|
|
areaStream.polygonStart();
|
|
},
|
|
polygonEnd: function() {
|
|
areaStream.polygonEnd();
|
|
boundsStream.point = boundsPoint;
|
|
boundsStream.lineStart = boundsLineStart;
|
|
boundsStream.lineEnd = boundsLineEnd;
|
|
if (areaRingSum < 0) lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90);
|
|
else if (deltaSum > epsilon) phi1 = 90;
|
|
else if (deltaSum < -epsilon) phi0 = -90;
|
|
range[0] = lambda0, range[1] = lambda1;
|
|
},
|
|
sphere: function() {
|
|
lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90);
|
|
}
|
|
};
|
|
|
|
function boundsPoint(lambda, phi) {
|
|
ranges.push(range = [lambda0 = lambda, lambda1 = lambda]);
|
|
if (phi < phi0) phi0 = phi;
|
|
if (phi > phi1) phi1 = phi;
|
|
}
|
|
|
|
function linePoint(lambda, phi) {
|
|
var p = cartesian([lambda * radians, phi * radians]);
|
|
if (p0) {
|
|
var normal = cartesianCross(p0, p),
|
|
equatorial = [normal[1], -normal[0], 0],
|
|
inflection = cartesianCross(equatorial, normal);
|
|
cartesianNormalizeInPlace(inflection);
|
|
inflection = spherical(inflection);
|
|
var delta = lambda - lambda2,
|
|
sign = delta > 0 ? 1 : -1,
|
|
lambdai = inflection[0] * degrees * sign,
|
|
phii,
|
|
antimeridian = abs(delta) > 180;
|
|
if (antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {
|
|
phii = inflection[1] * degrees;
|
|
if (phii > phi1) phi1 = phii;
|
|
} else if (lambdai = (lambdai + 360) % 360 - 180, antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {
|
|
phii = -inflection[1] * degrees;
|
|
if (phii < phi0) phi0 = phii;
|
|
} else {
|
|
if (phi < phi0) phi0 = phi;
|
|
if (phi > phi1) phi1 = phi;
|
|
}
|
|
if (antimeridian) {
|
|
if (lambda < lambda2) {
|
|
if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda;
|
|
} else {
|
|
if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda;
|
|
}
|
|
} else {
|
|
if (lambda1 >= lambda0) {
|
|
if (lambda < lambda0) lambda0 = lambda;
|
|
if (lambda > lambda1) lambda1 = lambda;
|
|
} else {
|
|
if (lambda > lambda2) {
|
|
if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda;
|
|
} else {
|
|
if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ranges.push(range = [lambda0 = lambda, lambda1 = lambda]);
|
|
}
|
|
if (phi < phi0) phi0 = phi;
|
|
if (phi > phi1) phi1 = phi;
|
|
p0 = p, lambda2 = lambda;
|
|
}
|
|
|
|
function boundsLineStart() {
|
|
boundsStream.point = linePoint;
|
|
}
|
|
|
|
function boundsLineEnd() {
|
|
range[0] = lambda0, range[1] = lambda1;
|
|
boundsStream.point = boundsPoint;
|
|
p0 = null;
|
|
}
|
|
|
|
function boundsRingPoint(lambda, phi) {
|
|
if (p0) {
|
|
var delta = lambda - lambda2;
|
|
deltaSum.add(abs(delta) > 180 ? delta + (delta > 0 ? 360 : -360) : delta);
|
|
} else {
|
|
lambda00 = lambda, phi00 = phi;
|
|
}
|
|
areaStream.point(lambda, phi);
|
|
linePoint(lambda, phi);
|
|
}
|
|
|
|
function boundsRingStart() {
|
|
areaStream.lineStart();
|
|
}
|
|
|
|
function boundsRingEnd() {
|
|
boundsRingPoint(lambda00, phi00);
|
|
areaStream.lineEnd();
|
|
if (abs(deltaSum) > epsilon) lambda0 = -(lambda1 = 180);
|
|
range[0] = lambda0, range[1] = lambda1;
|
|
p0 = null;
|
|
}
|
|
|
|
// Finds the left-right distance between two longitudes.
|
|
// This is almost the same as (lambda1 - lambda0 + 360°) % 360°, except that we want
|
|
// the distance between ±180° to be 360°.
|
|
function angle(lambda0, lambda1) {
|
|
return (lambda1 -= lambda0) < 0 ? lambda1 + 360 : lambda1;
|
|
}
|
|
|
|
function rangeCompare(a, b) {
|
|
return a[0] - b[0];
|
|
}
|
|
|
|
function rangeContains(range, x) {
|
|
return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;
|
|
}
|
|
|
|
export default function(feature) {
|
|
var i, n, a, b, merged, deltaMax, delta;
|
|
|
|
phi1 = lambda1 = -(lambda0 = phi0 = Infinity);
|
|
ranges = [];
|
|
stream(feature, boundsStream);
|
|
|
|
// First, sort ranges by their minimum longitudes.
|
|
if (n = ranges.length) {
|
|
ranges.sort(rangeCompare);
|
|
|
|
// Then, merge any ranges that overlap.
|
|
for (i = 1, a = ranges[0], merged = [a]; i < n; ++i) {
|
|
b = ranges[i];
|
|
if (rangeContains(a, b[0]) || rangeContains(a, b[1])) {
|
|
if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];
|
|
if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];
|
|
} else {
|
|
merged.push(a = b);
|
|
}
|
|
}
|
|
|
|
// Finally, find the largest gap between the merged ranges.
|
|
// The final bounding box will be the inverse of this gap.
|
|
for (deltaMax = -Infinity, n = merged.length - 1, i = 0, a = merged[n]; i <= n; a = b, ++i) {
|
|
b = merged[i];
|
|
if ((delta = angle(a[1], b[0])) > deltaMax) deltaMax = delta, lambda0 = b[0], lambda1 = a[1];
|
|
}
|
|
}
|
|
|
|
ranges = range = null;
|
|
|
|
return lambda0 === Infinity || phi0 === Infinity
|
|
? [[NaN, NaN], [NaN, NaN]]
|
|
: [[lambda0, phi0], [lambda1, phi1]];
|
|
}
|