d3-geo-polygon
Version:
Clipping and geometric operations for spherical polygons.
249 lines (233 loc) • 8.09 kB
JavaScript
import clip from "./index.js";
import {atan2, cos, max, min, pi, radians, sign, sin, sqrt} from "../math.js";
import {cartesian, cartesianCross, cartesianDot, cartesianEqual, spherical} from "../cartesian.js";
import {intersectCoincident, intersectPointOnLine, intersectSegment, intersect} from "../intersect.js";
import polygonContains from "../polygonContains.js";
const clipNone = (stream) => stream;
// clipPolygon
export default function (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(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(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(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([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([lambda, phi])));
(i = -1), --j;
intersections.length = 0;
continue;
}
const sph = spherical(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([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);
}
};
};
}