Skip to content

Commit

Permalink
d3-geo-polygon exports its own versions of geoPolyhedral, geoPolyhedr…
Browse files Browse the repository at this point in the history
…alButterfly, geoPolyhedralCollignon and geoPolyhedralWaterman, with minimal code duplication.

With this version, d3/d3-geo-projection#132 is not needed anymore.

(If you need to use both d3-geo-projection and d3-geo-polygon, they should be included in that order.)
  • Loading branch information
Fil committed Mar 4, 2018
1 parent 9513bb8 commit 2649e4d
Show file tree
Hide file tree
Showing 13 changed files with 541 additions and 6 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,31 @@ Given a GeoJSON *polygon*, returns a clip function suitable for [_projection_.pr

Given a clipPolygon function, returns the GeoJSON polygon.



## Projections

d3-geo-polygon adds polygon clipping to the polyhedral projections from [d3-geo-projection](https://github.com/d3/d3-geo-projection). Thus, it supercedes the following symbols:


<a href="#geoPolyhedral" name="geoPolyhedral">#</a> d3.<b>geoPolyhedral</b>(<i>root</i>, <i>face</i>[, <i>angle</i>]) [<>](https://github.com/d3/d3-geo-polygon/blob/master/src/polyhedral/index.js "Source")

Defines a new polyhedral projection. The *root* 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. The specified rotation *angle* is applied to the polyhedron; if an *angle* is not specified, it defaults to -π / 6 (for butterflies).

<a href="#geoPolyhedralButterfly" name="geoPolyhedralButterfly">#</a> d3.<b>geoPolyhedralButterfly</b>() [<>](https://github.com/d3/d3-geo-polygon/blob/master/src/polyhedral/butterfly.js "Source")

[<img src="https://raw.githubusercontent.com/d3/d3-geo-polygon/master/img/polyhedralButterfly.png" width="480" height="250">](http://bl.ocks.org/mbostock/4458680)

The gnomonic butterfly projection.

<a href="#geoPolyhedralCollignon" name="geoPolyhedralCollignon">#</a> d3.<b>geoPolyhedralCollignon</b>() [<>](https://github.com/d3/d3-geo-polygon/blob/master/src/polyhedral/collignon.js "Source")

[<img src="https://raw.githubusercontent.com/d3/d3-geo-polygon/master/img/polyhedralCollignon.png" width="480" height="250">](https://www.jasondavies.com/maps/collignon-butterfly/)

The Collignon butterfly projection.

<a href="#geoPolyhedralWaterman" name="geoPolyhedralWaterman">#</a> d3.<b>geoPolyhedralWaterman</b>() [<>](https://github.com/d3/d3-geo-polygon/blob/master/src/polyhedral/waterman.js "Source")

[<img src="https://raw.githubusercontent.com/d3/d3-geo-polygon/master/img/polyhedralWaterman.png" width="480" height="250">](https://www.jasondavies.com/maps/waterman-butterfly/)

A butterfly projection inspired by Steve Waterman’s design.
Binary file added img/polyhedralButterfly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/polyhedralCollignon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/polyhedralWaterman.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
export {default as geoClipPolygon} from "./src/clip/polygon";
export {default as geoPolyhedral} from "./src/polyhedral/index";
export {default as geoPolyhedralButterfly} from "./src/polyhedral/butterfly.js";
export {default as geoPolyhedralCollignon} from "./src/polyhedral/collignon.js";
export {default as geoPolyhedralWaterman} from "./src/polyhedral/waterman.js";

// if necessary, the following line could export a copy of the d3-geo-projection versions under the names xxxxUnclipped
// export {geoPolyhedral as geoPolyhedralUnclipped, geoPolyhedralButterfly as geoPolyhedralButterflyUnclipped, geoPolyhedralCollignon as geoPolyhedralCollignonUnclipped, geoPolyhedralWaterman as geoPolyhedralWatermanUnclipped} from "./node_modules/d3-geo-projection/index";

// if necessary, the following line could export a copy of these versions under the names xxxxClipped
// export {default as geoPolyhedral, default as geoPolyhedralClipped} from "./src/polyhedral/index";
// export {default as geoPolyhedralButterfly, default as geoPolyhedralButterflyClipped} from "./src/polyhedral/butterfly.js";
// export {default as geoPolyhedralCollignon, default as geoPolyhedralCollignonClipped} from "./src/polyhedral/collignon.js";
// export {default as geoPolyhedralWaterman, default as geoPolyhedralWatermanClipped} from "./src/polyhedral/waterman.js";
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-geo-polygon",
"version": "1.1.1",
"version": "1.2.0",
"description": "Clipping and geometric operations for spherical polygons.",
"keywords": [
"d3",
Expand Down Expand Up @@ -35,14 +35,15 @@
"scripts": {
"pretest": "rm -rf build && mkdir build && rollup -c --banner \"$(preamble)\"",
"test": "tape 'test/**/*-test.js' && eslint index.js src",
"prepublishOnly": "npm run test && uglifyjs -b beautify=false,preamble=\"'$(preamble)'\" build/d3-geo-polygon.js -c -m -o build/d3-geo-polygon.min.js",
"prepublishOnly": "npm run test && mkdir -p test/output && test/compare-images && uglifyjs -b beautify=false,preamble=\"'$(preamble)'\" build/d3-geo-polygon.js -c -m -o build/d3-geo-polygon.min.js",
"postpublish": "git push && git push --tags && zip -j build/d3-geo-polygon.zip -- LICENSE README.md build/d3-geo-polygon.js build/d3-geo-polygon.min.js"
},
"dependencies": {
"d3-array": "1"
"d3-array": "1",
"d3-geo-projection": "2"
},
"devDependencies": {
"d3-geo": "1",
"d3-geo": "^1.9",
"eslint": "4",
"package-preamble": "0.1",
"rollup": "0.50",
Expand Down
8 changes: 6 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
export default {
input: "index",
external: [
"d3-array"
"d3-array",
"d3-geo",
"d3-geo-projection"
],
output: {
extend: true,
file: "build/d3-geo-polygon.js",
format: "umd",
globals: {
"d3-array": "d3"
"d3-array": "d3",
"d3-geo": "d3",
"d3-geo-projection": "d3"
},
name: "d3"
}
Expand Down
31 changes: 31 additions & 0 deletions src/polyhedral/butterfly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {geoCentroid as centroid, geoGnomonic as gnomonic} from "d3-geo";
import {pi} from "../../node_modules/d3-geo-projection/src/math";
import polyhedral from "./index";
import octahedron from "../../node_modules/d3-geo-projection/src/polyhedral/octahedron";
"./octahedron";

export default function(faceProjection) {

faceProjection = faceProjection || function(face) {
var c = centroid({type: "MultiPoint", coordinates: face});
return gnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]);
};

var faces = octahedron.map(function(face) {
return {face: face, project: faceProjection(face)};
});

[-1, 0, 0, 1, 0, 1, 4, 5].forEach(function(d, i) {
var node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});

return polyhedral(faces[0], function(lambda, phi) {
return 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];
})
.scale(101.858)
.center([0, 45]);
}
42 changes: 42 additions & 0 deletions src/polyhedral/collignon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {geoCentroid as centroid, geoProjection as projection} from "d3-geo";
import {geoCollignonRaw as collignonRaw} from "d3-geo-projection";
import {pi, sqrt} from "../../node_modules/d3-geo-projection/src/math";
import polyhedral from "./index";
import octahedron from "../../node_modules/d3-geo-projection/src/polyhedral/octahedron";

var kx = 2 / sqrt(3);

function collignonK(a, b) {
var p = collignonRaw(a, b);
return [p[0] * kx, p[1]];
}

collignonK.invert = function(x,y) {
return collignonRaw.invert(x / kx, y);
};

export default function(faceProjection) {

faceProjection = faceProjection || function(face) {
var c = centroid({type: "MultiPoint", coordinates: face});
return projection(collignonK).translate([0, 0]).scale(1).rotate(c[1] > 0 ? [-c[0], 0] : [180 - c[0], 180]);
};

var faces = octahedron.map(function(face) {
return {face: face, project: faceProjection(face)};
});

[-1, 0, 0, 1, 0, 1, 4, 5].forEach(function(d, i) {
var node = faces[d];
node && (node.children || (node.children = [])).push(faces[i]);
});

return polyhedral(faces[0], function(lambda, phi) {
return 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];
})
.scale(121.906)
.center([0, 48.5904]);
}
194 changes: 194 additions & 0 deletions src/polyhedral/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {geoBounds as bounds, geoCentroid as centroid, geoInterpolate as interpolate, geoProjection as projection} from "d3-geo";
import {default as clipPolygon} from "../clip/polygon";
import {abs, cos, degrees, epsilon, pi, radians, sin} from "../../node_modules/d3-geo-projection/src/math";
import {default as matrix, multiply, inverse} from "../../node_modules/d3-geo-projection/src/polyhedral/matrix";

// Creates a polyhedral projection.
// * root: 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).
// * r: rotation angle for final polyhedral net. Defaults to -pi / 6 (for
// butterflies).
export default function(root, face, r) {

r = r == null ? -pi / 6 : r; // TODO automate

recurse(root, {transform: [
cos(r), sin(r), 0,
-sin(r), cos(r), 0
]});

function recurse(node, parent) {
node.edges = faceEdges(node.face);
// Find shared edge.
if (parent.face) {
var shared = node.shared = sharedEdge(node.face, parent.face),
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.
var edges = parent.edges;
for (var 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 (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(function(child) {
recurse(child, node);
});
}
return node;
}

function forward(lambda, phi) {
var node = face(lambda, phi),
point = node.project([lambda * degrees, phi * degrees]),
t;
if (t = node.transform) {
return [
t[0] * point[0] + t[1] * point[1] + t[2],
-(t[3] * point[0] + t[4] * point[1] + t[5])
];
}
point[1] = -point[1];
return point;
}

// Naive inverse! A faster solution would use bounding boxes, or even a
// polygonal quadtree.
if (hasInverse(root)) forward.invert = function(x, y) {
var coordinates = faceInvert(root, [x, -y]);
return coordinates && (coordinates[0] *= radians, coordinates[1] *= radians, coordinates);
};

function faceInvert(node, coordinates) {
var invert = node.project.invert,
t = node.transform,
point = coordinates;
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;
var p,
children = node.children;
for (var i = 0, n = children && children.length; i < n; ++i) {
if (p = faceInvert(children[i], coordinates)) return p;
}
}

function faceDegrees(coordinates) {
return face(coordinates[0] * radians, coordinates[1] * radians);
}

var proj = projection(forward),
stream_ = proj.stream;

// if d3-geo-polygon and proj.preclip are available:
// run around the mesh of faces and stream all vertices to create the clipping polygon
if (clipPolygon && proj.preclip) {
var polygon = [];
outline({point: function(lambda, phi) { polygon.push([lambda, phi]); }}, root);
polygon.push(polygon[0]);
proj.preclip(clipPolygon({ type: "Polygon", coordinates: [ polygon ] }));
}

function noClip(s) { return s; }

proj.stream = function(stream) {
var rotate = proj.rotate(),
preclip = proj.preclip ? proj.preclip() : null,
rotateStream = stream_(stream),
sphereStream = ((preclip ? proj.preclip(noClip) : proj).rotate([0, 0]), stream_(stream));
proj.rotate(rotate);
if (preclip) proj.preclip(preclip);
rotateStream.sphere = function() {
sphereStream.polygonStart();
sphereStream.lineStart();
outline(sphereStream, root);
sphereStream.lineEnd();
sphereStream.polygonEnd();
};
return rotateStream;
};

return proj;
}

function outline(stream, node, parent) {
var 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 (var 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);
}
}
}

// Tests equality of two spherical points.
function pointEqual(a, b) {
return a && b && a[0] === b[0] && a[1] === b[1];
}

// Finds a shared edge given two clockwise polygons.
function sharedEdge(a, b) {
var x, y, n = a.length, found = null;
for (var i = 0; i < n; ++i) {
x = a[i];
for (var 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) {
var n = face.length,
edges = [];
for (var a = face[n - 1], i = 0; i < n; ++i) edges.push([a, a = face[i]]);
return edges;
}

function hasInverse(node) {
return node.project.invert || node.children && node.children.some(hasInverse);
}
Loading

0 comments on commit 2649e4d

Please sign in to comment.