Skip to content

Commit

Permalink
Merge branch 'main' into fil/framework
Browse files Browse the repository at this point in the history
  • Loading branch information
Fil authored Oct 6, 2024
2 parents 7bc9d27 + c7953a0 commit e2869cb
Show file tree
Hide file tree
Showing 16 changed files with 161 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [20.x]
node-version: [20]

steps:
- uses: actions/checkout@v4
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ const projection = d3.geoEquirectangular()

Given a GeoJSON *polygon* or *multipolygon*, returns a clip function suitable for [_projection_.preclip](https://github.com/d3/d3-geo#preclip).

<a name="polygon" href="#polygon">#</a> clip.<b>polygon</b>()
<a name="polygon" href="#polygon">#</a> clip.<b>polygon</b>([<i>geometry</i>])

Given a clipPolygon function, returns the GeoJSON polygon.
If <i>geometry</i> is specified, sets the clipping polygon to the geometry and returns a new <i>clip</i> function. Otherwise returns the clipping polygon.

<a name="polygon" href="#clipPoint">#</a> clip.<b>clipPoint</b>([<i>clipPoint</i>])

Whether the projection should clip points. If <i>clipPoint</i> is false, the clip function only clips line and polygon geometries. If <i>clipPoint</i> is true, points outside the clipping polygon are not projected. Typically set to false when the projection covers the whole sphere, to make sure that all points —even those on the edge of the clipping polygon— get projected.

<a name="geoIntersectArc" href="#geoIntersectArc">#</a> d3.<b>geoIntersectArc</b>(<i>arcs</i>) · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/intersect.js), [Examples](https://observablehq.com/@fil/spherical-intersection)

Expand All @@ -58,6 +62,8 @@ d3-geo-polygon adds polygon clipping to the polyhedral and interrupted projectio

Defines a new polyhedral projection. The *tree* is a spanning tree of polygon face nodes; each *node* is assigned a *node*.transform matrix. The *face* function returns the appropriate *node* for a given *lambda* and *phi* in radians.

Polyhedral projections’ default **clipPoint** depends on whether the clipping polygon covers the whole sphere. When the polygon’s area is almost complete (larger than 4π minus .1 steradian), clipPoint is set to false, and all point geometries are displayed, even if they (technically) fall outside the clipping polygon. For smaller polygons, clipPoint defaults to true, thus hiding points outside the clipping region.

<a href="#geoPolyhedral_tree" name="geoPolyhedral_tree">#</a> <i>polyhedral</i>.<b>tree</b>() returns the spanning tree of the polyhedron, from which one can infer the faces’ centers, polygons, shared edges etc.

<a href="#geoPolyhedralButterfly" name="geoPolyhedralButterfly">#</a> d3.<b>geoPolyhedralButterfly</b>() · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/polyhedral/butterfly.js)
Expand Down Expand Up @@ -132,6 +138,17 @@ Alan K. Philbrick’s interrupted sinu-Mollweide projection.

An interrupted sinusoidal projection with asymmetrical lobe boundaries.

<a href="#geoTwoPointEquidistant" name="geoTwoPointEquidistant">#</a> d3.<b>geoTwoPointEquidistant</b>(point0, point1) · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js)

The two-point equidistant projection, displaying 99.9996% of the sphere thanks to polygon clipping.

<a href="#geoTwoPointEquidistantUsa" name="geoTwoPointEquidistantUsa">#</a> d3.<b>geoTwoPointEquidistantUsa</b>() · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js)

[<img src="https://raw.githubusercontent.com/d3/d3-geo-polygon/main/test/snapshots/twoPointEquidistantUsa.png" width="480" height="250">](https://observablehq.com/@d3/two-point-equidistant)

The two-point equidistant projection with points [-158°, 21.5°] and [-77°, 39°], approximately representing Honolulu, HI and Washington, D.C.

### New projections

New projections are introduced:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-geo-polygon",
"version": "1.12.1",
"version": "2.0.0-alpha.1",
"description": "Clipping and geometric operations for spherical polygons.",
"homepage": "https://github.com/d3/d3-geo-polygon",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/clip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {epsilon, halfPi} from "../math.js";
import polygonContains from "../polygonContains.js";
import {merge} from "d3-array";

export default function(pointVisible, clipLine, interpolate, start, sort) {
export default function(pointVisible, clipLine, interpolate, start, sort, {clipPoint = false} = {}) {
if (typeof sort === "undefined") sort = compareIntersection;

return function(sink) {
Expand Down Expand Up @@ -47,7 +47,7 @@ export default function(pointVisible, clipLine, interpolate, start, sort) {
};

function point(lambda, phi) {
if (pointVisible(lambda, phi)) sink.point(lambda, phi);
if ((!clipPoint && !ring) || pointVisible(lambda, phi)) sink.point(lambda, phi);
}

function pointLine(lambda, phi) {
Expand Down
24 changes: 10 additions & 14 deletions src/clip/polygon.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ import polygonContains from "../polygonContains.js";
const clipNone = (stream) => stream;

// clipPolygon
export default function(geometry) {
function clipGeometry(geometry) {
let polygons;

if (geometry.type === "MultiPolygon") {
polygons = geometry.coordinates;
} else if (geometry.type === "Polygon") {
polygons = [geometry.coordinates];
} else {
return clipNone;
}
export default function (geometry) {
let clipPoint = true;

const clips = polygons.map((polygon) => {
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?
Expand All @@ -28,7 +22,8 @@ export default function(geometry) {
clipLine(segments, pointVisible),
interpolate(segments, polygon),
polygon[0][0],
clipPolygonSort
clipPolygonSort,
{clipPoint}
);
});

Expand Down Expand Up @@ -56,7 +51,8 @@ export default function(geometry) {
};
}

clipPolygon.polygon = (_) => _ ? ((geometry = _), clipGeometry(geometry)) : geometry;
clipPolygon.polygon = (_) => _ !== undefined ? clipGeometry(geometry = _) : geometry;
clipPolygon.clipPoint = (_) => _ !== undefined ? ((clipPoint = !!_), clipGeometry(geometry)) : clipPoint;

return clipPolygon;
}
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ export {
geoInterruptedMollweide,
geoInterruptedMollweideHemispheres,
geoInterruptedSinuMollweide,
geoInterruptedSinusoidal
geoInterruptedSinusoidal,
geoTwoPointEquidistant,
geoTwoPointEquidistantUsa
} from "./reclip.js";
11 changes: 6 additions & 5 deletions src/polyhedral/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {geoBounds as bounds, geoCentroid as centroid, geoInterpolate as interpolate, geoProjection as projection} from "d3-geo";
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";
Expand Down Expand Up @@ -78,10 +78,11 @@ export default function(tree, face) {
const proj = projection(forward);

// run around the mesh of faces and stream all vertices to create the clipping polygon
const polygon = [];
outline({point: function(lambda, phi) { polygon.push([lambda, phi]); }}, tree);
polygon.push(polygon[0]);
proj.preclip(clipPolygon({ type: "Polygon", coordinates: [ 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;
Expand Down
22 changes: 21 additions & 1 deletion src/reclip.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {merge} from "d3-array";
import {geoInterpolate} from "d3-geo";
import {geoDistance, geoInterpolate} from "d3-geo";
import {
geoBerghaus as berghaus,
geoGingery as gingery,
Expand All @@ -11,6 +11,7 @@ import {
geoInterruptedMollweideHemispheres as interruptedMollweideHemispheres,
geoInterruptedSinuMollweide as interruptedSinuMollweide,
geoInterruptedSinusoidal as interruptedSinusoidal,
geoTwoPointEquidistant as twoPointEquidistant
} from "d3-geo-projection";
import geoClipPolygon from "./clip/polygon.js";

Expand All @@ -27,6 +28,8 @@ export function geoInterruptedMollweide() { return clipInterrupted(interruptedMo
export function geoInterruptedMollweideHemispheres() { return clipInterrupted(interruptedMollweideHemispheres.apply(this, arguments)); }
export function geoInterruptedSinuMollweide() { return clipInterrupted(interruptedSinuMollweide.apply(this, arguments)); }
export function geoInterruptedSinusoidal() { return clipInterrupted(interruptedSinusoidal.apply(this, arguments)); }
export function geoTwoPointEquidistant() { return clipTwoPointEquidistant.apply(this, arguments); }
export function geoTwoPointEquidistantUsa() { return geoTwoPointEquidistant([-158, 21.5], [-77, 39]); }

function reclip(projection, vertical = false) {
const {lobes} = projection;
Expand Down Expand Up @@ -97,3 +100,20 @@ function clipInterrupted(projection) {

return reset(projection);
}

function clipTwoPointEquidistant(a, b) {
const epsilon = 1e-3;
const u = geoDistance(a, b) * 90 / Math.PI + epsilon;
const ellipse = {
type: "Polygon",
coordinates: [[
[180 - u, epsilon],
[180 - u, -epsilon],
[-180 + u, -epsilon],
[-180 + u, epsilon],
[180 - u, epsilon]
]
]
};
return twoPointEquidistant(a, b).preclip(geoClipPolygon(ellipse).clipPoint(false));
}
59 changes: 19 additions & 40 deletions src/tetrahedralLee.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
geoProjection as projection,
geoStereographicRaw,
geoCentroid,
geoContains,
geoCentroid
} from "d3-geo";
import polyhedral from "./polyhedral/index.js";
import { greatest } from "d3-array";
import { abs, asin, degrees, sqrt } from "./math.js";
import {
Expand All @@ -15,9 +13,9 @@ import {
complexSub,
} from "./complex.js";
import { solve2d } from "./newton.js";
import voronoi from "./polyhedral/voronoi.js";

export function leeRaw(lambda, phi) {
// return d3.geoGnomonicRaw(...arguments);
const w = [-1 / 2, sqrt(3) / 2];
let k = [0, 0],
h = [0, 0],
Expand Down Expand Up @@ -104,40 +102,21 @@ const tetrahedron = [
[0, 1, 3],
].map((face) => face.map((i) => centers[i]));

export default function (faceProjection = (face) => {
const c = geoCentroid({ type: "MultiPoint", coordinates: face });
const rotate = (abs(c[1]) == 90) ? [0, -c[1], -30] : [-c[0], -c[1], 30];
return projection(leeRaw).scale(1).translate([0, 0]).rotate(rotate);
}) {
const faces = tetrahedron.map((face) => ({ face, project: faceProjection(face) }));

[-1, 0, 0, 0].forEach((d, i) => {
const node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});

const p = polyhedral(faces[0], (lambda, phi) => {
lambda *= degrees;
phi *= degrees;
for (let i = 0; i < faces.length; ++i) {
if (
geoContains(
{
type: "Polygon",
coordinates: [ [...tetrahedron[i], tetrahedron[i][0]] ],
},
[lambda, phi]
)
) {
return faces[i];
}
}
});

return p
.rotate([30, 180]) // North Pole aspect, needs clipPolygon
// .rotate([-30, 0]) // South Pole aspect
.angle(30)
.scale(118.662)
.translate([480, 195.47]);
export default function (
faceProjection = (face) => {
const c = geoCentroid({ type: "MultiPoint", coordinates: face });
const rotate = abs(c[1]) == 90 ? [0, -c[1], -30] : [-c[0], -c[1], 30];
return projection(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]);
}
Loading

0 comments on commit e2869cb

Please sign in to comment.