├── src ├── noop.js ├── identity.js ├── constant.js ├── pointEqual.js ├── distance.js ├── projection │ ├── albers.js │ ├── equirectangular.js │ ├── cylindricalEqualArea.js │ ├── orthographic.js │ ├── gnomonic.js │ ├── conic.js │ ├── stereographic.js │ ├── azimuthalEqualArea.js │ ├── azimuthalEquidistant.js │ ├── azimuthal.js │ ├── transverseMercator.js │ ├── conicEquidistant.js │ ├── conicEqualArea.js │ ├── naturalEarth1.js │ ├── equalEarth.js │ ├── conicConformal.js │ ├── mercator.js │ ├── fit.js │ ├── identity.js │ ├── resample.js │ ├── albersUsa.js │ └── index.js ├── compose.js ├── clip │ ├── buffer.js │ ├── extent.js │ ├── line.js │ ├── antimeridian.js │ ├── rejoin.js │ ├── index.js │ ├── rectangle.js │ └── circle.js ├── path │ ├── bounds.js │ ├── measure.js │ ├── context.js │ ├── area.js │ ├── string.js │ ├── index.js │ └── centroid.js ├── transform.js ├── math.js ├── interpolate.js ├── adder.js ├── cartesian.js ├── length.js ├── area.js ├── stream.js ├── circle.js ├── rotation.js ├── index.js ├── polygonContains.js ├── contains.js ├── graticule.js ├── centroid.js └── bounds.js ├── img ├── albers.png ├── gnomonic.png ├── mercator.png ├── albersUsa.png ├── equalEarth.png ├── graticule.png ├── angleorient30.png ├── naturalEarth1.png ├── orthographic.png ├── stereographic.png ├── conicConformal.png ├── conicEqualArea.png ├── equirectangular.png ├── azimuthalEqualArea.png ├── conicEquidistant.png ├── transverseMercator.png ├── albersUsa-parameters.png └── azimuthalEquidistant.png ├── .gitignore ├── d3-geo.sublime-project ├── .eslintrc.json ├── test ├── path │ ├── test-context.js │ ├── area-test.js │ ├── bounds-test.js │ ├── measure-test.js │ ├── string-test.js │ ├── index-test.js │ └── centroid-test.js ├── distance-test.js ├── projection │ ├── stereographic-test.js │ ├── rotate-test.js │ ├── albersUsa-test.js │ ├── projectionEqual.js │ ├── invert-test.js │ ├── angle-test.js │ ├── transverseMercator-test.js │ ├── mercator-test.js │ └── equirectangular-test.js ├── pathEqual.js ├── interpolate-test.js ├── inDelta.js ├── render-reference ├── render-us ├── compute-scale ├── rotation-test.js ├── circle-test.js ├── render-world ├── compare-images ├── length-test.js ├── contains-test.js ├── stream-test.js ├── area-test.js └── graticule-test.js ├── rollup.config.js ├── package.json └── LICENSE /src/noop.js: -------------------------------------------------------------------------------- 1 | export default function noop() {} 2 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | export default function(x) { 2 | return x; 3 | } 4 | -------------------------------------------------------------------------------- /img/albers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/albers.png -------------------------------------------------------------------------------- /img/gnomonic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/gnomonic.png -------------------------------------------------------------------------------- /img/mercator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/mercator.png -------------------------------------------------------------------------------- /img/albersUsa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/albersUsa.png -------------------------------------------------------------------------------- /img/equalEarth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/equalEarth.png -------------------------------------------------------------------------------- /img/graticule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/graticule.png -------------------------------------------------------------------------------- /img/angleorient30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/angleorient30.png -------------------------------------------------------------------------------- /img/naturalEarth1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/naturalEarth1.png -------------------------------------------------------------------------------- /img/orthographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/orthographic.png -------------------------------------------------------------------------------- /img/stereographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/stereographic.png -------------------------------------------------------------------------------- /img/conicConformal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/conicConformal.png -------------------------------------------------------------------------------- /img/conicEqualArea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/conicEqualArea.png -------------------------------------------------------------------------------- /img/equirectangular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/equirectangular.png -------------------------------------------------------------------------------- /img/azimuthalEqualArea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/azimuthalEqualArea.png -------------------------------------------------------------------------------- /img/conicEquidistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/conicEquidistant.png -------------------------------------------------------------------------------- /img/transverseMercator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/transverseMercator.png -------------------------------------------------------------------------------- /img/albersUsa-parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/albersUsa-parameters.png -------------------------------------------------------------------------------- /img/azimuthalEquidistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-it/d3-geo/master/img/azimuthalEquidistant.png -------------------------------------------------------------------------------- /src/constant.js: -------------------------------------------------------------------------------- 1 | export default function(x) { 2 | return function() { 3 | return x; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | .DS_Store 3 | dist/ 4 | test/output/ 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /src/pointEqual.js: -------------------------------------------------------------------------------- 1 | import {abs, epsilon} from "./math.js"; 2 | 3 | export default function(a, b) { 4 | return abs(a[0] - b[0]) < epsilon && abs(a[1] - b[1]) < epsilon; 5 | } 6 | -------------------------------------------------------------------------------- /src/distance.js: -------------------------------------------------------------------------------- 1 | import length from "./length.js"; 2 | 3 | var coordinates = [null, null], 4 | object = {type: "LineString", coordinates: coordinates}; 5 | 6 | export default function(a, b) { 7 | coordinates[0] = a; 8 | coordinates[1] = b; 9 | return length(object); 10 | } 11 | -------------------------------------------------------------------------------- /src/projection/albers.js: -------------------------------------------------------------------------------- 1 | import conicEqualArea from "./conicEqualArea.js"; 2 | 3 | export default function() { 4 | return conicEqualArea() 5 | .parallels([29.5, 45.5]) 6 | .scale(1070) 7 | .translate([480, 250]) 8 | .rotate([96, 0]) 9 | .center([-0.6, 38.7]); 10 | } 11 | -------------------------------------------------------------------------------- /src/compose.js: -------------------------------------------------------------------------------- 1 | export default function(a, b) { 2 | 3 | function compose(x, y) { 4 | return x = a(x, y), b(x[0], x[1]); 5 | } 6 | 7 | if (a.invert && b.invert) compose.invert = function(x, y) { 8 | return x = b.invert(x, y), x && a.invert(x[0], x[1]); 9 | }; 10 | 11 | return compose; 12 | } 13 | -------------------------------------------------------------------------------- /src/projection/equirectangular.js: -------------------------------------------------------------------------------- 1 | import projection from "./index.js"; 2 | 3 | export function equirectangularRaw(lambda, phi) { 4 | return [lambda, phi]; 5 | } 6 | 7 | equirectangularRaw.invert = equirectangularRaw; 8 | 9 | export default function() { 10 | return projection(equirectangularRaw) 11 | .scale(152.63); 12 | } 13 | -------------------------------------------------------------------------------- /src/projection/cylindricalEqualArea.js: -------------------------------------------------------------------------------- 1 | import {asin, cos, sin} from "../math.js"; 2 | 3 | export function cylindricalEqualAreaRaw(phi0) { 4 | var cosPhi0 = cos(phi0); 5 | 6 | function forward(lambda, phi) { 7 | return [lambda * cosPhi0, sin(phi) / cosPhi0]; 8 | } 9 | 10 | forward.invert = function(x, y) { 11 | return [x / cosPhi0, asin(y * cosPhi0)]; 12 | }; 13 | 14 | return forward; 15 | } 16 | -------------------------------------------------------------------------------- /d3-geo.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "file_exclude_patterns": ["*.sublime-workspace"], 6 | "folder_exclude_patterns": ["dist"] 7 | } 8 | ], 9 | "build_systems": [ 10 | { 11 | "name": "yarn test", 12 | "cmd": ["yarn", "test"], 13 | "file_regex": "\\((...*?):([0-9]*):([0-9]*)\\)", 14 | "working_dir": "$project_path" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:es5/no-es2016" 5 | ], 6 | "plugins": [ 7 | "es5" 8 | ], 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": 8 12 | }, 13 | "env": { 14 | "es6": true, 15 | "node": true, 16 | "browser": true 17 | }, 18 | "rules": { 19 | "no-cond-assign": 0, 20 | "no-constant-condition": 0, 21 | "no-prototype-builtins": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/projection/orthographic.js: -------------------------------------------------------------------------------- 1 | import {asin, cos, epsilon, sin} from "../math.js"; 2 | import {azimuthalInvert} from "./azimuthal.js"; 3 | import projection from "./index.js"; 4 | 5 | export function orthographicRaw(x, y) { 6 | return [cos(y) * sin(x), sin(y)]; 7 | } 8 | 9 | orthographicRaw.invert = azimuthalInvert(asin); 10 | 11 | export default function() { 12 | return projection(orthographicRaw) 13 | .scale(249.5) 14 | .clipAngle(90 + epsilon); 15 | } 16 | -------------------------------------------------------------------------------- /src/projection/gnomonic.js: -------------------------------------------------------------------------------- 1 | import {atan, cos, sin} from "../math.js"; 2 | import {azimuthalInvert} from "./azimuthal.js"; 3 | import projection from "./index.js"; 4 | 5 | export function gnomonicRaw(x, y) { 6 | var cy = cos(y), k = cos(x) * cy; 7 | return [cy * sin(x) / k, sin(y) / k]; 8 | } 9 | 10 | gnomonicRaw.invert = azimuthalInvert(atan); 11 | 12 | export default function() { 13 | return projection(gnomonicRaw) 14 | .scale(144.049) 15 | .clipAngle(60); 16 | } 17 | -------------------------------------------------------------------------------- /src/projection/conic.js: -------------------------------------------------------------------------------- 1 | import {degrees, pi, radians} from "../math.js"; 2 | import {projectionMutator} from "./index.js"; 3 | 4 | export function conicProjection(projectAt) { 5 | var phi0 = 0, 6 | phi1 = pi / 3, 7 | m = projectionMutator(projectAt), 8 | p = m(phi0, phi1); 9 | 10 | p.parallels = function(_) { 11 | return arguments.length ? m(phi0 = _[0] * radians, phi1 = _[1] * radians) : [phi0 * degrees, phi1 * degrees]; 12 | }; 13 | 14 | return p; 15 | } 16 | -------------------------------------------------------------------------------- /src/projection/stereographic.js: -------------------------------------------------------------------------------- 1 | import {atan, cos, sin} from "../math.js"; 2 | import {azimuthalInvert} from "./azimuthal.js"; 3 | import projection from "./index.js"; 4 | 5 | export function stereographicRaw(x, y) { 6 | var cy = cos(y), k = 1 + cos(x) * cy; 7 | return [cy * sin(x) / k, sin(y) / k]; 8 | } 9 | 10 | stereographicRaw.invert = azimuthalInvert(function(z) { 11 | return 2 * atan(z); 12 | }); 13 | 14 | export default function() { 15 | return projection(stereographicRaw) 16 | .scale(250) 17 | .clipAngle(142); 18 | } 19 | -------------------------------------------------------------------------------- /src/projection/azimuthalEqualArea.js: -------------------------------------------------------------------------------- 1 | import {asin, sqrt} from "../math.js"; 2 | import {azimuthalRaw, azimuthalInvert} from "./azimuthal.js"; 3 | import projection from "./index.js"; 4 | 5 | export var azimuthalEqualAreaRaw = azimuthalRaw(function(cxcy) { 6 | return sqrt(2 / (1 + cxcy)); 7 | }); 8 | 9 | azimuthalEqualAreaRaw.invert = azimuthalInvert(function(z) { 10 | return 2 * asin(z / 2); 11 | }); 12 | 13 | export default function() { 14 | return projection(azimuthalEqualAreaRaw) 15 | .scale(124.75) 16 | .clipAngle(180 - 1e-3); 17 | } 18 | -------------------------------------------------------------------------------- /src/projection/azimuthalEquidistant.js: -------------------------------------------------------------------------------- 1 | import {acos, sin} from "../math.js"; 2 | import {azimuthalRaw, azimuthalInvert} from "./azimuthal.js"; 3 | import projection from "./index.js"; 4 | 5 | export var azimuthalEquidistantRaw = azimuthalRaw(function(c) { 6 | return (c = acos(c)) && c / sin(c); 7 | }); 8 | 9 | azimuthalEquidistantRaw.invert = azimuthalInvert(function(z) { 10 | return z; 11 | }); 12 | 13 | export default function() { 14 | return projection(azimuthalEquidistantRaw) 15 | .scale(79.4188) 16 | .clipAngle(180 - 1e-3); 17 | } 18 | -------------------------------------------------------------------------------- /test/path/test-context.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | var buffer = []; 3 | return { 4 | arc: function(x, y, r, a0, a1) { buffer.push({type: "arc", x: Math.round(x), y: Math.round(y), r: r}); }, 5 | moveTo: function(x, y) { buffer.push({type: "moveTo", x: Math.round(x), y: Math.round(y)}); }, 6 | lineTo: function(x, y) { buffer.push({type: "lineTo", x: Math.round(x), y: Math.round(y)}); }, 7 | closePath: function() { buffer.push({type: "closePath"}); }, 8 | result: function() { var result = buffer; buffer = []; return result; } 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/clip/buffer.js: -------------------------------------------------------------------------------- 1 | import noop from "../noop.js"; 2 | 3 | export default function() { 4 | var lines = [], 5 | line; 6 | return { 7 | point: function(x, y) { 8 | line.push([x, y]); 9 | }, 10 | lineStart: function() { 11 | lines.push(line = []); 12 | }, 13 | lineEnd: noop, 14 | rejoin: function() { 15 | if (lines.length > 1) lines.push(lines.pop().concat(lines.shift())); 16 | }, 17 | result: function() { 18 | var result = lines; 19 | lines = []; 20 | line = null; 21 | return result; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test/distance-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"); 3 | 4 | require("./inDelta"); 5 | 6 | tape("geoDistance(a, b) computes the great-arc distance in radians between the two points a and b", function(test) { 7 | test.equal(d3.geoDistance([0, 0], [0, 0]), 0); 8 | test.inDelta(d3.geoDistance([118 + 24 / 60, 33 + 57 / 60], [73 + 47 / 60, 40 + 38 / 60]), 3973 / 6371, 0.5); 9 | test.end(); 10 | }); 11 | 12 | tape("geoDistance(a, b) correctly computes small distances", function(test) { 13 | test.assert(d3.geoDistance([0, 0], [0, 1e-12]) > 0); 14 | test.end(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/path/bounds.js: -------------------------------------------------------------------------------- 1 | import noop from "../noop.js"; 2 | 3 | var x0 = Infinity, 4 | y0 = x0, 5 | x1 = -x0, 6 | y1 = x1; 7 | 8 | var boundsStream = { 9 | point: boundsPoint, 10 | lineStart: noop, 11 | lineEnd: noop, 12 | polygonStart: noop, 13 | polygonEnd: noop, 14 | result: function() { 15 | var bounds = [[x0, y0], [x1, y1]]; 16 | x1 = y1 = -(y0 = x0 = Infinity); 17 | return bounds; 18 | } 19 | }; 20 | 21 | function boundsPoint(x, y) { 22 | if (x < x0) x0 = x; 23 | if (x > x1) x1 = x; 24 | if (y < y0) y0 = y; 25 | if (y > y1) y1 = y; 26 | } 27 | 28 | export default boundsStream; 29 | -------------------------------------------------------------------------------- /src/clip/extent.js: -------------------------------------------------------------------------------- 1 | import clipRectangle from "./rectangle.js"; 2 | 3 | export default function() { 4 | var x0 = 0, 5 | y0 = 0, 6 | x1 = 960, 7 | y1 = 500, 8 | cache, 9 | cacheStream, 10 | clip; 11 | 12 | return clip = { 13 | stream: function(stream) { 14 | return cache && cacheStream === stream ? cache : cache = clipRectangle(x0, y0, x1, y1)(cacheStream = stream); 15 | }, 16 | extent: function(_) { 17 | return arguments.length ? (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1], cache = cacheStream = null, clip) : [[x0, y0], [x1, y1]]; 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/projection/azimuthal.js: -------------------------------------------------------------------------------- 1 | import {asin, atan2, cos, sin, sqrt} from "../math.js"; 2 | 3 | export function azimuthalRaw(scale) { 4 | return function(x, y) { 5 | var cx = cos(x), 6 | cy = cos(y), 7 | k = scale(cx * cy); 8 | return [ 9 | k * cy * sin(x), 10 | k * sin(y) 11 | ]; 12 | } 13 | } 14 | 15 | export function azimuthalInvert(angle) { 16 | return function(x, y) { 17 | var z = sqrt(x * x + y * y), 18 | c = angle(z), 19 | sc = sin(c), 20 | cc = cos(c); 21 | return [ 22 | atan2(x * sc, z * cc), 23 | asin(z && y * sc / z) 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/projection/stereographic-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("./projectionEqual"); 5 | 6 | tape("stereographic(point) returns the expected result", function(test) { 7 | var stereographic = d3.geoStereographic().translate([0, 0]).scale(1); 8 | test.projectionEqual(stereographic, [ 0, 0], [ 0, 0]); 9 | test.projectionEqual(stereographic, [-90, 0], [-1, 0]); 10 | test.projectionEqual(stereographic, [ 90, 0], [ 1, 0]); 11 | test.projectionEqual(stereographic, [ 0, -90], [ 0, 1]); 12 | test.projectionEqual(stereographic, [ 0, 90], [ 0, -1]); 13 | test.end(); 14 | }); 15 | -------------------------------------------------------------------------------- /test/pathEqual.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | var reNumber = /[-+]?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][-]?\d+)?/g; 4 | 5 | tape.Test.prototype.pathEqual = function(actual, expected) { 6 | actual = normalizePath(actual + ""); 7 | // expected = normalizePath(expected + ""); 8 | this._assert(actual === expected, { 9 | message: "should be equal", 10 | operator: "pathEqual", 11 | actual: actual, 12 | expected: expected 13 | }); 14 | }; 15 | 16 | function normalizePath(path) { 17 | return path.replace(reNumber, formatNumber); 18 | } 19 | 20 | function formatNumber(s) { 21 | return Math.abs((s = +s) - Math.round(s)) < 1e-6 ? Math.round(s) : s.toFixed(6); 22 | } 23 | -------------------------------------------------------------------------------- /test/interpolate-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"); 3 | 4 | require("./inDelta"); 5 | 6 | tape("geoInterpolate(a, a) returns a", function(test) { 7 | test.deepEqual(d3.geoInterpolate([140.63289, -29.95101], [140.63289, -29.95101])(0.5), [140.63289, -29.95101]); 8 | test.end(); 9 | }); 10 | 11 | tape("geoInterpolate(a, b) returns the expected values when a and b lie on the equator", function(test) { 12 | test.inDelta(d3.geoInterpolate([10, 0], [20, 0])(0.5), [15, 0], 1e-6); 13 | test.end(); 14 | }); 15 | 16 | tape("geoInterpolate(a, b) returns the expected values when a and b lie on a meridian", function(test) { 17 | test.inDelta(d3.geoInterpolate([10, -20], [10, 40])(0.5), [10, 10], 1e-6); 18 | test.end(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | export default function(methods) { 2 | return { 3 | stream: transformer(methods) 4 | }; 5 | } 6 | 7 | export function transformer(methods) { 8 | return function(stream) { 9 | var s = new TransformStream; 10 | for (var key in methods) s[key] = methods[key]; 11 | s.stream = stream; 12 | return s; 13 | }; 14 | } 15 | 16 | function TransformStream() {} 17 | 18 | TransformStream.prototype = { 19 | constructor: TransformStream, 20 | point: function(x, y) { this.stream.point(x, y); }, 21 | sphere: function() { this.stream.sphere(); }, 22 | lineStart: function() { this.stream.lineStart(); }, 23 | lineEnd: function() { this.stream.lineEnd(); }, 24 | polygonStart: function() { this.stream.polygonStart(); }, 25 | polygonEnd: function() { this.stream.polygonEnd(); } 26 | }; 27 | -------------------------------------------------------------------------------- /test/projection/rotate-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("../pathEqual"); 5 | 6 | tape("a rotation of a degenerate polygon should not break", function(test) { 7 | var projection = d3.geoMercator().rotate([-134.300, 25.776]).scale(750).translate([0, 0]); 8 | test.pathEqual(d3.geoPath(projection)({ 9 | "type": "Polygon", 10 | "coordinates": [ 11 | [ 12 | [125.67351590459046, -14.17673705310531], 13 | [125.67351590459046, -14.173276873687367], 14 | [125.67351590459046, -14.173276873687367], 15 | [125.67351590459046, -14.169816694269425], 16 | [125.67351590459046, -14.17673705310531] 17 | ] 18 | ] 19 | }), "M-111.644162,-149.157654L-111.647235,-149.203744L-111.647235,-149.203744L-111.650307,-149.249835Z"); 20 | test.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/projection/albersUsa-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("./projectionEqual"); 5 | 6 | tape("albersUsa(point) and albersUsa.invert(point) returns the expected result", function(test) { 7 | var albersUsa = d3.geoAlbersUsa(); 8 | test.projectionEqual(albersUsa, [-122.4194, 37.7749], [107.4, 214.1], 0.1); // San Francisco, CA 9 | test.projectionEqual(albersUsa, [ -74.0059, 40.7128], [794.6, 176.5], 0.1); // New York, NY 10 | test.projectionEqual(albersUsa, [ -95.9928, 36.1540], [488.8, 298.0], 0.1); // Tulsa, OK 11 | test.projectionEqual(albersUsa, [-149.9003, 61.2181], [171.2, 446.9], 0.1); // Anchorage, AK 12 | test.projectionEqual(albersUsa, [-157.8583, 21.3069], [298.5, 451.0], 0.1); // Honolulu, HI 13 | test.equal(albersUsa([2.3522, 48.8566]), null); // Paris, France 14 | test.end(); 15 | }); 16 | -------------------------------------------------------------------------------- /test/inDelta.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | tape.Test.prototype.inDelta = function(actual, expected, delta) { 4 | delta = delta || 1e-6; 5 | this._assert(inDelta(actual, expected, delta), { 6 | message: "should be in delta " + delta, 7 | operator: "inDelta", 8 | actual: actual, 9 | expected: expected 10 | }); 11 | }; 12 | 13 | function inDelta(actual, expected, delta) { 14 | return (Array.isArray(expected) ? inDeltaArray : inDeltaNumber)(actual, expected, delta); 15 | } 16 | 17 | function inDeltaArray(actual, expected, delta) { 18 | var n = expected.length, i = -1; 19 | if (actual.length !== n) return false; 20 | while (++i < n) if (!inDelta(actual[i], expected[i], delta)) return false; 21 | return true; 22 | } 23 | 24 | function inDeltaNumber(actual, expected, delta) { 25 | return actual >= expected - delta && actual <= expected + delta; 26 | } 27 | -------------------------------------------------------------------------------- /src/projection/transverseMercator.js: -------------------------------------------------------------------------------- 1 | import {atan, exp, halfPi, log, tan} from "../math.js"; 2 | import {mercatorProjection} from "./mercator.js"; 3 | 4 | export function transverseMercatorRaw(lambda, phi) { 5 | return [log(tan((halfPi + phi) / 2)), -lambda]; 6 | } 7 | 8 | transverseMercatorRaw.invert = function(x, y) { 9 | return [-y, 2 * atan(exp(x)) - halfPi]; 10 | }; 11 | 12 | export default function() { 13 | var m = mercatorProjection(transverseMercatorRaw), 14 | center = m.center, 15 | rotate = m.rotate; 16 | 17 | m.center = function(_) { 18 | return arguments.length ? center([-_[1], _[0]]) : (_ = center(), [_[1], -_[0]]); 19 | }; 20 | 21 | m.rotate = function(_) { 22 | return arguments.length ? rotate([_[0], _[1], _.length > 2 ? _[2] + 90 : 90]) : (_ = rotate(), [_[0], _[1], _[2] - 90]); 23 | }; 24 | 25 | return rotate([0, 0, 90]) 26 | .scale(159.155); 27 | } 28 | -------------------------------------------------------------------------------- /src/projection/conicEquidistant.js: -------------------------------------------------------------------------------- 1 | import {abs, atan2, cos, epsilon, sign, sin, sqrt} from "../math.js"; 2 | import {conicProjection} from "./conic.js"; 3 | import {equirectangularRaw} from "./equirectangular.js"; 4 | 5 | export function conicEquidistantRaw(y0, y1) { 6 | var cy0 = cos(y0), 7 | n = y0 === y1 ? sin(y0) : (cy0 - cos(y1)) / (y1 - y0), 8 | g = cy0 / n + y0; 9 | 10 | if (abs(n) < epsilon) return equirectangularRaw; 11 | 12 | function project(x, y) { 13 | var gy = g - y, nx = n * x; 14 | return [gy * sin(nx), g - gy * cos(nx)]; 15 | } 16 | 17 | project.invert = function(x, y) { 18 | var gy = g - y; 19 | return [atan2(x, abs(gy)) / n * sign(gy), g - sign(n) * sqrt(x * x + gy * gy)]; 20 | }; 21 | 22 | return project; 23 | } 24 | 25 | export default function() { 26 | return conicProjection(conicEquidistantRaw) 27 | .scale(131.154) 28 | .center([0, 13.9389]); 29 | } 30 | -------------------------------------------------------------------------------- /src/projection/conicEqualArea.js: -------------------------------------------------------------------------------- 1 | import {abs, asin, atan2, cos, epsilon, sign, sin, sqrt} from "../math.js"; 2 | import {conicProjection} from "./conic.js"; 3 | import {cylindricalEqualAreaRaw} from "./cylindricalEqualArea.js"; 4 | 5 | export function conicEqualAreaRaw(y0, y1) { 6 | var sy0 = sin(y0), n = (sy0 + sin(y1)) / 2; 7 | 8 | // Are the parallels symmetrical around the Equator? 9 | if (abs(n) < epsilon) return cylindricalEqualAreaRaw(y0); 10 | 11 | var c = 1 + sy0 * (2 * n - sy0), r0 = sqrt(c) / n; 12 | 13 | function project(x, y) { 14 | var r = sqrt(c - 2 * n * sin(y)) / n; 15 | return [r * sin(x *= n), r0 - r * cos(x)]; 16 | } 17 | 18 | project.invert = function(x, y) { 19 | var r0y = r0 - y; 20 | return [atan2(x, abs(r0y)) / n * sign(r0y), asin((c - (x * x + r0y * r0y) * n * n) / (2 * n))]; 21 | }; 22 | 23 | return project; 24 | } 25 | 26 | export default function() { 27 | return conicProjection(conicEqualAreaRaw) 28 | .scale(155.424) 29 | .center([0, 33.6442]); 30 | } 31 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {terser} from "rollup-plugin-terser"; 2 | import * as meta from "./package.json"; 3 | 4 | const config = { 5 | input: "src/index.js", 6 | external: Object.keys(meta.dependencies || {}).filter(key => /^d3-/.test(key)), 7 | output: { 8 | file: `dist/${meta.name}.js`, 9 | name: "d3", 10 | format: "umd", 11 | indent: false, 12 | extend: true, 13 | banner: `// ${meta.homepage} v${meta.version} Copyright ${(new Date).getFullYear()} ${meta.author.name}`, 14 | globals: Object.assign({}, ...Object.keys(meta.dependencies || {}).filter(key => /^d3-/.test(key)).map(key => ({[key]: "d3"}))) 15 | }, 16 | plugins: [] 17 | }; 18 | 19 | export default [ 20 | config, 21 | { 22 | ...config, 23 | output: { 24 | ...config.output, 25 | file: `dist/${meta.name}.min.js` 26 | }, 27 | plugins: [ 28 | ...config.plugins, 29 | terser({ 30 | output: { 31 | preamble: config.output.banner 32 | } 33 | }) 34 | ] 35 | } 36 | ]; 37 | -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | export var epsilon = 1e-6; 2 | export var epsilon2 = 1e-12; 3 | export var pi = Math.PI; 4 | export var halfPi = pi / 2; 5 | export var quarterPi = pi / 4; 6 | export var tau = pi * 2; 7 | 8 | export var degrees = 180 / pi; 9 | export var radians = pi / 180; 10 | 11 | export var abs = Math.abs; 12 | export var atan = Math.atan; 13 | export var atan2 = Math.atan2; 14 | export var cos = Math.cos; 15 | export var ceil = Math.ceil; 16 | export var exp = Math.exp; 17 | export var floor = Math.floor; 18 | export var log = Math.log; 19 | export var pow = Math.pow; 20 | export var sin = Math.sin; 21 | export var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; }; 22 | export var sqrt = Math.sqrt; 23 | export var tan = Math.tan; 24 | 25 | export function acos(x) { 26 | return x > 1 ? 0 : x < -1 ? pi : Math.acos(x); 27 | } 28 | 29 | export function asin(x) { 30 | return x > 1 ? halfPi : x < -1 ? -halfPi : Math.asin(x); 31 | } 32 | 33 | export function haversin(x) { 34 | return (x = sin(x / 2)) * x; 35 | } 36 | -------------------------------------------------------------------------------- /src/path/measure.js: -------------------------------------------------------------------------------- 1 | import adder from "../adder.js"; 2 | import {sqrt} from "../math.js"; 3 | import noop from "../noop.js"; 4 | 5 | var lengthSum = adder(), 6 | lengthRing, 7 | x00, 8 | y00, 9 | x0, 10 | y0; 11 | 12 | var lengthStream = { 13 | point: noop, 14 | lineStart: function() { 15 | lengthStream.point = lengthPointFirst; 16 | }, 17 | lineEnd: function() { 18 | if (lengthRing) lengthPoint(x00, y00); 19 | lengthStream.point = noop; 20 | }, 21 | polygonStart: function() { 22 | lengthRing = true; 23 | }, 24 | polygonEnd: function() { 25 | lengthRing = null; 26 | }, 27 | result: function() { 28 | var length = +lengthSum; 29 | lengthSum.reset(); 30 | return length; 31 | } 32 | }; 33 | 34 | function lengthPointFirst(x, y) { 35 | lengthStream.point = lengthPoint; 36 | x00 = x0 = x, y00 = y0 = y; 37 | } 38 | 39 | function lengthPoint(x, y) { 40 | x0 -= x, y0 -= y; 41 | lengthSum.add(sqrt(x0 * x0 + y0 * y0)); 42 | x0 = x, y0 = y; 43 | } 44 | 45 | export default lengthStream; 46 | -------------------------------------------------------------------------------- /src/interpolate.js: -------------------------------------------------------------------------------- 1 | import {asin, atan2, cos, degrees, haversin, radians, sin, sqrt} from "./math.js"; 2 | 3 | export default function(a, b) { 4 | var x0 = a[0] * radians, 5 | y0 = a[1] * radians, 6 | x1 = b[0] * radians, 7 | y1 = b[1] * radians, 8 | cy0 = cos(y0), 9 | sy0 = sin(y0), 10 | cy1 = cos(y1), 11 | sy1 = sin(y1), 12 | kx0 = cy0 * cos(x0), 13 | ky0 = cy0 * sin(x0), 14 | kx1 = cy1 * cos(x1), 15 | ky1 = cy1 * sin(x1), 16 | d = 2 * asin(sqrt(haversin(y1 - y0) + cy0 * cy1 * haversin(x1 - x0))), 17 | k = sin(d); 18 | 19 | var interpolate = d ? function(t) { 20 | var B = sin(t *= d) / k, 21 | A = sin(d - t) / k, 22 | x = A * kx0 + B * kx1, 23 | y = A * ky0 + B * ky1, 24 | z = A * sy0 + B * sy1; 25 | return [ 26 | atan2(y, x) * degrees, 27 | atan2(z, sqrt(x * x + y * y)) * degrees 28 | ]; 29 | } : function() { 30 | return [x0 * degrees, y0 * degrees]; 31 | }; 32 | 33 | interpolate.distance = d; 34 | 35 | return interpolate; 36 | } 37 | -------------------------------------------------------------------------------- /src/adder.js: -------------------------------------------------------------------------------- 1 | // Adds floating point numbers with twice the normal precision. 2 | // Reference: J. R. Shewchuk, Adaptive Precision Floating-Point Arithmetic and 3 | // Fast Robust Geometric Predicates, Discrete & Computational Geometry 18(3) 4 | // 305–363 (1997). 5 | // Code adapted from GeographicLib by Charles F. F. Karney, 6 | // http://geographiclib.sourceforge.net/ 7 | 8 | export default function() { 9 | return new Adder; 10 | } 11 | 12 | function Adder() { 13 | this.reset(); 14 | } 15 | 16 | Adder.prototype = { 17 | constructor: Adder, 18 | reset: function() { 19 | this.s = // rounded value 20 | this.t = 0; // exact error 21 | }, 22 | add: function(y) { 23 | add(temp, y, this.t); 24 | add(this, temp.s, this.s); 25 | if (this.s) this.t += temp.t; 26 | else this.s = temp.t; 27 | }, 28 | valueOf: function() { 29 | return this.s; 30 | } 31 | }; 32 | 33 | var temp = new Adder; 34 | 35 | function add(adder, a, b) { 36 | var x = adder.s = a + b, 37 | bv = x - a, 38 | av = x - bv; 39 | adder.t = (a - av) + (b - bv); 40 | } 41 | -------------------------------------------------------------------------------- /src/cartesian.js: -------------------------------------------------------------------------------- 1 | import {asin, atan2, cos, sin, sqrt} from "./math.js"; 2 | 3 | export function spherical(cartesian) { 4 | return [atan2(cartesian[1], cartesian[0]), asin(cartesian[2])]; 5 | } 6 | 7 | export function cartesian(spherical) { 8 | var lambda = spherical[0], phi = spherical[1], cosPhi = cos(phi); 9 | return [cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)]; 10 | } 11 | 12 | export function cartesianDot(a, b) { 13 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 14 | } 15 | 16 | export function cartesianCross(a, b) { 17 | return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; 18 | } 19 | 20 | // TODO return a 21 | export function cartesianAddInPlace(a, b) { 22 | a[0] += b[0], a[1] += b[1], a[2] += b[2]; 23 | } 24 | 25 | export function cartesianScale(vector, k) { 26 | return [vector[0] * k, vector[1] * k, vector[2] * k]; 27 | } 28 | 29 | // TODO return d 30 | export function cartesianNormalizeInPlace(d) { 31 | var l = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); 32 | d[0] /= l, d[1] /= l, d[2] /= l; 33 | } 34 | -------------------------------------------------------------------------------- /test/path/area-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"); 3 | 4 | var equirectangular = d3_geo.geoEquirectangular() 5 | .scale(900 / Math.PI) 6 | .precision(0); 7 | 8 | function testArea(projection, object) { 9 | return d3_geo.geoPath() 10 | .projection(projection) 11 | .area(object); 12 | } 13 | 14 | tape("geoPath.area(…) of a polygon with no holes", function(test) { 15 | test.equal(testArea(equirectangular, { 16 | type: "Polygon", 17 | coordinates: [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]] 18 | }), 25); 19 | test.end(); 20 | }); 21 | 22 | tape("geoPath.area(…) of a polygon with holes", function(test) { 23 | test.equal(testArea(equirectangular, { 24 | type: "Polygon", 25 | coordinates: [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]], [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] 26 | }), 16); 27 | test.end(); 28 | }); 29 | 30 | tape("geoPath.area(…) of a sphere", function(test) { 31 | test.equal(testArea(equirectangular, { 32 | type: "Sphere", 33 | }), 1620000); 34 | test.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /src/path/context.js: -------------------------------------------------------------------------------- 1 | import {tau} from "../math.js"; 2 | import noop from "../noop.js"; 3 | 4 | export default function PathContext(context) { 5 | this._context = context; 6 | } 7 | 8 | PathContext.prototype = { 9 | _radius: 4.5, 10 | pointRadius: function(_) { 11 | return this._radius = _, this; 12 | }, 13 | polygonStart: function() { 14 | this._line = 0; 15 | }, 16 | polygonEnd: function() { 17 | this._line = NaN; 18 | }, 19 | lineStart: function() { 20 | this._point = 0; 21 | }, 22 | lineEnd: function() { 23 | if (this._line === 0) this._context.closePath(); 24 | this._point = NaN; 25 | }, 26 | point: function(x, y) { 27 | switch (this._point) { 28 | case 0: { 29 | this._context.moveTo(x, y); 30 | this._point = 1; 31 | break; 32 | } 33 | case 1: { 34 | this._context.lineTo(x, y); 35 | break; 36 | } 37 | default: { 38 | this._context.moveTo(x + this._radius, y); 39 | this._context.arc(x, y, this._radius, 0, tau); 40 | break; 41 | } 42 | } 43 | }, 44 | result: noop 45 | }; 46 | -------------------------------------------------------------------------------- /src/projection/naturalEarth1.js: -------------------------------------------------------------------------------- 1 | import projection from "./index.js"; 2 | import {abs, epsilon} from "../math.js"; 3 | 4 | export function naturalEarth1Raw(lambda, phi) { 5 | var phi2 = phi * phi, phi4 = phi2 * phi2; 6 | return [ 7 | lambda * (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))), 8 | phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) 9 | ]; 10 | } 11 | 12 | naturalEarth1Raw.invert = function(x, y) { 13 | var phi = y, i = 25, delta; 14 | do { 15 | var phi2 = phi * phi, phi4 = phi2 * phi2; 16 | phi -= delta = (phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) - y) / 17 | (1.007226 + phi2 * (0.015085 * 3 + phi4 * (-0.044475 * 7 + 0.028874 * 9 * phi2 - 0.005916 * 11 * phi4))); 18 | } while (abs(delta) > epsilon && --i > 0); 19 | return [ 20 | x / (0.8707 + (phi2 = phi * phi) * (-0.131979 + phi2 * (-0.013791 + phi2 * phi2 * phi2 * (0.003971 - 0.001529 * phi2)))), 21 | phi 22 | ]; 23 | }; 24 | 25 | export default function() { 26 | return projection(naturalEarth1Raw) 27 | .scale(175.295); 28 | } 29 | -------------------------------------------------------------------------------- /src/path/area.js: -------------------------------------------------------------------------------- 1 | import adder from "../adder.js"; 2 | import {abs} from "../math.js"; 3 | import noop from "../noop.js"; 4 | 5 | var areaSum = adder(), 6 | areaRingSum = adder(), 7 | x00, 8 | y00, 9 | x0, 10 | y0; 11 | 12 | var areaStream = { 13 | point: noop, 14 | lineStart: noop, 15 | lineEnd: noop, 16 | polygonStart: function() { 17 | areaStream.lineStart = areaRingStart; 18 | areaStream.lineEnd = areaRingEnd; 19 | }, 20 | polygonEnd: function() { 21 | areaStream.lineStart = areaStream.lineEnd = areaStream.point = noop; 22 | areaSum.add(abs(areaRingSum)); 23 | areaRingSum.reset(); 24 | }, 25 | result: function() { 26 | var area = areaSum / 2; 27 | areaSum.reset(); 28 | return area; 29 | } 30 | }; 31 | 32 | function areaRingStart() { 33 | areaStream.point = areaPointFirst; 34 | } 35 | 36 | function areaPointFirst(x, y) { 37 | areaStream.point = areaPoint; 38 | x00 = x0 = x, y00 = y0 = y; 39 | } 40 | 41 | function areaPoint(x, y) { 42 | areaRingSum.add(y0 * x - x0 * y); 43 | x0 = x, y0 = y; 44 | } 45 | 46 | function areaRingEnd() { 47 | areaPoint(x00, y00); 48 | } 49 | 50 | export default areaStream; 51 | -------------------------------------------------------------------------------- /src/projection/equalEarth.js: -------------------------------------------------------------------------------- 1 | import projection from "./index.js"; 2 | import {abs, asin, cos, epsilon2, sin, sqrt} from "../math.js"; 3 | 4 | var A1 = 1.340264, 5 | A2 = -0.081106, 6 | A3 = 0.000893, 7 | A4 = 0.003796, 8 | M = sqrt(3) / 2, 9 | iterations = 12; 10 | 11 | export function equalEarthRaw(lambda, phi) { 12 | var l = asin(M * sin(phi)), l2 = l * l, l6 = l2 * l2 * l2; 13 | return [ 14 | lambda * cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))), 15 | l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) 16 | ]; 17 | } 18 | 19 | equalEarthRaw.invert = function(x, y) { 20 | var l = y, l2 = l * l, l6 = l2 * l2 * l2; 21 | for (var i = 0, delta, fy, fpy; i < iterations; ++i) { 22 | fy = l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) - y; 23 | fpy = A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2); 24 | l -= delta = fy / fpy, l2 = l * l, l6 = l2 * l2 * l2; 25 | if (abs(delta) < epsilon2) break; 26 | } 27 | return [ 28 | M * x * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2)) / cos(l), 29 | asin(sin(l) / M) 30 | ]; 31 | }; 32 | 33 | export default function() { 34 | return projection(equalEarthRaw) 35 | .scale(177.158); 36 | } 37 | -------------------------------------------------------------------------------- /src/projection/conicConformal.js: -------------------------------------------------------------------------------- 1 | import {abs, atan, atan2, cos, epsilon, halfPi, log, pow, sign, sin, sqrt, tan} from "../math.js"; 2 | import {conicProjection} from "./conic.js"; 3 | import {mercatorRaw} from "./mercator.js"; 4 | 5 | function tany(y) { 6 | return tan((halfPi + y) / 2); 7 | } 8 | 9 | export function conicConformalRaw(y0, y1) { 10 | var cy0 = cos(y0), 11 | n = y0 === y1 ? sin(y0) : log(cy0 / cos(y1)) / log(tany(y1) / tany(y0)), 12 | f = cy0 * pow(tany(y0), n) / n; 13 | 14 | if (!n) return mercatorRaw; 15 | 16 | function project(x, y) { 17 | if (f > 0) { if (y < -halfPi + epsilon) y = -halfPi + epsilon; } 18 | else { if (y > halfPi - epsilon) y = halfPi - epsilon; } 19 | var r = f / pow(tany(y), n); 20 | return [r * sin(n * x), f - r * cos(n * x)]; 21 | } 22 | 23 | project.invert = function(x, y) { 24 | var fy = f - y, r = sign(n) * sqrt(x * x + fy * fy); 25 | return [atan2(x, abs(fy)) / n * sign(fy), 2 * atan(pow(f / r, 1 / n)) - halfPi]; 26 | }; 27 | 28 | return project; 29 | } 30 | 31 | export default function() { 32 | return conicProjection(conicConformalRaw) 33 | .scale(109.5) 34 | .parallels([30, 30]); 35 | } 36 | -------------------------------------------------------------------------------- /test/render-reference: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var width = 960, 4 | height = 500; 5 | 6 | var fs = require("fs"), 7 | topojson = require("topojson-client"), 8 | Canvas = require("canvas"), 9 | d3 = require("d3"); 10 | 11 | var canvas = new Canvas(width, height), 12 | context = canvas.getContext("2d"); 13 | 14 | var us = require("./data/us-10m.json"); 15 | 16 | var path = d3.geo.path() 17 | .projection(d3.geo.albers()) 18 | .context(context); 19 | 20 | context.fillStyle = "#fff"; 21 | context.fillRect(0, 0, width, height); 22 | 23 | context.beginPath(); 24 | path(topojson.feature(us, us.objects.land)); 25 | context.fillStyle = "#000"; 26 | context.fill(); 27 | 28 | context.beginPath(); 29 | path(topojson.mesh(us, us.objects.counties, function(a, b) { return a !== b && !(a.id / 1000 ^ b.id / 1000); })); 30 | context.lineWidth = 0.5; 31 | context.strokeStyle = "#fff"; 32 | context.stroke(); 33 | 34 | context.beginPath(); 35 | path(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })); 36 | context.lineWidth = 1; 37 | context.strokeStyle = "#fff"; 38 | context.stroke(); 39 | 40 | console.warn("↳ test/images/albers.png"); 41 | canvas.pngStream().pipe(fs.createWriteStream("test/images/albers.png")); 42 | -------------------------------------------------------------------------------- /test/path/bounds-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"), 3 | testContext = require("./test-context"); 4 | 5 | var equirectangular = d3_geo.geoEquirectangular() 6 | .scale(900 / Math.PI) 7 | .precision(0); 8 | 9 | function testBounds(projection, object) { 10 | return d3_geo.geoPath() 11 | .projection(projection) 12 | .bounds(object); 13 | } 14 | 15 | tape("geoPath.bounds(…) of a polygon with no holes", function(test) { 16 | test.deepEqual(testBounds(equirectangular, { 17 | type: "Polygon", 18 | coordinates: [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]] 19 | }), [[980, 245], [985, 250]]); 20 | test.end(); 21 | }); 22 | 23 | tape("geoPath.bounds(…) of a polygon with holes", function(test) { 24 | test.deepEqual(testBounds(equirectangular, { 25 | type: "Polygon", 26 | coordinates: [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]], [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] 27 | }), [[980, 245], [985, 250]]); 28 | test.end(); 29 | }); 30 | 31 | tape("geoPath.bounds(…) of a sphere", function(test) { 32 | test.deepEqual(testBounds(equirectangular, { 33 | type: "Sphere" 34 | }), [[-420, -200], [1380, 700]]); 35 | test.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/projection/projectionEqual.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | tape.Test.prototype.projectionEqual = function(projection, location, point, delta) { 4 | this._assert(planarEqual(projection(location), point, delta || 1e-6) 5 | && sphericalEqual(projection.invert(point), location, delta || 1e-3), { 6 | message: "should be projected equivalents", 7 | operator: "planarEqual", 8 | actual: [projection.invert(point), projection(location)], 9 | expected: [location, point] 10 | }); 11 | }; 12 | 13 | function planarEqual(actual, expected, delta) { 14 | return Array.isArray(actual) 15 | && actual.length === 2 16 | && inDelta(actual[0], expected[0], delta) 17 | && inDelta(actual[1], expected[1], delta); 18 | } 19 | 20 | function sphericalEqual(actual, expected, delta) { 21 | return Array.isArray(actual) 22 | && actual.length === 2 23 | && longitudeEqual(actual[0], expected[0], delta) 24 | && inDelta(actual[1], expected[1], delta); 25 | } 26 | 27 | function longitudeEqual(actual, expected, delta) { 28 | actual = Math.abs(actual - expected) % 360; 29 | return actual <= delta || actual >= 360 - delta; 30 | } 31 | 32 | function inDelta(actual, expected, delta) { 33 | return Math.abs(actual - expected) <= delta; 34 | } 35 | -------------------------------------------------------------------------------- /src/clip/line.js: -------------------------------------------------------------------------------- 1 | export default function(a, b, x0, y0, x1, y1) { 2 | var ax = a[0], 3 | ay = a[1], 4 | bx = b[0], 5 | by = b[1], 6 | t0 = 0, 7 | t1 = 1, 8 | dx = bx - ax, 9 | dy = by - ay, 10 | r; 11 | 12 | r = x0 - ax; 13 | if (!dx && r > 0) return; 14 | r /= dx; 15 | if (dx < 0) { 16 | if (r < t0) return; 17 | if (r < t1) t1 = r; 18 | } else if (dx > 0) { 19 | if (r > t1) return; 20 | if (r > t0) t0 = r; 21 | } 22 | 23 | r = x1 - ax; 24 | if (!dx && r < 0) return; 25 | r /= dx; 26 | if (dx < 0) { 27 | if (r > t1) return; 28 | if (r > t0) t0 = r; 29 | } else if (dx > 0) { 30 | if (r < t0) return; 31 | if (r < t1) t1 = r; 32 | } 33 | 34 | r = y0 - ay; 35 | if (!dy && r > 0) return; 36 | r /= dy; 37 | if (dy < 0) { 38 | if (r < t0) return; 39 | if (r < t1) t1 = r; 40 | } else if (dy > 0) { 41 | if (r > t1) return; 42 | if (r > t0) t0 = r; 43 | } 44 | 45 | r = y1 - ay; 46 | if (!dy && r < 0) return; 47 | r /= dy; 48 | if (dy < 0) { 49 | if (r > t1) return; 50 | if (r > t0) t0 = r; 51 | } else if (dy > 0) { 52 | if (r < t0) return; 53 | if (r < t1) t1 = r; 54 | } 55 | 56 | if (t0 > 0) a[0] = ax + t0 * dx, a[1] = ay + t0 * dy; 57 | if (t1 < 1) b[0] = ax + t1 * dx, b[1] = ay + t1 * dy; 58 | return true; 59 | } 60 | -------------------------------------------------------------------------------- /test/render-us: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var width = 960, 4 | height = 500, 5 | projectionName = process.argv[2], 6 | projectionSymbol = "geo" + projectionName[0].toUpperCase() + projectionName.slice(1); 7 | 8 | if (!/^[a-z0-9]+$/i.test(projectionName)) throw new Error; 9 | 10 | var fs = require("fs"), 11 | topojson = require("topojson-client"), 12 | Canvas = require("canvas"), 13 | d3_geo = require("../"); 14 | 15 | var canvas = new Canvas(width, height), 16 | context = canvas.getContext("2d"); 17 | 18 | var us = require("./data/us-10m.json"); 19 | 20 | var path = d3_geo.geoPath() 21 | .projection(d3_geo[projectionSymbol]().precision(0.1)) 22 | .context(context); 23 | 24 | context.fillStyle = "#fff"; 25 | context.fillRect(0, 0, width, height); 26 | 27 | context.beginPath(); 28 | path(topojson.feature(us, us.objects.land)); 29 | context.fillStyle = "#000"; 30 | context.fill(); 31 | 32 | context.beginPath(); 33 | path(topojson.mesh(us, us.objects.counties, function(a, b) { return a !== b && !(a.id / 1000 ^ b.id / 1000); })); 34 | context.lineWidth = 0.5; 35 | context.strokeStyle = "#fff"; 36 | context.stroke(); 37 | 38 | context.beginPath(); 39 | path(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })); 40 | context.lineWidth = 1; 41 | context.strokeStyle = "#fff"; 42 | context.stroke(); 43 | 44 | canvas.pngStream().pipe(process.stdout); 45 | -------------------------------------------------------------------------------- /test/compute-scale: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var width = 960 - 1, 4 | height = 500 - 1, 5 | projectionName = process.argv[2], 6 | projectionSymbol = "geo" + projectionName[0].toUpperCase() + projectionName.slice(1); 7 | 8 | if (!/^[a-z0-9]+$/i.test(projectionName)) throw new Error; 9 | 10 | var topojson = require("topojson-client"), 11 | d3_format = require("d3-format"), 12 | d3_geo = require("../"); 13 | 14 | var formatNumber = d3_format.format(".6"); 15 | 16 | var projection = d3_geo[projectionSymbol]() 17 | .precision(0.01) 18 | .scale(1) 19 | .translate([0, 0]) 20 | .center([0, 0]); 21 | 22 | if (projection.rotate) projection.rotate([0, 0]); 23 | 24 | var land = {type: "Sphere"}; 25 | 26 | switch (projectionName) { 27 | case "conicConformal": 28 | case "stereographic": { 29 | projection.clipAngle(90); 30 | break; 31 | } 32 | } 33 | 34 | var path = d3_geo.geoPath() 35 | .projection(projection); 36 | 37 | var bounds = path.bounds(land), 38 | dx = bounds[1][0] - bounds[0][0], 39 | dy = bounds[1][1] - bounds[0][1], 40 | cx = (bounds[1][0] + bounds[0][0]) / 2, 41 | cy = (bounds[1][1] + bounds[0][1]) / 2, 42 | scale = Math.min(width / dx, height / dy); 43 | 44 | console.log(`d3.${projectionSymbol}() 45 | .scale(${formatNumber(scale)}) 46 | .center([${(projection.invert ? projection.angle(0).invert([cx, cy]) : [0, 0]).map(formatNumber).join(", ")}]); 47 | `); 48 | -------------------------------------------------------------------------------- /src/length.js: -------------------------------------------------------------------------------- 1 | import adder from "./adder.js"; 2 | import {abs, atan2, cos, radians, sin, sqrt} from "./math.js"; 3 | import noop from "./noop.js"; 4 | import stream from "./stream.js"; 5 | 6 | var lengthSum = adder(), 7 | lambda0, 8 | sinPhi0, 9 | cosPhi0; 10 | 11 | var lengthStream = { 12 | sphere: noop, 13 | point: noop, 14 | lineStart: lengthLineStart, 15 | lineEnd: noop, 16 | polygonStart: noop, 17 | polygonEnd: noop 18 | }; 19 | 20 | function lengthLineStart() { 21 | lengthStream.point = lengthPointFirst; 22 | lengthStream.lineEnd = lengthLineEnd; 23 | } 24 | 25 | function lengthLineEnd() { 26 | lengthStream.point = lengthStream.lineEnd = noop; 27 | } 28 | 29 | function lengthPointFirst(lambda, phi) { 30 | lambda *= radians, phi *= radians; 31 | lambda0 = lambda, sinPhi0 = sin(phi), cosPhi0 = cos(phi); 32 | lengthStream.point = lengthPoint; 33 | } 34 | 35 | function lengthPoint(lambda, phi) { 36 | lambda *= radians, phi *= radians; 37 | var sinPhi = sin(phi), 38 | cosPhi = cos(phi), 39 | delta = abs(lambda - lambda0), 40 | cosDelta = cos(delta), 41 | sinDelta = sin(delta), 42 | x = cosPhi * sinDelta, 43 | y = cosPhi0 * sinPhi - sinPhi0 * cosPhi * cosDelta, 44 | z = sinPhi0 * sinPhi + cosPhi0 * cosPhi * cosDelta; 45 | lengthSum.add(atan2(sqrt(x * x + y * y), z)); 46 | lambda0 = lambda, sinPhi0 = sinPhi, cosPhi0 = cosPhi; 47 | } 48 | 49 | export default function(object) { 50 | lengthSum.reset(); 51 | stream(object, lengthStream); 52 | return +lengthSum; 53 | } 54 | -------------------------------------------------------------------------------- /test/rotation-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"); 3 | 4 | require("./inDelta"); 5 | 6 | tape("a rotation of [+90°, 0°] only rotates longitude", function(test) { 7 | var rotation = d3.geoRotation([90, 0])([0, 0]); 8 | test.inDelta(rotation[0], 90, 1e-6); 9 | test.inDelta(rotation[1], 0, 1e-6); 10 | test.end(); 11 | }); 12 | 13 | tape("a rotation of [+90°, 0°] wraps around when crossing the antimeridian", function(test) { 14 | var rotation = d3.geoRotation([90, 0])([150, 0]); 15 | test.inDelta(rotation[0], -120, 1e-6); 16 | test.inDelta(rotation[1], 0, 1e-6); 17 | test.end(); 18 | }); 19 | 20 | tape("a rotation of [-45°, -45°] rotates longitude and latitude", function(test) { 21 | var rotation = d3.geoRotation([-45, 45])([0, 0]); 22 | test.inDelta(rotation[0], -54.73561, 1e-6); 23 | test.inDelta(rotation[1], 30, 1e-6); 24 | test.end(); 25 | }); 26 | 27 | tape("a rotation of [-45°, -45°] inverse rotation of longitude and latitude", function(test) { 28 | var rotation = d3.geoRotation([-45, 45]).invert([-54.73561, 30]); 29 | test.inDelta(rotation[0], 0, 1e-6); 30 | test.inDelta(rotation[1], 0, 1e-6); 31 | test.end(); 32 | }); 33 | 34 | tape("the identity rotation constrains longitudes to [-180°, 180°]", function(test) { 35 | var rotate = d3.geoRotation([0, 0]); 36 | test.equal(rotate([180,0])[0], 180); 37 | test.equal(rotate([-180,0])[0], -180); 38 | test.equal(rotate([360,0])[0], 0); 39 | test.inDelta(rotate([2562,0])[0], 42, 1e-10); 40 | test.inDelta(rotate([-2562,0])[0], -42, 1e-10); 41 | test.end(); 42 | }); 43 | -------------------------------------------------------------------------------- /src/path/string.js: -------------------------------------------------------------------------------- 1 | export default function PathString() { 2 | this._string = []; 3 | } 4 | 5 | PathString.prototype = { 6 | _radius: 4.5, 7 | _circle: circle(4.5), 8 | pointRadius: function(_) { 9 | if ((_ = +_) !== this._radius) this._radius = _, this._circle = null; 10 | return this; 11 | }, 12 | polygonStart: function() { 13 | this._line = 0; 14 | }, 15 | polygonEnd: function() { 16 | this._line = NaN; 17 | }, 18 | lineStart: function() { 19 | this._point = 0; 20 | }, 21 | lineEnd: function() { 22 | if (this._line === 0) this._string.push("Z"); 23 | this._point = NaN; 24 | }, 25 | point: function(x, y) { 26 | switch (this._point) { 27 | case 0: { 28 | this._string.push("M", x, ",", y); 29 | this._point = 1; 30 | break; 31 | } 32 | case 1: { 33 | this._string.push("L", x, ",", y); 34 | break; 35 | } 36 | default: { 37 | if (this._circle == null) this._circle = circle(this._radius); 38 | this._string.push("M", x, ",", y, this._circle); 39 | break; 40 | } 41 | } 42 | }, 43 | result: function() { 44 | if (this._string.length) { 45 | var result = this._string.join(""); 46 | this._string = []; 47 | return result; 48 | } else { 49 | return null; 50 | } 51 | } 52 | }; 53 | 54 | function circle(radius) { 55 | return "m0," + radius 56 | + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius 57 | + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius 58 | + "z"; 59 | } 60 | -------------------------------------------------------------------------------- /test/circle-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"), 3 | array = require("d3-array"); 4 | 5 | require("./inDelta"); 6 | 7 | tape("circle generates a Polygon", function(test) { 8 | var o = d3.geoCircle()(); 9 | test.equal(o.type, "Polygon"); 10 | test.inDelta(o.coordinates, [[[-78.69007,-90],[-90,-84],[-90,-78],[-90,-72],[-90,-66],[-90,-60],[-90,-54],[-90,-48],[-90,-42],[-90,-36],[-90,-30],[-90,-24],[-90,-18],[-90,-12],[-90,-6],[-90,0],[-90,6],[-90,12],[-90,18],[-90,24],[-90,30],[-90,36],[-90,42],[-90,48],[-90,54],[-90,60],[-90,66],[-90,72],[-90,78],[-90,84],[-89.59666,90],[90,84],[90,78],[90,72],[90,66],[90,60],[90,54],[90,48],[90,42],[90,36],[90,30],[90,24],[90,18],[90,12],[90,6],[90,0],[90,-6],[90,-12],[90,-18],[90,-24],[90,-30],[90,-36],[90,-42],[90,-48],[90,-54],[90,-60],[90,-66],[90,-72],[90,-78],[90,-84],[89.56977,-90]]], 1e-5); 11 | test.end(); 12 | }); 13 | 14 | tape("circle.center([0, 90])", function(test) { 15 | var o = d3.geoCircle().center([0, 90])(); 16 | test.equal(o.type, "Polygon"); 17 | test.inDelta(o.coordinates, [array.range(360, -1, -6).map(function(x) { return [x >= 180 ? x - 360 : x, 0]; })], 1e-6); 18 | test.end(); 19 | }); 20 | 21 | tape("circle.center([45, 45])", function(test) { 22 | var o = d3.geoCircle().center([45, 45]).radius(0)(); 23 | test.equal(o.type, "Polygon"); 24 | test.inDelta(o.coordinates[0][0], [45, 45], 1e-6); 25 | test.end(); 26 | }); 27 | 28 | tape("circle: first and last points are coincident", function(test) { 29 | var o = d3.geoCircle().center([0, 0]).radius(0.02).precision(45)(); 30 | test.inDelta(o.coordinates[0][0], o.coordinates[0].pop(), 1e-6); 31 | test.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /src/projection/mercator.js: -------------------------------------------------------------------------------- 1 | import {atan, exp, halfPi, log, pi, tan, tau} from "../math.js"; 2 | import rotation from "../rotation.js"; 3 | import projection from "./index.js"; 4 | 5 | export function mercatorRaw(lambda, phi) { 6 | return [lambda, log(tan((halfPi + phi) / 2))]; 7 | } 8 | 9 | mercatorRaw.invert = function(x, y) { 10 | return [x, 2 * atan(exp(y)) - halfPi]; 11 | }; 12 | 13 | export default function() { 14 | return mercatorProjection(mercatorRaw) 15 | .scale(961 / tau); 16 | } 17 | 18 | export function mercatorProjection(project) { 19 | var m = projection(project), 20 | center = m.center, 21 | scale = m.scale, 22 | translate = m.translate, 23 | clipExtent = m.clipExtent, 24 | x0 = null, y0, x1, y1; // clip extent 25 | 26 | m.scale = function(_) { 27 | return arguments.length ? (scale(_), reclip()) : scale(); 28 | }; 29 | 30 | m.translate = function(_) { 31 | return arguments.length ? (translate(_), reclip()) : translate(); 32 | }; 33 | 34 | m.center = function(_) { 35 | return arguments.length ? (center(_), reclip()) : center(); 36 | }; 37 | 38 | m.clipExtent = function(_) { 39 | return arguments.length ? ((_ == null ? x0 = y0 = x1 = y1 = null : (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1])), reclip()) : x0 == null ? null : [[x0, y0], [x1, y1]]; 40 | }; 41 | 42 | function reclip() { 43 | var k = pi * scale(), 44 | t = m(rotation(m.rotate()).invert([0, 0])); 45 | return clipExtent(x0 == null 46 | ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] : project === mercatorRaw 47 | ? [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]] 48 | : [[x0, Math.max(t[1] - k, y0)], [x1, Math.min(t[1] + k, y1)]]); 49 | } 50 | 51 | return reclip(); 52 | } 53 | -------------------------------------------------------------------------------- /src/projection/fit.js: -------------------------------------------------------------------------------- 1 | import {default as geoStream} from "../stream.js"; 2 | import boundsStream from "../path/bounds.js"; 3 | 4 | function fit(projection, fitBounds, object) { 5 | var clip = projection.clipExtent && projection.clipExtent(); 6 | projection.scale(150).translate([0, 0]); 7 | if (clip != null) projection.clipExtent(null); 8 | geoStream(object, projection.stream(boundsStream)); 9 | fitBounds(boundsStream.result()); 10 | if (clip != null) projection.clipExtent(clip); 11 | return projection; 12 | } 13 | 14 | export function fitExtent(projection, extent, object) { 15 | return fit(projection, function(b) { 16 | var w = extent[1][0] - extent[0][0], 17 | h = extent[1][1] - extent[0][1], 18 | k = Math.min(w / (b[1][0] - b[0][0]), h / (b[1][1] - b[0][1])), 19 | x = +extent[0][0] + (w - k * (b[1][0] + b[0][0])) / 2, 20 | y = +extent[0][1] + (h - k * (b[1][1] + b[0][1])) / 2; 21 | projection.scale(150 * k).translate([x, y]); 22 | }, object); 23 | } 24 | 25 | export function fitSize(projection, size, object) { 26 | return fitExtent(projection, [[0, 0], size], object); 27 | } 28 | 29 | export function fitWidth(projection, width, object) { 30 | return fit(projection, function(b) { 31 | var w = +width, 32 | k = w / (b[1][0] - b[0][0]), 33 | x = (w - k * (b[1][0] + b[0][0])) / 2, 34 | y = -k * b[0][1]; 35 | projection.scale(150 * k).translate([x, y]); 36 | }, object); 37 | } 38 | 39 | export function fitHeight(projection, height, object) { 40 | return fit(projection, function(b) { 41 | var h = +height, 42 | k = h / (b[1][1] - b[0][1]), 43 | x = -k * b[0][0], 44 | y = (h - k * (b[1][1] + b[0][1])) / 2; 45 | projection.scale(150 * k).translate([x, y]); 46 | }, object); 47 | } 48 | -------------------------------------------------------------------------------- /test/path/measure-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"); 3 | 4 | tape("geoPath.measure(…) of a Point", function(test) { 5 | test.equal(d3_geo.geoPath().measure({ 6 | type: "Point", 7 | coordinates: [0, 0] 8 | }), 0); 9 | test.end(); 10 | }); 11 | 12 | tape("geoPath.measure(…) of a MultiPoint", function(test) { 13 | test.equal(d3_geo.geoPath().measure({ 14 | type: "Point", 15 | coordinates: [[0, 0], [0, 1], [1, 1], [1, 0]] 16 | }), 0); 17 | test.end(); 18 | }); 19 | 20 | tape("geoPath.measure(…) of a LineString", function(test) { 21 | test.equal(d3_geo.geoPath().measure({ 22 | type: "LineString", 23 | coordinates: [[0, 0], [0, 1], [1, 1], [1, 0]] 24 | }), 3); 25 | test.end(); 26 | }); 27 | 28 | tape("geoPath.measure(…) of a MultiLineString", function(test) { 29 | test.equal(d3_geo.geoPath().measure({ 30 | type: "MultiLineString", 31 | coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0]]] 32 | }), 3); 33 | test.end(); 34 | }); 35 | 36 | tape("geoPath.measure(…) of a Polygon", function(test) { 37 | test.equal(d3_geo.geoPath().measure({ 38 | type: "Polygon", 39 | coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]] 40 | }), 4); 41 | test.end(); 42 | }); 43 | 44 | tape("geoPath.measure(…) of a Polygon with a hole", function(test) { 45 | test.equal(d3_geo.geoPath().measure({ 46 | type: "Polygon", 47 | coordinates: [[[-1, -1], [-1, 2], [2, 2], [2, -1], [-1, -1]], [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] 48 | }), 16); 49 | test.end(); 50 | }); 51 | 52 | tape("geoPath.measure(…) of a MultiPolygon", function(test) { 53 | test.equal(d3_geo.geoPath().measure({ 54 | type: "MultiPolygon", 55 | coordinates: [[[[-1, -1], [-1, 2], [2, 2], [2, -1], [-1, -1]]], [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]] 56 | }), 16); 57 | test.end(); 58 | }); 59 | -------------------------------------------------------------------------------- /test/render-world: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var width = 960, 4 | height = 500, 5 | projectionName = process.argv[2], 6 | projectionSymbol = "geo" + projectionName[0].toUpperCase() + projectionName.slice(1); 7 | 8 | if (!/^[a-z0-9]+$/i.test(projectionName)) throw new Error; 9 | 10 | var fs = require("fs"), 11 | topojson = require("topojson-client"), 12 | Canvas = require("canvas"), 13 | d3_geo = require("../"); 14 | 15 | var canvas = new Canvas(width, height), 16 | context = canvas.getContext("2d"); 17 | 18 | var world = require("world-atlas/world/50m.json"), 19 | graticule = d3_geo.geoGraticule(), 20 | outline = {type: "Sphere"}; 21 | 22 | // switch (projectionName) { 23 | // case "littrow": outline = graticule.extent([[-90, -60], [90, 60]]).outline(); break; 24 | // } 25 | 26 | var projection; 27 | 28 | if (projectionSymbol == 'geoAngleorient30') 29 | projection = d3_geo.geoEquirectangular().clipAngle(90).angle(-30).precision(0.1).fitExtent([[0,0],[width,height]], {type:"Sphere"}); 30 | else 31 | projection = d3_geo[projectionSymbol]().precision(0.1); 32 | 33 | var path = d3_geo.geoPath() 34 | .projection(projection) 35 | .context(context); 36 | 37 | context.fillStyle = "#fff"; 38 | context.fillRect(0, 0, width, height); 39 | context.save(); 40 | 41 | // switch (projectionName) { 42 | // case "armadillo": { 43 | // context.beginPath(); 44 | // path(outline); 45 | // context.clip(); 46 | // break; 47 | // } 48 | // } 49 | 50 | context.beginPath(); 51 | path(topojson.feature(world, world.objects.land)); 52 | context.fillStyle = "#000"; 53 | context.fill(); 54 | 55 | context.beginPath(); 56 | path(graticule()); 57 | context.strokeStyle = "rgba(119,119,119,0.5)"; 58 | context.stroke(); 59 | 60 | context.restore(); 61 | 62 | context.beginPath(); 63 | path(outline); 64 | context.strokeStyle = "#000"; 65 | context.stroke(); 66 | 67 | canvas.pngStream().pipe(process.stdout); 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-geo", 3 | "version": "1.11.9", 4 | "description": "Shapes and calculators for spherical coordinates.", 5 | "keywords": [ 6 | "d3", 7 | "d3-module", 8 | "geo", 9 | "maps", 10 | "cartography" 11 | ], 12 | "homepage": "https://d3js.org/d3-geo/", 13 | "license": "BSD-3-Clause", 14 | "author": { 15 | "name": "Mike Bostock", 16 | "url": "https://bost.ocks.org/mike" 17 | }, 18 | "main": "dist/d3-geo.js", 19 | "unpkg": "dist/d3-geo.min.js", 20 | "jsdelivr": "dist/d3-geo.min.js", 21 | "module": "src/index.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/d3/d3-geo.git" 25 | }, 26 | "files": [ 27 | "dist/**/*.js", 28 | "src/**/*.js" 29 | ], 30 | "scripts": { 31 | "pretest": "rollup -c", 32 | "test": "tape -r esm 'test/**/*-test.js' && eslint src", 33 | "prepublishOnly": "rm -rf dist && yarn test && mkdir -p test/output && test/compare-images", 34 | "postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../${npm_package_name}/dist/${npm_package_name}.js ${npm_package_name}.v${npm_package_version%%.*}.js && cp ../${npm_package_name}/dist/${npm_package_name}.min.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git add ${npm_package_name}.v${npm_package_version%%.*}.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git commit -m \"${npm_package_name} ${npm_package_version}\" && git push && cd - && zip -j dist/${npm_package_name}.zip -- LICENSE README.md dist/${npm_package_name}.js dist/${npm_package_name}.min.js" 35 | }, 36 | "dependencies": { 37 | "d3-array": "1" 38 | }, 39 | "sideEffects": false, 40 | "devDependencies": { 41 | "canvas": "1", 42 | "d3-format": "1", 43 | "eslint": "6", 44 | "eslint-plugin-es5": "1", 45 | "esm": "3", 46 | "rollup": "1", 47 | "rollup-plugin-terser": "5", 48 | "tape": "4", 49 | "topojson-client": "3", 50 | "world-atlas": "1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/compare-images: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in \ 4 | azimuthalEqualArea \ 5 | azimuthalEquidistant \ 6 | conicConformal \ 7 | conicEqualArea \ 8 | conicEquidistant \ 9 | equalEarth \ 10 | equirectangular \ 11 | gnomonic \ 12 | mercator \ 13 | naturalEarth1 \ 14 | angleorient30 \ 15 | orthographic \ 16 | stereographic \ 17 | transverseMercator; do 18 | test/render-world $i > test/output/$i.png \ 19 | && [ "$(gm compare -metric rmse img/$i.png test/output/$i.png 2>&1)" = "Image Difference (RootMeanSquaredError): 20 | Normalized Absolute 21 | ============ ========== 22 | Red: 0.0000000000 0.0 23 | Green: 0.0000000000 0.0 24 | Blue: 0.0000000000 0.0 25 | Total: 0.0000000000 0.0" ] \ 26 | && echo -e "\x1B[1;32m✓ $2\x1B[0mtest/output/$i.png" \ 27 | && rm -f -- test/output/$i-difference.png \ 28 | || (gm compare -type TrueColor -highlight-style assign -highlight-color red -file test/output/$i-difference.png test/output/$i.png img/$i.png; \ 29 | echo -e "\x1B[1;31m✗ $2\x1B[0mtest/output/$i.png\n test/output/$i-difference.png") 30 | done 31 | 32 | for i in \ 33 | albers \ 34 | albersUsa; do 35 | test/render-us $i > test/output/$i.png \ 36 | && [ "$(gm compare -metric rmse img/$i.png test/output/$i.png 2>&1)" = "Image Difference (RootMeanSquaredError): 37 | Normalized Absolute 38 | ============ ========== 39 | Red: 0.0000000000 0.0 40 | Green: 0.0000000000 0.0 41 | Blue: 0.0000000000 0.0 42 | Total: 0.0000000000 0.0" ] \ 43 | && echo -e "\x1B[1;32m✓ $2\x1B[0mtest/output/$i.png" \ 44 | && rm -f -- test/output/$i-difference.png \ 45 | || (gm compare -type TrueColor -highlight-style assign -highlight-color red -file test/output/$i-difference.png test/output/$i.png img/$i.png; \ 46 | echo -e "\x1B[1;31m✗ $2\x1B[0mtest/output/$i.png\n test/output/$i-difference.png") 47 | done 48 | -------------------------------------------------------------------------------- /test/projection/invert-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("./projectionEqual"); 5 | 6 | [ 7 | d3.geoAlbers, 8 | d3.geoAzimuthalEqualArea, 9 | d3.geoAzimuthalEquidistant, 10 | d3.geoConicConformal, 11 | function conicConformal() { return d3.geoConicConformal().parallels([20, 30]); }, 12 | function conicConformal() { return d3.geoConicConformal().parallels([30, 30]); }, 13 | function conicConformal() { return d3.geoConicConformal().parallels([-35, -50]); }, 14 | d3.geoConicEqualArea, 15 | function conicEqualArea() { return d3.geoConicEqualArea().parallels([20, 30]); }, 16 | function conicEqualArea() { return d3.geoConicEqualArea().parallels([-30, 30]); }, 17 | function conicEqualArea() { return d3.geoConicEqualArea().parallels([-35, -50]); }, 18 | d3.geoConicEquidistant, 19 | function conicEquidistant() { return d3.geoConicEquidistant().parallels([20, 30]); }, 20 | function conicEquidistant() { return d3.geoConicEquidistant().parallels([30, 30]); }, 21 | function conicEquidistant() { return d3.geoConicEquidistant().parallels([-35, -50]); }, 22 | d3.geoEquirectangular, 23 | d3.geoEqualEarth, 24 | d3.geoGnomonic, 25 | d3.geoMercator, 26 | d3.geoOrthographic, 27 | d3.geoStereographic, 28 | d3.geoTransverseMercator 29 | ].forEach(function(factory) { 30 | var name = factory.name, projection = factory(); 31 | tape(name + "(point) and " + name + ".invert(point) are symmetric", function(test) { 32 | [[0, 0], [30.3, 24.1], [-10, 42], [-2, -5]].forEach(function(point) { 33 | test.projectionEqual(projection, point, projection(point)); 34 | }); 35 | test.end(); 36 | }); 37 | }); 38 | 39 | tape("albersUsa(point) and albersUsa.invert(point) are symmetric", function(test) { 40 | var projection = d3.geoAlbersUsa(); 41 | [[-122.4194, 37.7749], [-74.0059, 40.7128], [-149.9003, 61.2181], [-157.8583, 21.3069]].forEach(function(point) { 42 | test.projectionEqual(projection, point, projection(point)); 43 | }); 44 | test.end(); 45 | }); 46 | -------------------------------------------------------------------------------- /src/path/index.js: -------------------------------------------------------------------------------- 1 | import identity from "../identity.js"; 2 | import stream from "../stream.js"; 3 | import pathArea from "./area.js"; 4 | import pathBounds from "./bounds.js"; 5 | import pathCentroid from "./centroid.js"; 6 | import PathContext from "./context.js"; 7 | import pathMeasure from "./measure.js"; 8 | import PathString from "./string.js"; 9 | 10 | export default function(projection, context) { 11 | var pointRadius = 4.5, 12 | projectionStream, 13 | contextStream; 14 | 15 | function path(object) { 16 | if (object) { 17 | if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments)); 18 | stream(object, projectionStream(contextStream)); 19 | } 20 | return contextStream.result(); 21 | } 22 | 23 | path.area = function(object) { 24 | stream(object, projectionStream(pathArea)); 25 | return pathArea.result(); 26 | }; 27 | 28 | path.measure = function(object) { 29 | stream(object, projectionStream(pathMeasure)); 30 | return pathMeasure.result(); 31 | }; 32 | 33 | path.bounds = function(object) { 34 | stream(object, projectionStream(pathBounds)); 35 | return pathBounds.result(); 36 | }; 37 | 38 | path.centroid = function(object) { 39 | stream(object, projectionStream(pathCentroid)); 40 | return pathCentroid.result(); 41 | }; 42 | 43 | path.projection = function(_) { 44 | return arguments.length ? (projectionStream = _ == null ? (projection = null, identity) : (projection = _).stream, path) : projection; 45 | }; 46 | 47 | path.context = function(_) { 48 | if (!arguments.length) return context; 49 | contextStream = _ == null ? (context = null, new PathString) : new PathContext(context = _); 50 | if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius); 51 | return path; 52 | }; 53 | 54 | path.pointRadius = function(_) { 55 | if (!arguments.length) return pointRadius; 56 | pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_); 57 | return path; 58 | }; 59 | 60 | return path.projection(projection).context(context); 61 | } 62 | -------------------------------------------------------------------------------- /src/area.js: -------------------------------------------------------------------------------- 1 | import adder from "./adder.js"; 2 | import {atan2, cos, quarterPi, radians, sin, tau} from "./math.js"; 3 | import noop from "./noop.js"; 4 | import stream from "./stream.js"; 5 | 6 | export var areaRingSum = adder(); 7 | 8 | var areaSum = adder(), 9 | lambda00, 10 | phi00, 11 | lambda0, 12 | cosPhi0, 13 | sinPhi0; 14 | 15 | export var areaStream = { 16 | point: noop, 17 | lineStart: noop, 18 | lineEnd: noop, 19 | polygonStart: function() { 20 | areaRingSum.reset(); 21 | areaStream.lineStart = areaRingStart; 22 | areaStream.lineEnd = areaRingEnd; 23 | }, 24 | polygonEnd: function() { 25 | var areaRing = +areaRingSum; 26 | areaSum.add(areaRing < 0 ? tau + areaRing : areaRing); 27 | this.lineStart = this.lineEnd = this.point = noop; 28 | }, 29 | sphere: function() { 30 | areaSum.add(tau); 31 | } 32 | }; 33 | 34 | function areaRingStart() { 35 | areaStream.point = areaPointFirst; 36 | } 37 | 38 | function areaRingEnd() { 39 | areaPoint(lambda00, phi00); 40 | } 41 | 42 | function areaPointFirst(lambda, phi) { 43 | areaStream.point = areaPoint; 44 | lambda00 = lambda, phi00 = phi; 45 | lambda *= radians, phi *= radians; 46 | lambda0 = lambda, cosPhi0 = cos(phi = phi / 2 + quarterPi), sinPhi0 = sin(phi); 47 | } 48 | 49 | function areaPoint(lambda, phi) { 50 | lambda *= radians, phi *= radians; 51 | phi = phi / 2 + quarterPi; // half the angular distance from south pole 52 | 53 | // Spherical excess E for a spherical triangle with vertices: south pole, 54 | // previous point, current point. Uses a formula derived from Cagnoli’s 55 | // theorem. See Todhunter, Spherical Trig. (1871), Sec. 103, Eq. (2). 56 | var dLambda = lambda - lambda0, 57 | sdLambda = dLambda >= 0 ? 1 : -1, 58 | adLambda = sdLambda * dLambda, 59 | cosPhi = cos(phi), 60 | sinPhi = sin(phi), 61 | k = sinPhi0 * sinPhi, 62 | u = cosPhi0 * cosPhi + k * cos(adLambda), 63 | v = k * sdLambda * sin(adLambda); 64 | areaRingSum.add(atan2(v, u)); 65 | 66 | // Advance the previous points. 67 | lambda0 = lambda, cosPhi0 = cosPhi, sinPhi0 = sinPhi; 68 | } 69 | 70 | export default function(object) { 71 | areaSum.reset(); 72 | stream(object, areaStream); 73 | return areaSum * 2; 74 | } 75 | -------------------------------------------------------------------------------- /src/projection/identity.js: -------------------------------------------------------------------------------- 1 | import clipRectangle from "../clip/rectangle.js"; 2 | import identity from "../identity.js"; 3 | import {transformer} from "../transform.js"; 4 | import {fitExtent, fitSize, fitWidth, fitHeight} from "./fit.js"; 5 | 6 | function scaleTranslate(kx, ky, tx, ty) { 7 | return kx === 1 && ky === 1 && tx === 0 && ty === 0 ? identity : transformer({ 8 | point: function(x, y) { 9 | this.stream.point(x * kx + tx, y * ky + ty); 10 | } 11 | }); 12 | } 13 | 14 | export default function() { 15 | var k = 1, tx = 0, ty = 0, sx = 1, sy = 1, transform = identity, // scale, translate and reflect 16 | x0 = null, y0, x1, y1, // clip extent 17 | postclip = identity, 18 | cache, 19 | cacheStream, 20 | projection; 21 | 22 | function reset() { 23 | cache = cacheStream = null; 24 | return projection; 25 | } 26 | 27 | return projection = { 28 | stream: function(stream) { 29 | return cache && cacheStream === stream ? cache : cache = transform(postclip(cacheStream = stream)); 30 | }, 31 | postclip: function(_) { 32 | return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip; 33 | }, 34 | clipExtent: function(_) { 35 | return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]]; 36 | }, 37 | scale: function(_) { 38 | return arguments.length ? (transform = scaleTranslate((k = +_) * sx, k * sy, tx, ty), reset()) : k; 39 | }, 40 | translate: function(_) { 41 | return arguments.length ? (transform = scaleTranslate(k * sx, k * sy, tx = +_[0], ty = +_[1]), reset()) : [tx, ty]; 42 | }, 43 | reflectX: function(_) { 44 | return arguments.length ? (transform = scaleTranslate(k * (sx = _ ? -1 : 1), k * sy, tx, ty), reset()) : sx < 0; 45 | }, 46 | reflectY: function(_) { 47 | return arguments.length ? (transform = scaleTranslate(k * sx, k * (sy = _ ? -1 : 1), tx, ty), reset()) : sy < 0; 48 | }, 49 | fitExtent: function(extent, object) { 50 | return fitExtent(projection, extent, object); 51 | }, 52 | fitSize: function(size, object) { 53 | return fitSize(projection, size, object); 54 | }, 55 | fitWidth: function(width, object) { 56 | return fitWidth(projection, width, object); 57 | }, 58 | fitHeight: function(height, object) { 59 | return fitHeight(projection, height, object); 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/path/centroid.js: -------------------------------------------------------------------------------- 1 | import {sqrt} from "../math.js"; 2 | 3 | // TODO Enforce positive area for exterior, negative area for interior? 4 | 5 | var X0 = 0, 6 | Y0 = 0, 7 | Z0 = 0, 8 | X1 = 0, 9 | Y1 = 0, 10 | Z1 = 0, 11 | X2 = 0, 12 | Y2 = 0, 13 | Z2 = 0, 14 | x00, 15 | y00, 16 | x0, 17 | y0; 18 | 19 | var centroidStream = { 20 | point: centroidPoint, 21 | lineStart: centroidLineStart, 22 | lineEnd: centroidLineEnd, 23 | polygonStart: function() { 24 | centroidStream.lineStart = centroidRingStart; 25 | centroidStream.lineEnd = centroidRingEnd; 26 | }, 27 | polygonEnd: function() { 28 | centroidStream.point = centroidPoint; 29 | centroidStream.lineStart = centroidLineStart; 30 | centroidStream.lineEnd = centroidLineEnd; 31 | }, 32 | result: function() { 33 | var centroid = Z2 ? [X2 / Z2, Y2 / Z2] 34 | : Z1 ? [X1 / Z1, Y1 / Z1] 35 | : Z0 ? [X0 / Z0, Y0 / Z0] 36 | : [NaN, NaN]; 37 | X0 = Y0 = Z0 = 38 | X1 = Y1 = Z1 = 39 | X2 = Y2 = Z2 = 0; 40 | return centroid; 41 | } 42 | }; 43 | 44 | function centroidPoint(x, y) { 45 | X0 += x; 46 | Y0 += y; 47 | ++Z0; 48 | } 49 | 50 | function centroidLineStart() { 51 | centroidStream.point = centroidPointFirstLine; 52 | } 53 | 54 | function centroidPointFirstLine(x, y) { 55 | centroidStream.point = centroidPointLine; 56 | centroidPoint(x0 = x, y0 = y); 57 | } 58 | 59 | function centroidPointLine(x, y) { 60 | var dx = x - x0, dy = y - y0, z = sqrt(dx * dx + dy * dy); 61 | X1 += z * (x0 + x) / 2; 62 | Y1 += z * (y0 + y) / 2; 63 | Z1 += z; 64 | centroidPoint(x0 = x, y0 = y); 65 | } 66 | 67 | function centroidLineEnd() { 68 | centroidStream.point = centroidPoint; 69 | } 70 | 71 | function centroidRingStart() { 72 | centroidStream.point = centroidPointFirstRing; 73 | } 74 | 75 | function centroidRingEnd() { 76 | centroidPointRing(x00, y00); 77 | } 78 | 79 | function centroidPointFirstRing(x, y) { 80 | centroidStream.point = centroidPointRing; 81 | centroidPoint(x00 = x0 = x, y00 = y0 = y); 82 | } 83 | 84 | function centroidPointRing(x, y) { 85 | var dx = x - x0, 86 | dy = y - y0, 87 | z = sqrt(dx * dx + dy * dy); 88 | 89 | X1 += z * (x0 + x) / 2; 90 | Y1 += z * (y0 + y) / 2; 91 | Z1 += z; 92 | 93 | z = y0 * x - x0 * y; 94 | X2 += z * (x0 + x); 95 | Y2 += z * (y0 + y); 96 | Z2 += z * 3; 97 | centroidPoint(x0 = x, y0 = y); 98 | } 99 | 100 | export default centroidStream; 101 | -------------------------------------------------------------------------------- /src/stream.js: -------------------------------------------------------------------------------- 1 | function streamGeometry(geometry, stream) { 2 | if (geometry && streamGeometryType.hasOwnProperty(geometry.type)) { 3 | streamGeometryType[geometry.type](geometry, stream); 4 | } 5 | } 6 | 7 | var streamObjectType = { 8 | Feature: function(object, stream) { 9 | streamGeometry(object.geometry, stream); 10 | }, 11 | FeatureCollection: function(object, stream) { 12 | var features = object.features, i = -1, n = features.length; 13 | while (++i < n) streamGeometry(features[i].geometry, stream); 14 | } 15 | }; 16 | 17 | var streamGeometryType = { 18 | Sphere: function(object, stream) { 19 | stream.sphere(); 20 | }, 21 | Point: function(object, stream) { 22 | object = object.coordinates; 23 | stream.point(object[0], object[1], object[2]); 24 | }, 25 | MultiPoint: function(object, stream) { 26 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 27 | while (++i < n) object = coordinates[i], stream.point(object[0], object[1], object[2]); 28 | }, 29 | LineString: function(object, stream) { 30 | streamLine(object.coordinates, stream, 0); 31 | }, 32 | MultiLineString: function(object, stream) { 33 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 34 | while (++i < n) streamLine(coordinates[i], stream, 0); 35 | }, 36 | Polygon: function(object, stream) { 37 | streamPolygon(object.coordinates, stream); 38 | }, 39 | MultiPolygon: function(object, stream) { 40 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 41 | while (++i < n) streamPolygon(coordinates[i], stream); 42 | }, 43 | GeometryCollection: function(object, stream) { 44 | var geometries = object.geometries, i = -1, n = geometries.length; 45 | while (++i < n) streamGeometry(geometries[i], stream); 46 | } 47 | }; 48 | 49 | function streamLine(coordinates, stream, closed) { 50 | var i = -1, n = coordinates.length - closed, coordinate; 51 | stream.lineStart(); 52 | while (++i < n) coordinate = coordinates[i], stream.point(coordinate[0], coordinate[1], coordinate[2]); 53 | stream.lineEnd(); 54 | } 55 | 56 | function streamPolygon(coordinates, stream) { 57 | var i = -1, n = coordinates.length; 58 | stream.polygonStart(); 59 | while (++i < n) streamLine(coordinates[i], stream, 1); 60 | stream.polygonEnd(); 61 | } 62 | 63 | export default function(object, stream) { 64 | if (object && streamObjectType.hasOwnProperty(object.type)) { 65 | streamObjectType[object.type](object, stream); 66 | } else { 67 | streamGeometry(object, stream); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/circle.js: -------------------------------------------------------------------------------- 1 | import {cartesian, cartesianNormalizeInPlace, spherical} from "./cartesian.js"; 2 | import constant from "./constant.js"; 3 | import {acos, cos, degrees, epsilon, radians, sin, tau} from "./math.js"; 4 | import {rotateRadians} from "./rotation.js"; 5 | 6 | // Generates a circle centered at [0°, 0°], with a given radius and precision. 7 | export function circleStream(stream, radius, delta, direction, t0, t1) { 8 | if (!delta) return; 9 | var cosRadius = cos(radius), 10 | sinRadius = sin(radius), 11 | step = direction * delta; 12 | if (t0 == null) { 13 | t0 = radius + direction * tau; 14 | t1 = radius - step / 2; 15 | } else { 16 | t0 = circleRadius(cosRadius, t0); 17 | t1 = circleRadius(cosRadius, t1); 18 | if (direction > 0 ? t0 < t1 : t0 > t1) t0 += direction * tau; 19 | } 20 | for (var point, t = t0; direction > 0 ? t > t1 : t < t1; t -= step) { 21 | point = spherical([cosRadius, -sinRadius * cos(t), -sinRadius * sin(t)]); 22 | stream.point(point[0], point[1]); 23 | } 24 | } 25 | 26 | // Returns the signed angle of a cartesian point relative to [cosRadius, 0, 0]. 27 | function circleRadius(cosRadius, point) { 28 | point = cartesian(point), point[0] -= cosRadius; 29 | cartesianNormalizeInPlace(point); 30 | var radius = acos(-point[1]); 31 | return ((-point[2] < 0 ? -radius : radius) + tau - epsilon) % tau; 32 | } 33 | 34 | export default function() { 35 | var center = constant([0, 0]), 36 | radius = constant(90), 37 | precision = constant(6), 38 | ring, 39 | rotate, 40 | stream = {point: point}; 41 | 42 | function point(x, y) { 43 | ring.push(x = rotate(x, y)); 44 | x[0] *= degrees, x[1] *= degrees; 45 | } 46 | 47 | function circle() { 48 | var c = center.apply(this, arguments), 49 | r = radius.apply(this, arguments) * radians, 50 | p = precision.apply(this, arguments) * radians; 51 | ring = []; 52 | rotate = rotateRadians(-c[0] * radians, -c[1] * radians, 0).invert; 53 | circleStream(stream, r, p, 1); 54 | c = {type: "Polygon", coordinates: [ring]}; 55 | ring = rotate = null; 56 | return c; 57 | } 58 | 59 | circle.center = function(_) { 60 | return arguments.length ? (center = typeof _ === "function" ? _ : constant([+_[0], +_[1]]), circle) : center; 61 | }; 62 | 63 | circle.radius = function(_) { 64 | return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), circle) : radius; 65 | }; 66 | 67 | circle.precision = function(_) { 68 | return arguments.length ? (precision = typeof _ === "function" ? _ : constant(+_), circle) : precision; 69 | }; 70 | 71 | return circle; 72 | } 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010-2016 Mike Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the author nor the names of contributors may be used to 15 | endorse or promote products derived from this software without specific prior 16 | written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | This license applies to GeographicLib, versions 1.12 and later. 30 | 31 | Copyright (c) 2008-2012, Charles Karney 32 | 33 | Permission is hereby granted, free of charge, to any person obtaining a copy of 34 | this software and associated documentation files (the "Software"), to deal in 35 | the Software without restriction, including without limitation the rights to 36 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 37 | the Software, and to permit persons to whom the Software is furnished to do so, 38 | subject to the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be included in all 41 | copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 44 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 45 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 46 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 47 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 48 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 49 | -------------------------------------------------------------------------------- /src/rotation.js: -------------------------------------------------------------------------------- 1 | import compose from "./compose.js"; 2 | import {abs, asin, atan2, cos, degrees, pi, radians, sin, tau} from "./math.js"; 3 | 4 | function rotationIdentity(lambda, phi) { 5 | return [abs(lambda) > pi ? lambda + Math.round(-lambda / tau) * tau : lambda, phi]; 6 | } 7 | 8 | rotationIdentity.invert = rotationIdentity; 9 | 10 | export function rotateRadians(deltaLambda, deltaPhi, deltaGamma) { 11 | return (deltaLambda %= tau) ? (deltaPhi || deltaGamma ? compose(rotationLambda(deltaLambda), rotationPhiGamma(deltaPhi, deltaGamma)) 12 | : rotationLambda(deltaLambda)) 13 | : (deltaPhi || deltaGamma ? rotationPhiGamma(deltaPhi, deltaGamma) 14 | : rotationIdentity); 15 | } 16 | 17 | function forwardRotationLambda(deltaLambda) { 18 | return function(lambda, phi) { 19 | return lambda += deltaLambda, [lambda > pi ? lambda - tau : lambda < -pi ? lambda + tau : lambda, phi]; 20 | }; 21 | } 22 | 23 | function rotationLambda(deltaLambda) { 24 | var rotation = forwardRotationLambda(deltaLambda); 25 | rotation.invert = forwardRotationLambda(-deltaLambda); 26 | return rotation; 27 | } 28 | 29 | function rotationPhiGamma(deltaPhi, deltaGamma) { 30 | var cosDeltaPhi = cos(deltaPhi), 31 | sinDeltaPhi = sin(deltaPhi), 32 | cosDeltaGamma = cos(deltaGamma), 33 | sinDeltaGamma = sin(deltaGamma); 34 | 35 | function rotation(lambda, phi) { 36 | var cosPhi = cos(phi), 37 | x = cos(lambda) * cosPhi, 38 | y = sin(lambda) * cosPhi, 39 | z = sin(phi), 40 | k = z * cosDeltaPhi + x * sinDeltaPhi; 41 | return [ 42 | atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi), 43 | asin(k * cosDeltaGamma + y * sinDeltaGamma) 44 | ]; 45 | } 46 | 47 | rotation.invert = function(lambda, phi) { 48 | var cosPhi = cos(phi), 49 | x = cos(lambda) * cosPhi, 50 | y = sin(lambda) * cosPhi, 51 | z = sin(phi), 52 | k = z * cosDeltaGamma - y * sinDeltaGamma; 53 | return [ 54 | atan2(y * cosDeltaGamma + z * sinDeltaGamma, x * cosDeltaPhi + k * sinDeltaPhi), 55 | asin(k * cosDeltaPhi - x * sinDeltaPhi) 56 | ]; 57 | }; 58 | 59 | return rotation; 60 | } 61 | 62 | export default function(rotate) { 63 | rotate = rotateRadians(rotate[0] * radians, rotate[1] * radians, rotate.length > 2 ? rotate[2] * radians : 0); 64 | 65 | function forward(coordinates) { 66 | coordinates = rotate(coordinates[0] * radians, coordinates[1] * radians); 67 | return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates; 68 | } 69 | 70 | forward.invert = function(coordinates) { 71 | coordinates = rotate.invert(coordinates[0] * radians, coordinates[1] * radians); 72 | return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates; 73 | }; 74 | 75 | return forward; 76 | } 77 | -------------------------------------------------------------------------------- /test/length-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"); 3 | 4 | require("./inDelta"); 5 | 6 | tape("geoLength(Point) returns zero", function(test) { 7 | test.inDelta(d3.geoLength({type: "Point", coordinates: [0, 0]}), 0, 1e-6); 8 | test.end(); 9 | }); 10 | 11 | tape("geoLength(MultiPoint) returns zero", function(test) { 12 | test.inDelta(d3.geoLength({type: "MultiPoint", coordinates: [[0, 1], [2, 3]]}), 0, 1e-6); 13 | test.end(); 14 | }); 15 | 16 | tape("geoLength(LineString) returns the sum of its great-arc segments", function(test) { 17 | test.inDelta(d3.geoLength({type: "LineString", coordinates: [[-45, 0], [45, 0]]}), Math.PI / 2, 1e-6); 18 | test.inDelta(d3.geoLength({type: "LineString", coordinates: [[-45, 0], [-30, 0], [-15, 0], [0, 0]]}), Math.PI / 4, 1e-6); 19 | test.end(); 20 | }); 21 | 22 | tape("geoLength(MultiLineString) returns the sum of its great-arc segments", function(test) { 23 | test.inDelta(d3.geoLength({type: "MultiLineString", coordinates: [[[-45, 0], [-30, 0]], [[-15, 0], [0, 0]]]}), Math.PI / 6, 1e-6); 24 | test.end(); 25 | }); 26 | 27 | tape("geoLength(Polygon) returns the length of its perimeter", function(test) { 28 | test.inDelta(d3.geoLength({type: "Polygon", coordinates: [[[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]]}), 0.157008, 1e-6); 29 | test.end(); 30 | }); 31 | 32 | tape("geoLength(Polygon) returns the length of its perimeter, including holes", function(test) { 33 | test.inDelta(d3.geoLength({type: "Polygon", coordinates: [[[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]], [[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]]}), 0.209354, 1e-6); 34 | test.end(); 35 | }); 36 | 37 | tape("geoLength(MultiPolygon) returns the summed length of the perimeters", function(test) { 38 | test.inDelta(d3.geoLength({type: "MultiPolygon", coordinates: [[[[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]]]}), 0.157008, 1e-6); 39 | test.inDelta(d3.geoLength({type: "MultiPolygon", coordinates: [[[[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]], [[[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]]]}), 0.209354, 1e-6); 40 | test.end(); 41 | }); 42 | 43 | tape("geoLength(FeatureCollection) returns the sum of its features’ lengths", function(test) { 44 | test.inDelta(d3.geoLength({ 45 | type: "FeatureCollection", features: [ 46 | {type: "Feature", geometry: {type: "LineString", coordinates: [[-45, 0], [0, 0]]}}, 47 | {type: "Feature", geometry: {type: "LineString", coordinates: [[0, 0], [45, 0]]}} 48 | ] 49 | }), Math.PI / 2, 1e-6); 50 | test.end(); 51 | }); 52 | 53 | tape("geoLength(GeometryCollection) returns the sum of its geometries’ lengths", function(test) { 54 | test.inDelta(d3.geoLength({ 55 | type: "GeometryCollection", geometries: [ 56 | {type: "GeometryCollection", geometries: [{type: "LineString", coordinates: [[-45, 0], [0, 0]]}]}, 57 | {type: "LineString", coordinates: [[0, 0], [45, 0]]} 58 | ] 59 | }), Math.PI / 2, 1e-6); 60 | test.end(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default as geoArea} from "./area.js"; 2 | export {default as geoBounds} from "./bounds.js"; 3 | export {default as geoCentroid} from "./centroid.js"; 4 | export {default as geoCircle} from "./circle.js"; 5 | export {default as geoClipAntimeridian} from "./clip/antimeridian.js"; 6 | export {default as geoClipCircle} from "./clip/circle.js"; 7 | export {default as geoClipExtent} from "./clip/extent.js"; // DEPRECATED! Use d3.geoIdentity().clipExtent(…). 8 | export {default as geoClipRectangle} from "./clip/rectangle.js"; 9 | export {default as geoContains} from "./contains.js"; 10 | export {default as geoDistance} from "./distance.js"; 11 | export {default as geoGraticule, graticule10 as geoGraticule10} from "./graticule.js"; 12 | export {default as geoInterpolate} from "./interpolate.js"; 13 | export {default as geoLength} from "./length.js"; 14 | export {default as geoPath} from "./path/index.js"; 15 | export {default as geoAlbers} from "./projection/albers.js"; 16 | export {default as geoAlbersUsa} from "./projection/albersUsa.js"; 17 | export {default as geoAzimuthalEqualArea, azimuthalEqualAreaRaw as geoAzimuthalEqualAreaRaw} from "./projection/azimuthalEqualArea.js"; 18 | export {default as geoAzimuthalEquidistant, azimuthalEquidistantRaw as geoAzimuthalEquidistantRaw} from "./projection/azimuthalEquidistant.js"; 19 | export {default as geoConicConformal, conicConformalRaw as geoConicConformalRaw} from "./projection/conicConformal.js"; 20 | export {default as geoConicEqualArea, conicEqualAreaRaw as geoConicEqualAreaRaw} from "./projection/conicEqualArea.js"; 21 | export {default as geoConicEquidistant, conicEquidistantRaw as geoConicEquidistantRaw} from "./projection/conicEquidistant.js"; 22 | export {default as geoEqualEarth, equalEarthRaw as geoEqualEarthRaw} from "./projection/equalEarth.js"; 23 | export {default as geoEquirectangular, equirectangularRaw as geoEquirectangularRaw} from "./projection/equirectangular.js"; 24 | export {default as geoGnomonic, gnomonicRaw as geoGnomonicRaw} from "./projection/gnomonic.js"; 25 | export {default as geoIdentity} from "./projection/identity.js"; 26 | export {default as geoProjection, projectionMutator as geoProjectionMutator} from "./projection/index.js"; 27 | export {default as geoMercator, mercatorRaw as geoMercatorRaw} from "./projection/mercator.js"; 28 | export {default as geoNaturalEarth1, naturalEarth1Raw as geoNaturalEarth1Raw} from "./projection/naturalEarth1.js"; 29 | export {default as geoOrthographic, orthographicRaw as geoOrthographicRaw} from "./projection/orthographic.js"; 30 | export {default as geoStereographic, stereographicRaw as geoStereographicRaw} from "./projection/stereographic.js"; 31 | export {default as geoTransverseMercator, transverseMercatorRaw as geoTransverseMercatorRaw} from "./projection/transverseMercator.js"; 32 | export {default as geoRotation} from "./rotation.js"; 33 | export {default as geoStream} from "./stream.js"; 34 | export {default as geoTransform} from "./transform.js"; 35 | -------------------------------------------------------------------------------- /src/polygonContains.js: -------------------------------------------------------------------------------- 1 | import adder from "./adder.js"; 2 | import {cartesian, cartesianCross, cartesianNormalizeInPlace} from "./cartesian.js"; 3 | import {abs, asin, atan2, cos, epsilon, halfPi, pi, quarterPi, sign, sin, tau} from "./math.js"; 4 | 5 | var sum = adder(); 6 | 7 | function longitude(point) { 8 | if (abs(point[0]) <= pi) 9 | return point[0]; 10 | else 11 | return sign(point[0]) * ((abs(point[0]) + pi) % tau - pi); 12 | } 13 | 14 | export default function(polygon, point) { 15 | var lambda = longitude(point), 16 | phi = point[1], 17 | sinPhi = sin(phi), 18 | normal = [sin(lambda), -cos(lambda), 0], 19 | angle = 0, 20 | winding = 0; 21 | 22 | sum.reset(); 23 | 24 | if (sinPhi === 1) phi = halfPi + epsilon; 25 | else if (sinPhi === -1) phi = -halfPi - epsilon; 26 | 27 | for (var i = 0, n = polygon.length; i < n; ++i) { 28 | if (!(m = (ring = polygon[i]).length)) continue; 29 | var ring, 30 | m, 31 | point0 = ring[m - 1], 32 | lambda0 = longitude(point0), 33 | phi0 = point0[1] / 2 + quarterPi, 34 | sinPhi0 = sin(phi0), 35 | cosPhi0 = cos(phi0); 36 | 37 | for (var j = 0; j < m; ++j, lambda0 = lambda1, sinPhi0 = sinPhi1, cosPhi0 = cosPhi1, point0 = point1) { 38 | var point1 = ring[j], 39 | lambda1 = longitude(point1), 40 | phi1 = point1[1] / 2 + quarterPi, 41 | sinPhi1 = sin(phi1), 42 | cosPhi1 = cos(phi1), 43 | delta = lambda1 - lambda0, 44 | sign = delta >= 0 ? 1 : -1, 45 | absDelta = sign * delta, 46 | antimeridian = absDelta > pi, 47 | k = sinPhi0 * sinPhi1; 48 | 49 | sum.add(atan2(k * sign * sin(absDelta), cosPhi0 * cosPhi1 + k * cos(absDelta))); 50 | angle += antimeridian ? delta + sign * tau : delta; 51 | 52 | // Are the longitudes either side of the point’s meridian (lambda), 53 | // and are the latitudes smaller than the parallel (phi)? 54 | if (antimeridian ^ lambda0 >= lambda ^ lambda1 >= lambda) { 55 | var arc = cartesianCross(cartesian(point0), cartesian(point1)); 56 | cartesianNormalizeInPlace(arc); 57 | var intersection = cartesianCross(normal, arc); 58 | cartesianNormalizeInPlace(intersection); 59 | var phiArc = (antimeridian ^ delta >= 0 ? -1 : 1) * asin(intersection[2]); 60 | if (phi > phiArc || phi === phiArc && (arc[0] || arc[1])) { 61 | winding += antimeridian ^ delta >= 0 ? 1 : -1; 62 | } 63 | } 64 | } 65 | } 66 | 67 | // First, determine whether the South pole is inside or outside: 68 | // 69 | // It is inside if: 70 | // * the polygon winds around it in a clockwise direction. 71 | // * the polygon does not (cumulatively) wind around it, but has a negative 72 | // (counter-clockwise) area. 73 | // 74 | // Second, count the (signed) number of times a segment crosses a lambda 75 | // from the point to the South pole. If it is zero, then the point is the 76 | // same side as the South pole. 77 | 78 | return (angle < -epsilon || angle < epsilon && sum < -epsilon) ^ (winding & 1); 79 | } 80 | -------------------------------------------------------------------------------- /src/clip/antimeridian.js: -------------------------------------------------------------------------------- 1 | import clip from "./index.js"; 2 | import {abs, atan, cos, epsilon, halfPi, pi, sin} from "../math.js"; 3 | 4 | export default clip( 5 | function() { return true; }, 6 | clipAntimeridianLine, 7 | clipAntimeridianInterpolate, 8 | [-pi, -halfPi] 9 | ); 10 | 11 | // Takes a line and cuts into visible segments. Return values: 0 - there were 12 | // intersections or the line was empty; 1 - no intersections; 2 - there were 13 | // intersections, and the first and last segments should be rejoined. 14 | function clipAntimeridianLine(stream) { 15 | var lambda0 = NaN, 16 | phi0 = NaN, 17 | sign0 = NaN, 18 | clean; // no intersections 19 | 20 | return { 21 | lineStart: function() { 22 | stream.lineStart(); 23 | clean = 1; 24 | }, 25 | point: function(lambda1, phi1) { 26 | var sign1 = lambda1 > 0 ? pi : -pi, 27 | delta = abs(lambda1 - lambda0); 28 | if (abs(delta - pi) < epsilon) { // line crosses a pole 29 | stream.point(lambda0, phi0 = (phi0 + phi1) / 2 > 0 ? halfPi : -halfPi); 30 | stream.point(sign0, phi0); 31 | stream.lineEnd(); 32 | stream.lineStart(); 33 | stream.point(sign1, phi0); 34 | stream.point(lambda1, phi0); 35 | clean = 0; 36 | } else if (sign0 !== sign1 && delta >= pi) { // line crosses antimeridian 37 | if (abs(lambda0 - sign0) < epsilon) lambda0 -= sign0 * epsilon; // handle degeneracies 38 | if (abs(lambda1 - sign1) < epsilon) lambda1 -= sign1 * epsilon; 39 | phi0 = clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1); 40 | stream.point(sign0, phi0); 41 | stream.lineEnd(); 42 | stream.lineStart(); 43 | stream.point(sign1, phi0); 44 | clean = 0; 45 | } 46 | stream.point(lambda0 = lambda1, phi0 = phi1); 47 | sign0 = sign1; 48 | }, 49 | lineEnd: function() { 50 | stream.lineEnd(); 51 | lambda0 = phi0 = NaN; 52 | }, 53 | clean: function() { 54 | return 2 - clean; // if intersections, rejoin first and last segments 55 | } 56 | }; 57 | } 58 | 59 | function clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1) { 60 | var cosPhi0, 61 | cosPhi1, 62 | sinLambda0Lambda1 = sin(lambda0 - lambda1); 63 | return abs(sinLambda0Lambda1) > epsilon 64 | ? atan((sin(phi0) * (cosPhi1 = cos(phi1)) * sin(lambda1) 65 | - sin(phi1) * (cosPhi0 = cos(phi0)) * sin(lambda0)) 66 | / (cosPhi0 * cosPhi1 * sinLambda0Lambda1)) 67 | : (phi0 + phi1) / 2; 68 | } 69 | 70 | function clipAntimeridianInterpolate(from, to, direction, stream) { 71 | var phi; 72 | if (from == null) { 73 | phi = direction * halfPi; 74 | stream.point(-pi, phi); 75 | stream.point(0, phi); 76 | stream.point(pi, phi); 77 | stream.point(pi, 0); 78 | stream.point(pi, -phi); 79 | stream.point(0, -phi); 80 | stream.point(-pi, -phi); 81 | stream.point(-pi, 0); 82 | stream.point(-pi, phi); 83 | } else if (abs(from[0] - to[0]) > epsilon) { 84 | var lambda = from[0] < to[0] ? pi : -pi; 85 | phi = direction * lambda / 2; 86 | stream.point(-lambda, phi); 87 | stream.point(0, phi); 88 | stream.point(lambda, phi); 89 | } else { 90 | stream.point(to[0], to[1]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/clip/rejoin.js: -------------------------------------------------------------------------------- 1 | import pointEqual from "../pointEqual.js"; 2 | 3 | function Intersection(point, points, other, entry) { 4 | this.x = point; 5 | this.z = points; 6 | this.o = other; // another intersection 7 | this.e = entry; // is an entry? 8 | this.v = false; // visited 9 | this.n = this.p = null; // next & previous 10 | } 11 | 12 | // A generalized polygon clipping algorithm: given a polygon that has been cut 13 | // into its visible line segments, and rejoins the segments by interpolating 14 | // along the clip edge. 15 | export default function(segments, compareIntersection, startInside, interpolate, stream) { 16 | var subject = [], 17 | clip = [], 18 | i, 19 | n; 20 | 21 | segments.forEach(function(segment) { 22 | if ((n = segment.length - 1) <= 0) return; 23 | var n, p0 = segment[0], p1 = segment[n], x; 24 | 25 | // If the first and last points of a segment are coincident, then treat as a 26 | // closed ring. TODO if all rings are closed, then the winding order of the 27 | // exterior ring should be checked. 28 | if (pointEqual(p0, p1)) { 29 | stream.lineStart(); 30 | for (i = 0; i < n; ++i) stream.point((p0 = segment[i])[0], p0[1]); 31 | stream.lineEnd(); 32 | return; 33 | } 34 | 35 | subject.push(x = new Intersection(p0, segment, null, true)); 36 | clip.push(x.o = new Intersection(p0, null, x, false)); 37 | subject.push(x = new Intersection(p1, segment, null, false)); 38 | clip.push(x.o = new Intersection(p1, null, x, true)); 39 | }); 40 | 41 | if (!subject.length) return; 42 | 43 | clip.sort(compareIntersection); 44 | link(subject); 45 | link(clip); 46 | 47 | for (i = 0, n = clip.length; i < n; ++i) { 48 | clip[i].e = startInside = !startInside; 49 | } 50 | 51 | var start = subject[0], 52 | points, 53 | point; 54 | 55 | while (1) { 56 | // Find first unvisited intersection. 57 | var current = start, 58 | isSubject = true; 59 | while (current.v) if ((current = current.n) === start) return; 60 | points = current.z; 61 | stream.lineStart(); 62 | do { 63 | current.v = current.o.v = true; 64 | if (current.e) { 65 | if (isSubject) { 66 | for (i = 0, n = points.length; i < n; ++i) stream.point((point = points[i])[0], point[1]); 67 | } else { 68 | interpolate(current.x, current.n.x, 1, stream); 69 | } 70 | current = current.n; 71 | } else { 72 | if (isSubject) { 73 | points = current.p.z; 74 | for (i = points.length - 1; i >= 0; --i) stream.point((point = points[i])[0], point[1]); 75 | } else { 76 | interpolate(current.x, current.p.x, -1, stream); 77 | } 78 | current = current.p; 79 | } 80 | current = current.o; 81 | points = current.z; 82 | isSubject = !isSubject; 83 | } while (!current.v); 84 | stream.lineEnd(); 85 | } 86 | } 87 | 88 | function link(array) { 89 | if (!(n = array.length)) return; 90 | var n, 91 | i = 0, 92 | a = array[0], 93 | b; 94 | while (++i < n) { 95 | a.n = b = array[i]; 96 | b.p = a; 97 | a = b; 98 | } 99 | a.n = b = array[0]; 100 | b.p = a; 101 | } 102 | -------------------------------------------------------------------------------- /src/contains.js: -------------------------------------------------------------------------------- 1 | import {default as polygonContains} from "./polygonContains.js"; 2 | import {default as distance} from "./distance.js"; 3 | import {epsilon2, radians} from "./math.js"; 4 | 5 | var containsObjectType = { 6 | Feature: function(object, point) { 7 | return containsGeometry(object.geometry, point); 8 | }, 9 | FeatureCollection: function(object, point) { 10 | var features = object.features, i = -1, n = features.length; 11 | while (++i < n) if (containsGeometry(features[i].geometry, point)) return true; 12 | return false; 13 | } 14 | }; 15 | 16 | var containsGeometryType = { 17 | Sphere: function() { 18 | return true; 19 | }, 20 | Point: function(object, point) { 21 | return containsPoint(object.coordinates, point); 22 | }, 23 | MultiPoint: function(object, point) { 24 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 25 | while (++i < n) if (containsPoint(coordinates[i], point)) return true; 26 | return false; 27 | }, 28 | LineString: function(object, point) { 29 | return containsLine(object.coordinates, point); 30 | }, 31 | MultiLineString: function(object, point) { 32 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 33 | while (++i < n) if (containsLine(coordinates[i], point)) return true; 34 | return false; 35 | }, 36 | Polygon: function(object, point) { 37 | return containsPolygon(object.coordinates, point); 38 | }, 39 | MultiPolygon: function(object, point) { 40 | var coordinates = object.coordinates, i = -1, n = coordinates.length; 41 | while (++i < n) if (containsPolygon(coordinates[i], point)) return true; 42 | return false; 43 | }, 44 | GeometryCollection: function(object, point) { 45 | var geometries = object.geometries, i = -1, n = geometries.length; 46 | while (++i < n) if (containsGeometry(geometries[i], point)) return true; 47 | return false; 48 | } 49 | }; 50 | 51 | function containsGeometry(geometry, point) { 52 | return geometry && containsGeometryType.hasOwnProperty(geometry.type) 53 | ? containsGeometryType[geometry.type](geometry, point) 54 | : false; 55 | } 56 | 57 | function containsPoint(coordinates, point) { 58 | return distance(coordinates, point) === 0; 59 | } 60 | 61 | function containsLine(coordinates, point) { 62 | var ao, bo, ab; 63 | for (var i = 0, n = coordinates.length; i < n; i++) { 64 | bo = distance(coordinates[i], point); 65 | if (bo === 0) return true; 66 | if (i > 0) { 67 | ab = distance(coordinates[i], coordinates[i - 1]); 68 | if ( 69 | ab > 0 && 70 | ao <= ab && 71 | bo <= ab && 72 | (ao + bo - ab) * (1 - Math.pow((ao - bo) / ab, 2)) < epsilon2 * ab 73 | ) 74 | return true; 75 | } 76 | ao = bo; 77 | } 78 | return false; 79 | } 80 | 81 | function containsPolygon(coordinates, point) { 82 | return !!polygonContains(coordinates.map(ringRadians), pointRadians(point)); 83 | } 84 | 85 | function ringRadians(ring) { 86 | return ring = ring.map(pointRadians), ring.pop(), ring; 87 | } 88 | 89 | function pointRadians(point) { 90 | return [point[0] * radians, point[1] * radians]; 91 | } 92 | 93 | export default function(object, point) { 94 | return (object && containsObjectType.hasOwnProperty(object.type) 95 | ? containsObjectType[object.type] 96 | : containsGeometry)(object, point); 97 | } 98 | -------------------------------------------------------------------------------- /test/projection/angle-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("../inDelta"); 5 | require("./projectionEqual"); 6 | 7 | tape("projection.angle(…) defaults to zero", function(test) { 8 | var projection = d3.geoGnomonic().scale(1).translate([0, 0]); 9 | test.equal(projection.angle(), 0); 10 | test.projectionEqual(projection, [0, 0], [0, 0]); 11 | test.projectionEqual(projection, [10, 0], [0.17632698070846498, 0]); 12 | test.projectionEqual(projection, [-10, 0], [-0.17632698070846498, 0]); 13 | test.projectionEqual(projection, [0, 10], [0, -0.17632698070846498]); 14 | test.projectionEqual(projection, [0, -10], [0, 0.17632698070846498]); 15 | test.projectionEqual(projection, [10, 10], [0.17632698070846495, -0.17904710860483972]); 16 | test.projectionEqual(projection, [10, -10], [0.17632698070846495, 0.17904710860483972]); 17 | test.projectionEqual(projection, [-10, 10], [-0.17632698070846495, -0.17904710860483972]); 18 | test.projectionEqual(projection, [-10, -10], [-0.17632698070846495, 0.17904710860483972]); 19 | test.end(); 20 | }); 21 | 22 | tape("projection.angle(…) rotates by the specified degrees after projecting", function(test) { 23 | var projection = d3.geoGnomonic().scale(1).translate([0, 0]).angle(30); 24 | test.inDelta(projection.angle(), 30); 25 | test.projectionEqual(projection, [0, 0], [0, 0]); 26 | test.projectionEqual(projection, [10, 0], [0.1527036446661393, -0.08816349035423247]); 27 | test.projectionEqual(projection, [-10, 0], [-0.1527036446661393, 0.08816349035423247]); 28 | test.projectionEqual(projection, [0, 10], [-0.08816349035423247, -0.1527036446661393]); 29 | test.projectionEqual(projection, [0, -10], [0.08816349035423247, 0.1527036446661393]); 30 | test.projectionEqual(projection, [10, 10], [0.06318009036371944, -0.24322283488017502]); 31 | test.projectionEqual(projection, [10, -10], [0.24222719896855913, 0.0668958541717101]); 32 | test.projectionEqual(projection, [-10, 10], [-0.24222719896855913, -0.0668958541717101]); 33 | test.projectionEqual(projection, [-10, -10], [-0.06318009036371944, 0.24322283488017502]); 34 | test.end(); 35 | }); 36 | 37 | tape("projection.angle(…) rotates by the specified degrees after projecting", function(test) { 38 | var projection = d3.geoGnomonic().scale(1).translate([0, 0]).angle(-30); 39 | test.inDelta(projection.angle(), -30); 40 | test.projectionEqual(projection, [0, 0], [0, 0]); 41 | test.projectionEqual(projection, [10, 0], [0.1527036446661393, 0.08816349035423247]); 42 | test.projectionEqual(projection, [-10, 0], [-0.1527036446661393, -0.08816349035423247]); 43 | test.projectionEqual(projection, [0, 10], [0.08816349035423247, -0.1527036446661393]); 44 | test.projectionEqual(projection, [0, -10], [-0.08816349035423247, 0.1527036446661393]); 45 | test.projectionEqual(projection, [10, 10], [0.24222719896855913, -0.0668958541717101]); 46 | test.projectionEqual(projection, [10, -10], [0.06318009036371944, 0.24322283488017502]); 47 | test.projectionEqual(projection, [-10, 10], [-0.06318009036371944, -0.24322283488017502]); 48 | test.projectionEqual(projection, [-10, -10], [-0.24222719896855913, 0.0668958541717101]); 49 | test.end(); 50 | }); 51 | 52 | tape("projection.angle(…) wraps around 360°", function(test) { 53 | var projection = d3.geoGnomonic().scale(1).translate([0, 0]).angle(360); 54 | test.equal(projection.angle(), 0); 55 | test.end(); 56 | }); 57 | -------------------------------------------------------------------------------- /test/projection/transverseMercator-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("../pathEqual"); 5 | 6 | tape("transverseMercator.clipExtent(null) sets the default automatic clip extent", function(test) { 7 | var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).clipExtent(null).precision(0); 8 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M3.141593,3.141593L0,3.141593L-3.141593,3.141593L-3.141593,-3.141593L-3.141593,-3.141593L0,-3.141593L3.141593,-3.141593L3.141593,3.141593Z"); 9 | test.equal(projection.clipExtent(), null); 10 | test.end(); 11 | }); 12 | 13 | tape("transverseMercator.center(center) sets the correct automatic clip extent", function(test) { 14 | var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).center([10, 10]).precision(0); 15 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M2.966167,3.316126L-0.175426,3.316126L-3.317018,3.316126L-3.317019,-2.967060L-3.317019,-2.967060L-0.175426,-2.967060L2.966167,-2.967060L2.966167,3.316126Z"); 16 | test.equal(projection.clipExtent(), null); 17 | test.end(); 18 | }); 19 | 20 | tape("transverseMercator.clipExtent(extent) intersects the specified clip extent with the automatic clip extent", function(test) { 21 | var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).clipExtent([[-10, -10], [10, 10]]).precision(0); 22 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M10,3.141593L0,3.141593L-10,3.141593L-10,-3.141593L-10,-3.141593L0,-3.141593L10,-3.141593L10,3.141593Z"); 23 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 24 | test.end(); 25 | }); 26 | 27 | tape("transverseMercator.clipExtent(extent).scale(scale) updates the intersected clip extent", function(test) { 28 | var projection = d3.geoTransverseMercator().translate([0, 0]).clipExtent([[-10, -10], [10, 10]]).scale(1).precision(0); 29 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M10,3.141593L0,3.141593L-10,3.141593L-10,-3.141593L-10,-3.141593L0,-3.141593L10,-3.141593L10,3.141593Z"); 30 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 31 | test.end(); 32 | }); 33 | 34 | tape("transverseMercator.clipExtent(extent).translate(translate) updates the intersected clip extent", function(test) { 35 | var projection = d3.geoTransverseMercator().scale(1).clipExtent([[-10, -10], [10, 10]]).translate([0, 0]).precision(0); 36 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M10,3.141593L0,3.141593L-10,3.141593L-10,-3.141593L-10,-3.141593L0,-3.141593L10,-3.141593L10,3.141593Z"); 37 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 38 | test.end(); 39 | }); 40 | 41 | tape("transverseMercator.rotate(…) does not affect the automatic clip extent", function(test) { 42 | var projection = d3.geoTransverseMercator(), object = { 43 | type: "MultiPoint", 44 | coordinates: [ 45 | [-82.35024908550241, 29.649391549778745], 46 | [-82.35014449996858, 29.65075946917633], 47 | [-82.34916073446641, 29.65070265688781], 48 | [-82.3492653331286, 29.64933474064504] 49 | ] 50 | }; 51 | projection.fitExtent([[0, 0], [960, 600]], object); 52 | test.deepEqual(projection.scale(), 15724992.330511674); 53 | test.deepEqual(projection.translate(), [20418843.897824813, 21088401.790971387]); 54 | projection.rotate([0, 95]).fitExtent([[0, 0], [960, 600]], object); 55 | test.deepEqual(projection.scale(), 15724992.330511674); 56 | test.deepEqual(projection.translate(), [20418843.897824813, 47161426.43770847]); 57 | test.end(); 58 | }); 59 | -------------------------------------------------------------------------------- /src/graticule.js: -------------------------------------------------------------------------------- 1 | import {range} from "d3-array"; 2 | import {abs, ceil, epsilon} from "./math.js"; 3 | 4 | function graticuleX(y0, y1, dy) { 5 | var y = range(y0, y1 - epsilon, dy).concat(y1); 6 | return function(x) { return y.map(function(y) { return [x, y]; }); }; 7 | } 8 | 9 | function graticuleY(x0, x1, dx) { 10 | var x = range(x0, x1 - epsilon, dx).concat(x1); 11 | return function(y) { return x.map(function(x) { return [x, y]; }); }; 12 | } 13 | 14 | export default function graticule() { 15 | var x1, x0, X1, X0, 16 | y1, y0, Y1, Y0, 17 | dx = 10, dy = dx, DX = 90, DY = 360, 18 | x, y, X, Y, 19 | precision = 2.5; 20 | 21 | function graticule() { 22 | return {type: "MultiLineString", coordinates: lines()}; 23 | } 24 | 25 | function lines() { 26 | return range(ceil(X0 / DX) * DX, X1, DX).map(X) 27 | .concat(range(ceil(Y0 / DY) * DY, Y1, DY).map(Y)) 28 | .concat(range(ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return abs(x % DX) > epsilon; }).map(x)) 29 | .concat(range(ceil(y0 / dy) * dy, y1, dy).filter(function(y) { return abs(y % DY) > epsilon; }).map(y)); 30 | } 31 | 32 | graticule.lines = function() { 33 | return lines().map(function(coordinates) { return {type: "LineString", coordinates: coordinates}; }); 34 | }; 35 | 36 | graticule.outline = function() { 37 | return { 38 | type: "Polygon", 39 | coordinates: [ 40 | X(X0).concat( 41 | Y(Y1).slice(1), 42 | X(X1).reverse().slice(1), 43 | Y(Y0).reverse().slice(1)) 44 | ] 45 | }; 46 | }; 47 | 48 | graticule.extent = function(_) { 49 | if (!arguments.length) return graticule.extentMinor(); 50 | return graticule.extentMajor(_).extentMinor(_); 51 | }; 52 | 53 | graticule.extentMajor = function(_) { 54 | if (!arguments.length) return [[X0, Y0], [X1, Y1]]; 55 | X0 = +_[0][0], X1 = +_[1][0]; 56 | Y0 = +_[0][1], Y1 = +_[1][1]; 57 | if (X0 > X1) _ = X0, X0 = X1, X1 = _; 58 | if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _; 59 | return graticule.precision(precision); 60 | }; 61 | 62 | graticule.extentMinor = function(_) { 63 | if (!arguments.length) return [[x0, y0], [x1, y1]]; 64 | x0 = +_[0][0], x1 = +_[1][0]; 65 | y0 = +_[0][1], y1 = +_[1][1]; 66 | if (x0 > x1) _ = x0, x0 = x1, x1 = _; 67 | if (y0 > y1) _ = y0, y0 = y1, y1 = _; 68 | return graticule.precision(precision); 69 | }; 70 | 71 | graticule.step = function(_) { 72 | if (!arguments.length) return graticule.stepMinor(); 73 | return graticule.stepMajor(_).stepMinor(_); 74 | }; 75 | 76 | graticule.stepMajor = function(_) { 77 | if (!arguments.length) return [DX, DY]; 78 | DX = +_[0], DY = +_[1]; 79 | return graticule; 80 | }; 81 | 82 | graticule.stepMinor = function(_) { 83 | if (!arguments.length) return [dx, dy]; 84 | dx = +_[0], dy = +_[1]; 85 | return graticule; 86 | }; 87 | 88 | graticule.precision = function(_) { 89 | if (!arguments.length) return precision; 90 | precision = +_; 91 | x = graticuleX(y0, y1, 90); 92 | y = graticuleY(x0, x1, precision); 93 | X = graticuleX(Y0, Y1, 90); 94 | Y = graticuleY(X0, X1, precision); 95 | return graticule; 96 | }; 97 | 98 | return graticule 99 | .extentMajor([[-180, -90 + epsilon], [180, 90 - epsilon]]) 100 | .extentMinor([[-180, -80 - epsilon], [180, 80 + epsilon]]); 101 | } 102 | 103 | export function graticule10() { 104 | return graticule()(); 105 | } 106 | -------------------------------------------------------------------------------- /test/projection/mercator-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("../pathEqual"); 5 | 6 | tape("mercator.clipExtent(null) sets the default automatic clip extent", function(test) { 7 | var projection = d3.geoMercator().translate([0, 0]).scale(1).clipExtent(null).precision(0); 8 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M3.141593,-3.141593L3.141593,0L3.141593,3.141593L3.141593,3.141593L-3.141593,3.141593L-3.141593,3.141593L-3.141593,0L-3.141593,-3.141593L-3.141593,-3.141593L3.141593,-3.141593Z"); 9 | test.equal(projection.clipExtent(), null); 10 | test.end(); 11 | }); 12 | 13 | tape("mercator.center(center) sets the correct automatic clip extent", function(test) { 14 | var projection = d3.geoMercator().translate([0, 0]).scale(1).center([10, 10]).precision(0); 15 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M2.967060,-2.966167L2.967060,0.175426L2.967060,3.317018L2.967060,3.317018L-3.316126,3.317018L-3.316126,3.317019L-3.316126,0.175426L-3.316126,-2.966167L-3.316126,-2.966167L2.967060,-2.966167Z"); 16 | test.equal(projection.clipExtent(), null); 17 | test.end(); 18 | }); 19 | 20 | tape("mercator.clipExtent(extent) intersects the specified clip extent with the automatic clip extent", function(test) { 21 | var projection = d3.geoMercator().translate([0, 0]).scale(1).clipExtent([[-10, -10], [10, 10]]).precision(0); 22 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M3.141593,-10L3.141593,0L3.141593,10L3.141593,10L-3.141593,10L-3.141593,10L-3.141593,0L-3.141593,-10L-3.141593,-10L3.141593,-10Z"); 23 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 24 | test.end(); 25 | }); 26 | 27 | tape("mercator.clipExtent(extent).scale(scale) updates the intersected clip extent", function(test) { 28 | var projection = d3.geoMercator().translate([0, 0]).clipExtent([[-10, -10], [10, 10]]).scale(1).precision(0); 29 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M3.141593,-10L3.141593,0L3.141593,10L3.141593,10L-3.141593,10L-3.141593,10L-3.141593,0L-3.141593,-10L-3.141593,-10L3.141593,-10Z"); 30 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 31 | test.end(); 32 | }); 33 | 34 | tape("mercator.clipExtent(extent).translate(translate) updates the intersected clip extent", function(test) { 35 | var projection = d3.geoMercator().scale(1).clipExtent([[-10, -10], [10, 10]]).translate([0, 0]).precision(0); 36 | test.pathEqual(d3.geoPath(projection)({type: "Sphere"}), "M3.141593,-10L3.141593,0L3.141593,10L3.141593,10L-3.141593,10L-3.141593,10L-3.141593,0L-3.141593,-10L-3.141593,-10L3.141593,-10Z"); 37 | test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); 38 | test.end(); 39 | }); 40 | 41 | tape("mercator.rotate(…) does not affect the automatic clip extent", function(test) { 42 | var projection = d3.geoMercator(), object = { 43 | type: "MultiPoint", 44 | coordinates: [ 45 | [-82.35024908550241, 29.649391549778745], 46 | [-82.35014449996858, 29.65075946917633], 47 | [-82.34916073446641, 29.65070265688781], 48 | [-82.3492653331286, 29.64933474064504] 49 | ] 50 | }; 51 | projection.fitExtent([[0, 0], [960, 600]], object); 52 | test.deepEqual(projection.scale(), 20969742.365692537); 53 | test.deepEqual(projection.translate(), [30139734.76760269, 11371473.949706702]); 54 | projection.rotate([0, 95]).fitExtent([[0, 0], [960, 600]], object); 55 | test.deepEqual(projection.scale(), 35781690.650920525); 56 | test.deepEqual(projection.translate(), [75115911.95344563, 2586046.4116968135]); 57 | test.end(); 58 | }); 59 | -------------------------------------------------------------------------------- /src/projection/resample.js: -------------------------------------------------------------------------------- 1 | import {cartesian} from "../cartesian.js"; 2 | import {abs, asin, atan2, cos, epsilon, radians, sqrt} from "../math.js"; 3 | import {transformer} from "../transform.js"; 4 | 5 | var maxDepth = 16, // maximum depth of subdivision 6 | cosMinDistance = cos(30 * radians); // cos(minimum angular distance) 7 | 8 | export default function(project, delta2) { 9 | return +delta2 ? resample(project, delta2) : resampleNone(project); 10 | } 11 | 12 | function resampleNone(project) { 13 | return transformer({ 14 | point: function(x, y) { 15 | x = project(x, y); 16 | this.stream.point(x[0], x[1]); 17 | } 18 | }); 19 | } 20 | 21 | function resample(project, delta2) { 22 | 23 | function resampleLineTo(x0, y0, lambda0, a0, b0, c0, x1, y1, lambda1, a1, b1, c1, depth, stream) { 24 | var dx = x1 - x0, 25 | dy = y1 - y0, 26 | d2 = dx * dx + dy * dy; 27 | if (d2 > 4 * delta2 && depth--) { 28 | var a = a0 + a1, 29 | b = b0 + b1, 30 | c = c0 + c1, 31 | m = sqrt(a * a + b * b + c * c), 32 | phi2 = asin(c /= m), 33 | lambda2 = abs(abs(c) - 1) < epsilon || abs(lambda0 - lambda1) < epsilon ? (lambda0 + lambda1) / 2 : atan2(b, a), 34 | p = project(lambda2, phi2), 35 | x2 = p[0], 36 | y2 = p[1], 37 | dx2 = x2 - x0, 38 | dy2 = y2 - y0, 39 | dz = dy * dx2 - dx * dy2; 40 | if (dz * dz / d2 > delta2 // perpendicular projected distance 41 | || abs((dx * dx2 + dy * dy2) / d2 - 0.5) > 0.3 // midpoint close to an end 42 | || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { // angular distance 43 | resampleLineTo(x0, y0, lambda0, a0, b0, c0, x2, y2, lambda2, a /= m, b /= m, c, depth, stream); 44 | stream.point(x2, y2); 45 | resampleLineTo(x2, y2, lambda2, a, b, c, x1, y1, lambda1, a1, b1, c1, depth, stream); 46 | } 47 | } 48 | } 49 | return function(stream) { 50 | var lambda00, x00, y00, a00, b00, c00, // first point 51 | lambda0, x0, y0, a0, b0, c0; // previous point 52 | 53 | var resampleStream = { 54 | point: point, 55 | lineStart: lineStart, 56 | lineEnd: lineEnd, 57 | polygonStart: function() { stream.polygonStart(); resampleStream.lineStart = ringStart; }, 58 | polygonEnd: function() { stream.polygonEnd(); resampleStream.lineStart = lineStart; } 59 | }; 60 | 61 | function point(x, y) { 62 | x = project(x, y); 63 | stream.point(x[0], x[1]); 64 | } 65 | 66 | function lineStart() { 67 | x0 = NaN; 68 | resampleStream.point = linePoint; 69 | stream.lineStart(); 70 | } 71 | 72 | function linePoint(lambda, phi) { 73 | var c = cartesian([lambda, phi]), p = project(lambda, phi); 74 | resampleLineTo(x0, y0, lambda0, a0, b0, c0, x0 = p[0], y0 = p[1], lambda0 = lambda, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); 75 | stream.point(x0, y0); 76 | } 77 | 78 | function lineEnd() { 79 | resampleStream.point = point; 80 | stream.lineEnd(); 81 | } 82 | 83 | function ringStart() { 84 | lineStart(); 85 | resampleStream.point = ringPoint; 86 | resampleStream.lineEnd = ringEnd; 87 | } 88 | 89 | function ringPoint(lambda, phi) { 90 | linePoint(lambda00 = lambda, phi), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; 91 | resampleStream.point = linePoint; 92 | } 93 | 94 | function ringEnd() { 95 | resampleLineTo(x0, y0, lambda0, a0, b0, c0, x00, y00, lambda00, a00, b00, c00, maxDepth, stream); 96 | resampleStream.lineEnd = lineEnd; 97 | lineEnd(); 98 | } 99 | 100 | return resampleStream; 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /test/path/string-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"); 3 | 4 | require("../pathEqual"); 5 | 6 | var equirectangular = d3_geo.geoEquirectangular() 7 | .scale(900 / Math.PI) 8 | .precision(0); 9 | 10 | function testPath(projection, object) { 11 | return d3_geo.geoPath() 12 | .projection(projection) 13 | (object); 14 | } 15 | 16 | tape("geoPath(Point) renders a point", function(test) { 17 | test.pathEqual(testPath(equirectangular, { 18 | type: "Point", 19 | coordinates: [-63, 18] 20 | }), "M165,160m0,4.500000a4.500000,4.500000 0 1,1 0,-9a4.500000,4.500000 0 1,1 0,9z"); 21 | test.end(); 22 | }); 23 | 24 | tape("geoPath.pointRadius(radius)(Point) renders a point of the given radius", function(test) { 25 | test.pathEqual(d3_geo.geoPath() 26 | .projection(equirectangular) 27 | .pointRadius(10)({ 28 | type: "Point", 29 | coordinates: [-63, 18] 30 | }), "M165,160m0,10a10,10 0 1,1 0,-20a10,10 0 1,1 0,20z"); 31 | test.end(); 32 | }); 33 | 34 | tape("geoPath(MultiPoint) renders a point", function(test) { 35 | test.pathEqual(testPath(equirectangular, { 36 | type: "MultiPoint", 37 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 38 | }), "M165,160m0,4.500000a4.500000,4.500000 0 1,1 0,-9a4.500000,4.500000 0 1,1 0,9zM170,160m0,4.500000a4.500000,4.500000 0 1,1 0,-9a4.500000,4.500000 0 1,1 0,9zM170,165m0,4.500000a4.500000,4.500000 0 1,1 0,-9a4.500000,4.500000 0 1,1 0,9z"); 39 | test.end(); 40 | }); 41 | 42 | tape("geoPath(LineString) renders a line string", function(test) { 43 | test.pathEqual(testPath(equirectangular, { 44 | type: "LineString", 45 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 46 | }), "M165,160L170,160L170,165"); 47 | test.end(); 48 | }); 49 | 50 | tape("geoPath(Polygon) renders a polygon", function(test) { 51 | test.pathEqual(testPath(equirectangular, { 52 | type: "Polygon", 53 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 54 | }), "M165,160L170,160L170,165Z"); 55 | test.end(); 56 | }); 57 | 58 | tape("geoPath(GeometryCollection) renders a geometry collection", function(test) { 59 | test.pathEqual(testPath(equirectangular, { 60 | type: "GeometryCollection", 61 | geometries: [{ 62 | type: "Polygon", 63 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 64 | }] 65 | }), "M165,160L170,160L170,165Z"); 66 | test.end(); 67 | }); 68 | 69 | tape("geoPath(Feature) renders a feature", function(test) { 70 | test.pathEqual(testPath(equirectangular, { 71 | type: "Feature", 72 | geometry: { 73 | type: "Polygon", 74 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 75 | } 76 | }), "M165,160L170,160L170,165Z"); 77 | test.end(); 78 | }); 79 | 80 | tape("geoPath(FeatureCollection) renders a feature collection", function(test) { 81 | test.pathEqual(testPath(equirectangular, { 82 | type: "FeatureCollection", 83 | features: [{ 84 | type: "Feature", 85 | geometry: { 86 | type: "Polygon", 87 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 88 | } 89 | }] 90 | }), "M165,160L170,160L170,165Z"); 91 | test.end(); 92 | }); 93 | 94 | tape("geoPath(LineString) then geoPath(Point) does not treat the point as part of a line", function(test) { 95 | var path = d3_geo.geoPath().projection(equirectangular); 96 | test.pathEqual(path({ 97 | type: "LineString", 98 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 99 | }), "M165,160L170,160L170,165"); 100 | test.pathEqual(path({ 101 | type: "Point", 102 | coordinates: [-63, 18] 103 | }), "M165,160m0,4.500000a4.500000,4.500000 0 1,1 0,-9a4.500000,4.500000 0 1,1 0,9z"); 104 | test.end(); 105 | }); 106 | -------------------------------------------------------------------------------- /src/clip/index.js: -------------------------------------------------------------------------------- 1 | import clipBuffer from "./buffer.js"; 2 | import clipRejoin from "./rejoin.js"; 3 | import {epsilon, halfPi} from "../math.js"; 4 | import polygonContains from "../polygonContains.js"; 5 | import {merge} from "d3-array"; 6 | 7 | export default function(pointVisible, clipLine, interpolate, start) { 8 | return function(sink) { 9 | var line = clipLine(sink), 10 | ringBuffer = clipBuffer(), 11 | ringSink = clipLine(ringBuffer), 12 | polygonStarted = false, 13 | polygon, 14 | segments, 15 | ring; 16 | 17 | var clip = { 18 | point: point, 19 | lineStart: lineStart, 20 | lineEnd: lineEnd, 21 | polygonStart: function() { 22 | clip.point = pointRing; 23 | clip.lineStart = ringStart; 24 | clip.lineEnd = ringEnd; 25 | segments = []; 26 | polygon = []; 27 | }, 28 | polygonEnd: function() { 29 | clip.point = point; 30 | clip.lineStart = lineStart; 31 | clip.lineEnd = lineEnd; 32 | segments = merge(segments); 33 | var startInside = polygonContains(polygon, start); 34 | if (segments.length) { 35 | if (!polygonStarted) sink.polygonStart(), polygonStarted = true; 36 | clipRejoin(segments, compareIntersection, startInside, interpolate, sink); 37 | } else if (startInside) { 38 | if (!polygonStarted) sink.polygonStart(), polygonStarted = true; 39 | sink.lineStart(); 40 | interpolate(null, null, 1, sink); 41 | sink.lineEnd(); 42 | } 43 | if (polygonStarted) sink.polygonEnd(), polygonStarted = false; 44 | segments = polygon = null; 45 | }, 46 | sphere: function() { 47 | sink.polygonStart(); 48 | sink.lineStart(); 49 | interpolate(null, null, 1, sink); 50 | sink.lineEnd(); 51 | sink.polygonEnd(); 52 | } 53 | }; 54 | 55 | function point(lambda, phi) { 56 | if (pointVisible(lambda, phi)) sink.point(lambda, phi); 57 | } 58 | 59 | function pointLine(lambda, phi) { 60 | line.point(lambda, phi); 61 | } 62 | 63 | function lineStart() { 64 | clip.point = pointLine; 65 | line.lineStart(); 66 | } 67 | 68 | function lineEnd() { 69 | clip.point = point; 70 | line.lineEnd(); 71 | } 72 | 73 | function pointRing(lambda, phi) { 74 | ring.push([lambda, phi]); 75 | ringSink.point(lambda, phi); 76 | } 77 | 78 | function ringStart() { 79 | ringSink.lineStart(); 80 | ring = []; 81 | } 82 | 83 | function ringEnd() { 84 | pointRing(ring[0][0], ring[0][1]); 85 | ringSink.lineEnd(); 86 | 87 | var clean = ringSink.clean(), 88 | ringSegments = ringBuffer.result(), 89 | i, n = ringSegments.length, m, 90 | segment, 91 | point; 92 | 93 | ring.pop(); 94 | polygon.push(ring); 95 | ring = null; 96 | 97 | if (!n) return; 98 | 99 | // No intersections. 100 | if (clean & 1) { 101 | segment = ringSegments[0]; 102 | if ((m = segment.length - 1) > 0) { 103 | if (!polygonStarted) sink.polygonStart(), polygonStarted = true; 104 | sink.lineStart(); 105 | for (i = 0; i < m; ++i) sink.point((point = segment[i])[0], point[1]); 106 | sink.lineEnd(); 107 | } 108 | return; 109 | } 110 | 111 | // Rejoin connected segments. 112 | // TODO reuse ringBuffer.rejoin()? 113 | if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); 114 | 115 | segments.push(ringSegments.filter(validSegment)); 116 | } 117 | 118 | return clip; 119 | }; 120 | } 121 | 122 | function validSegment(segment) { 123 | return segment.length > 1; 124 | } 125 | 126 | // Intersections are sorted along the clip edge. For both antimeridian cutting 127 | // and circle clipping, the same comparison is used. 128 | function compareIntersection(a, b) { 129 | return ((a = a.x)[0] < 0 ? a[1] - halfPi - epsilon : halfPi - a[1]) 130 | - ((b = b.x)[0] < 0 ? b[1] - halfPi - epsilon : halfPi - b[1]); 131 | } 132 | -------------------------------------------------------------------------------- /src/centroid.js: -------------------------------------------------------------------------------- 1 | import {asin, atan2, cos, degrees, epsilon, epsilon2, radians, sin, sqrt} from "./math.js"; 2 | import noop from "./noop.js"; 3 | import stream from "./stream.js"; 4 | 5 | var W0, W1, 6 | X0, Y0, Z0, 7 | X1, Y1, Z1, 8 | X2, Y2, Z2, 9 | lambda00, phi00, // first point 10 | x0, y0, z0; // previous point 11 | 12 | var centroidStream = { 13 | sphere: noop, 14 | point: centroidPoint, 15 | lineStart: centroidLineStart, 16 | lineEnd: centroidLineEnd, 17 | polygonStart: function() { 18 | centroidStream.lineStart = centroidRingStart; 19 | centroidStream.lineEnd = centroidRingEnd; 20 | }, 21 | polygonEnd: function() { 22 | centroidStream.lineStart = centroidLineStart; 23 | centroidStream.lineEnd = centroidLineEnd; 24 | } 25 | }; 26 | 27 | // Arithmetic mean of Cartesian vectors. 28 | function centroidPoint(lambda, phi) { 29 | lambda *= radians, phi *= radians; 30 | var cosPhi = cos(phi); 31 | centroidPointCartesian(cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)); 32 | } 33 | 34 | function centroidPointCartesian(x, y, z) { 35 | ++W0; 36 | X0 += (x - X0) / W0; 37 | Y0 += (y - Y0) / W0; 38 | Z0 += (z - Z0) / W0; 39 | } 40 | 41 | function centroidLineStart() { 42 | centroidStream.point = centroidLinePointFirst; 43 | } 44 | 45 | function centroidLinePointFirst(lambda, phi) { 46 | lambda *= radians, phi *= radians; 47 | var cosPhi = cos(phi); 48 | x0 = cosPhi * cos(lambda); 49 | y0 = cosPhi * sin(lambda); 50 | z0 = sin(phi); 51 | centroidStream.point = centroidLinePoint; 52 | centroidPointCartesian(x0, y0, z0); 53 | } 54 | 55 | function centroidLinePoint(lambda, phi) { 56 | lambda *= radians, phi *= radians; 57 | var cosPhi = cos(phi), 58 | x = cosPhi * cos(lambda), 59 | y = cosPhi * sin(lambda), 60 | z = sin(phi), 61 | w = atan2(sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); 62 | W1 += w; 63 | X1 += w * (x0 + (x0 = x)); 64 | Y1 += w * (y0 + (y0 = y)); 65 | Z1 += w * (z0 + (z0 = z)); 66 | centroidPointCartesian(x0, y0, z0); 67 | } 68 | 69 | function centroidLineEnd() { 70 | centroidStream.point = centroidPoint; 71 | } 72 | 73 | // See J. E. Brock, The Inertia Tensor for a Spherical Triangle, 74 | // J. Applied Mechanics 42, 239 (1975). 75 | function centroidRingStart() { 76 | centroidStream.point = centroidRingPointFirst; 77 | } 78 | 79 | function centroidRingEnd() { 80 | centroidRingPoint(lambda00, phi00); 81 | centroidStream.point = centroidPoint; 82 | } 83 | 84 | function centroidRingPointFirst(lambda, phi) { 85 | lambda00 = lambda, phi00 = phi; 86 | lambda *= radians, phi *= radians; 87 | centroidStream.point = centroidRingPoint; 88 | var cosPhi = cos(phi); 89 | x0 = cosPhi * cos(lambda); 90 | y0 = cosPhi * sin(lambda); 91 | z0 = sin(phi); 92 | centroidPointCartesian(x0, y0, z0); 93 | } 94 | 95 | function centroidRingPoint(lambda, phi) { 96 | lambda *= radians, phi *= radians; 97 | var cosPhi = cos(phi), 98 | x = cosPhi * cos(lambda), 99 | y = cosPhi * sin(lambda), 100 | z = sin(phi), 101 | cx = y0 * z - z0 * y, 102 | cy = z0 * x - x0 * z, 103 | cz = x0 * y - y0 * x, 104 | m = sqrt(cx * cx + cy * cy + cz * cz), 105 | w = asin(m), // line weight = angle 106 | v = m && -w / m; // area weight multiplier 107 | X2 += v * cx; 108 | Y2 += v * cy; 109 | Z2 += v * cz; 110 | W1 += w; 111 | X1 += w * (x0 + (x0 = x)); 112 | Y1 += w * (y0 + (y0 = y)); 113 | Z1 += w * (z0 + (z0 = z)); 114 | centroidPointCartesian(x0, y0, z0); 115 | } 116 | 117 | export default function(object) { 118 | W0 = W1 = 119 | X0 = Y0 = Z0 = 120 | X1 = Y1 = Z1 = 121 | X2 = Y2 = Z2 = 0; 122 | stream(object, centroidStream); 123 | 124 | var x = X2, 125 | y = Y2, 126 | z = Z2, 127 | m = x * x + y * y + z * z; 128 | 129 | // If the area-weighted ccentroid is undefined, fall back to length-weighted ccentroid. 130 | if (m < epsilon2) { 131 | x = X1, y = Y1, z = Z1; 132 | // If the feature has zero length, fall back to arithmetic mean of point vectors. 133 | if (W1 < epsilon) x = X0, y = Y0, z = Z0; 134 | m = x * x + y * y + z * z; 135 | // If the feature still has an undefined ccentroid, then return. 136 | if (m < epsilon2) return [NaN, NaN]; 137 | } 138 | 139 | return [atan2(y, x) * degrees, asin(z / sqrt(m)) * degrees]; 140 | } 141 | -------------------------------------------------------------------------------- /src/projection/albersUsa.js: -------------------------------------------------------------------------------- 1 | import {epsilon} from "../math.js"; 2 | import albers from "./albers.js"; 3 | import conicEqualArea from "./conicEqualArea.js"; 4 | import {fitExtent, fitSize, fitWidth, fitHeight} from "./fit.js"; 5 | 6 | // The projections must have mutually exclusive clip regions on the sphere, 7 | // as this will avoid emitting interleaving lines and polygons. 8 | function multiplex(streams) { 9 | var n = streams.length; 10 | return { 11 | point: function(x, y) { var i = -1; while (++i < n) streams[i].point(x, y); }, 12 | sphere: function() { var i = -1; while (++i < n) streams[i].sphere(); }, 13 | lineStart: function() { var i = -1; while (++i < n) streams[i].lineStart(); }, 14 | lineEnd: function() { var i = -1; while (++i < n) streams[i].lineEnd(); }, 15 | polygonStart: function() { var i = -1; while (++i < n) streams[i].polygonStart(); }, 16 | polygonEnd: function() { var i = -1; while (++i < n) streams[i].polygonEnd(); } 17 | }; 18 | } 19 | 20 | // A composite projection for the United States, configured by default for 21 | // 960×500. The projection also works quite well at 960×600 if you change the 22 | // scale to 1285 and adjust the translate accordingly. The set of standard 23 | // parallels for each region comes from USGS, which is published here: 24 | // http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers 25 | export default function() { 26 | var cache, 27 | cacheStream, 28 | lower48 = albers(), lower48Point, 29 | alaska = conicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]), alaskaPoint, // EPSG:3338 30 | hawaii = conicEqualArea().rotate([157, 0]).center([-3, 19.9]).parallels([8, 18]), hawaiiPoint, // ESRI:102007 31 | point, pointStream = {point: function(x, y) { point = [x, y]; }}; 32 | 33 | function albersUsa(coordinates) { 34 | var x = coordinates[0], y = coordinates[1]; 35 | return point = null, 36 | (lower48Point.point(x, y), point) 37 | || (alaskaPoint.point(x, y), point) 38 | || (hawaiiPoint.point(x, y), point); 39 | } 40 | 41 | albersUsa.invert = function(coordinates) { 42 | var k = lower48.scale(), 43 | t = lower48.translate(), 44 | x = (coordinates[0] - t[0]) / k, 45 | y = (coordinates[1] - t[1]) / k; 46 | return (y >= 0.120 && y < 0.234 && x >= -0.425 && x < -0.214 ? alaska 47 | : y >= 0.166 && y < 0.234 && x >= -0.214 && x < -0.115 ? hawaii 48 | : lower48).invert(coordinates); 49 | }; 50 | 51 | albersUsa.stream = function(stream) { 52 | return cache && cacheStream === stream ? cache : cache = multiplex([lower48.stream(cacheStream = stream), alaska.stream(stream), hawaii.stream(stream)]); 53 | }; 54 | 55 | albersUsa.precision = function(_) { 56 | if (!arguments.length) return lower48.precision(); 57 | lower48.precision(_), alaska.precision(_), hawaii.precision(_); 58 | return reset(); 59 | }; 60 | 61 | albersUsa.scale = function(_) { 62 | if (!arguments.length) return lower48.scale(); 63 | lower48.scale(_), alaska.scale(_ * 0.35), hawaii.scale(_); 64 | return albersUsa.translate(lower48.translate()); 65 | }; 66 | 67 | albersUsa.translate = function(_) { 68 | if (!arguments.length) return lower48.translate(); 69 | var k = lower48.scale(), x = +_[0], y = +_[1]; 70 | 71 | lower48Point = lower48 72 | .translate(_) 73 | .clipExtent([[x - 0.455 * k, y - 0.238 * k], [x + 0.455 * k, y + 0.238 * k]]) 74 | .stream(pointStream); 75 | 76 | alaskaPoint = alaska 77 | .translate([x - 0.307 * k, y + 0.201 * k]) 78 | .clipExtent([[x - 0.425 * k + epsilon, y + 0.120 * k + epsilon], [x - 0.214 * k - epsilon, y + 0.234 * k - epsilon]]) 79 | .stream(pointStream); 80 | 81 | hawaiiPoint = hawaii 82 | .translate([x - 0.205 * k, y + 0.212 * k]) 83 | .clipExtent([[x - 0.214 * k + epsilon, y + 0.166 * k + epsilon], [x - 0.115 * k - epsilon, y + 0.234 * k - epsilon]]) 84 | .stream(pointStream); 85 | 86 | return reset(); 87 | }; 88 | 89 | albersUsa.fitExtent = function(extent, object) { 90 | return fitExtent(albersUsa, extent, object); 91 | }; 92 | 93 | albersUsa.fitSize = function(size, object) { 94 | return fitSize(albersUsa, size, object); 95 | }; 96 | 97 | albersUsa.fitWidth = function(width, object) { 98 | return fitWidth(albersUsa, width, object); 99 | }; 100 | 101 | albersUsa.fitHeight = function(height, object) { 102 | return fitHeight(albersUsa, height, object); 103 | }; 104 | 105 | function reset() { 106 | cache = cacheStream = null; 107 | return albersUsa; 108 | } 109 | 110 | return albersUsa.scale(1070); 111 | } 112 | -------------------------------------------------------------------------------- /test/projection/equirectangular-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../../"); 3 | 4 | require("./projectionEqual"); 5 | 6 | var pi = Math.PI; 7 | 8 | tape("equirectangular(point) returns the expected result", function(test) { 9 | var equirectangular = d3.geoEquirectangular().translate([0, 0]).scale(1); 10 | test.projectionEqual(equirectangular, [ 0, 0], [ 0, 0]); 11 | test.projectionEqual(equirectangular, [-180, 0], [-pi, 0]); 12 | test.projectionEqual(equirectangular, [ 180, 0], [ pi, 0]); 13 | test.projectionEqual(equirectangular, [ 0, 30], [ 0, -pi / 6]); 14 | test.projectionEqual(equirectangular, [ 0, -30], [ 0, pi / 6]); 15 | test.projectionEqual(equirectangular, [ 30, 30], [ pi / 6, -pi / 6]); 16 | test.projectionEqual(equirectangular, [ 30, -30], [ pi / 6, pi / 6]); 17 | test.projectionEqual(equirectangular, [ -30, 30], [-pi / 6, -pi / 6]); 18 | test.projectionEqual(equirectangular, [ -30, -30], [-pi / 6, pi / 6]); 19 | test.end(); 20 | }); 21 | 22 | tape("equirectangular.rotate([30, 0])(point) returns the expected result", function(test) { 23 | var equirectangular = d3.geoEquirectangular().rotate([30, 0]).translate([0, 0]).scale(1); 24 | test.projectionEqual(equirectangular, [ 0, 0], [ pi / 6, 0]); 25 | test.projectionEqual(equirectangular, [-180, 0], [-5 / 6 * pi, 0]); 26 | test.projectionEqual(equirectangular, [ 180, 0], [-5 / 6 * pi, 0]); 27 | test.projectionEqual(equirectangular, [ 0, 30], [ pi / 6, -pi / 6]); 28 | test.projectionEqual(equirectangular, [ 0, -30], [ pi / 6, pi / 6]); 29 | test.projectionEqual(equirectangular, [ 30, 30], [ pi / 3, -pi / 6]); 30 | test.projectionEqual(equirectangular, [ 30, -30], [ pi / 3, pi / 6]); 31 | test.projectionEqual(equirectangular, [ -30, 30], [ 0 , -pi / 6]); 32 | test.projectionEqual(equirectangular, [ -30, -30], [ 0 , pi / 6]); 33 | test.end(); 34 | }); 35 | 36 | tape("equirectangular.rotate([30, 30])(point) returns the expected result", function(test) { 37 | var equirectangular = d3.geoEquirectangular().rotate([30, 30]).translate([0, 0]).scale(1); 38 | test.projectionEqual(equirectangular, [ 0, 0], [ 0.5880026035475674, -0.44783239692893245]); 39 | test.projectionEqual(equirectangular, [-180, 0], [-2.5535900500422257, 0.44783239692893245]); 40 | test.projectionEqual(equirectangular, [ 180, 0], [-2.5535900500422257, 0.44783239692893245]); 41 | test.projectionEqual(equirectangular, [ 0, 30], [ 0.8256075561643480, -0.94077119517052080]); 42 | test.projectionEqual(equirectangular, [ 0, -30], [ 0.4486429615608479, 0.05804529130778048]); 43 | test.projectionEqual(equirectangular, [ 30, 30], [ 1.4056476493802694, -0.70695172788721770]); 44 | test.projectionEqual(equirectangular, [ 30, -30], [ 0.8760580505981933, 0.21823451436745955]); 45 | test.projectionEqual(equirectangular, [ -30, 30], [ 0.0000000000000000, -1.04719755119659760]); 46 | test.projectionEqual(equirectangular, [ -30, -30], [ 0.0000000000000000, 0.00000000000000000]); 47 | test.end(); 48 | }); 49 | 50 | tape("equirectangular.rotate([0, 0, 30])(point) returns the expected result", function(test) { 51 | var equirectangular = d3.geoEquirectangular().rotate([0, 0, 30]).translate([0, 0]).scale(1); 52 | test.projectionEqual(equirectangular, [ 0, 0], [ 0, 0]); 53 | test.projectionEqual(equirectangular, [-180, 0], [-pi, 0]); 54 | test.projectionEqual(equirectangular, [ 180, 0], [ pi, 0]); 55 | test.projectionEqual(equirectangular, [ 0, 30], [-0.2810349015028135, -0.44783239692893245]); 56 | test.projectionEqual(equirectangular, [ 0, -30], [ 0.2810349015028135, 0.44783239692893245]); 57 | test.projectionEqual(equirectangular, [ 30, 30], [ 0.1651486774146268, -0.70695172788721760]); 58 | test.projectionEqual(equirectangular, [ 30, -30], [ 0.6947382761967031, 0.21823451436745964]); 59 | test.projectionEqual(equirectangular, [ -30, 30], [-0.6947382761967031, -0.21823451436745964]); 60 | test.projectionEqual(equirectangular, [ -30, -30], [-0.1651486774146268, 0.70695172788721760]); 61 | test.end(); 62 | }); 63 | 64 | tape("equirectangular.rotate([30, 30, 30])(point) returns the expected result", function(test) { 65 | var equirectangular = d3.geoEquirectangular().rotate([30, 30, 30]).translate([0, 0]).scale(1); 66 | test.projectionEqual(equirectangular, [ 0, 0], [ 0.2810349015028135, -0.67513153293703170]); 67 | test.projectionEqual(equirectangular, [-180, 0], [-2.8605577520869800, 0.67513153293703170]); 68 | test.projectionEqual(equirectangular, [ 180, 0], [-2.8605577520869800, 0.67513153293703170]); 69 | test.projectionEqual(equirectangular, [ 0, 30], [-0.0724760059270816, -1.15865677086597720]); 70 | test.projectionEqual(equirectangular, [ 0, -30], [ 0.4221351552567053, -0.16704161863132252]); 71 | test.projectionEqual(equirectangular, [ 30, 30], [ 1.2033744221750944, -1.21537512510467320]); 72 | test.projectionEqual(equirectangular, [ 30, -30], [ 0.8811235701944905, -0.18861638617540410]); 73 | test.projectionEqual(equirectangular, [ -30, 30], [-0.7137243789447654, -0.84806207898148100]); 74 | test.projectionEqual(equirectangular, [ -30, -30], [ 0, 0]); 75 | test.end(); 76 | }); 77 | -------------------------------------------------------------------------------- /src/clip/rectangle.js: -------------------------------------------------------------------------------- 1 | import {abs, epsilon} from "../math.js"; 2 | import clipBuffer from "./buffer.js"; 3 | import clipLine from "./line.js"; 4 | import clipRejoin from "./rejoin.js"; 5 | import {merge} from "d3-array"; 6 | 7 | var clipMax = 1e9, clipMin = -clipMax; 8 | 9 | // TODO Use d3-polygon’s polygonContains here for the ring check? 10 | // TODO Eliminate duplicate buffering in clipBuffer and polygon.push? 11 | 12 | export default function clipRectangle(x0, y0, x1, y1) { 13 | 14 | function visible(x, y) { 15 | return x0 <= x && x <= x1 && y0 <= y && y <= y1; 16 | } 17 | 18 | function interpolate(from, to, direction, stream) { 19 | var a = 0, a1 = 0; 20 | if (from == null 21 | || (a = corner(from, direction)) !== (a1 = corner(to, direction)) 22 | || comparePoint(from, to) < 0 ^ direction > 0) { 23 | do stream.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0); 24 | while ((a = (a + direction + 4) % 4) !== a1); 25 | } else { 26 | stream.point(to[0], to[1]); 27 | } 28 | } 29 | 30 | function corner(p, direction) { 31 | return abs(p[0] - x0) < epsilon ? direction > 0 ? 0 : 3 32 | : abs(p[0] - x1) < epsilon ? direction > 0 ? 2 : 1 33 | : abs(p[1] - y0) < epsilon ? direction > 0 ? 1 : 0 34 | : direction > 0 ? 3 : 2; // abs(p[1] - y1) < epsilon 35 | } 36 | 37 | function compareIntersection(a, b) { 38 | return comparePoint(a.x, b.x); 39 | } 40 | 41 | function comparePoint(a, b) { 42 | var ca = corner(a, 1), 43 | cb = corner(b, 1); 44 | return ca !== cb ? ca - cb 45 | : ca === 0 ? b[1] - a[1] 46 | : ca === 1 ? a[0] - b[0] 47 | : ca === 2 ? a[1] - b[1] 48 | : b[0] - a[0]; 49 | } 50 | 51 | return function(stream) { 52 | var activeStream = stream, 53 | bufferStream = clipBuffer(), 54 | segments, 55 | polygon, 56 | ring, 57 | x__, y__, v__, // first point 58 | x_, y_, v_, // previous point 59 | first, 60 | clean; 61 | 62 | var clipStream = { 63 | point: point, 64 | lineStart: lineStart, 65 | lineEnd: lineEnd, 66 | polygonStart: polygonStart, 67 | polygonEnd: polygonEnd 68 | }; 69 | 70 | function point(x, y) { 71 | if (visible(x, y)) activeStream.point(x, y); 72 | } 73 | 74 | function polygonInside() { 75 | var winding = 0; 76 | 77 | for (var i = 0, n = polygon.length; i < n; ++i) { 78 | for (var ring = polygon[i], j = 1, m = ring.length, point = ring[0], a0, a1, b0 = point[0], b1 = point[1]; j < m; ++j) { 79 | a0 = b0, a1 = b1, point = ring[j], b0 = point[0], b1 = point[1]; 80 | if (a1 <= y1) { if (b1 > y1 && (b0 - a0) * (y1 - a1) > (b1 - a1) * (x0 - a0)) ++winding; } 81 | else { if (b1 <= y1 && (b0 - a0) * (y1 - a1) < (b1 - a1) * (x0 - a0)) --winding; } 82 | } 83 | } 84 | 85 | return winding; 86 | } 87 | 88 | // Buffer geometry within a polygon and then clip it en masse. 89 | function polygonStart() { 90 | activeStream = bufferStream, segments = [], polygon = [], clean = true; 91 | } 92 | 93 | function polygonEnd() { 94 | var startInside = polygonInside(), 95 | cleanInside = clean && startInside, 96 | visible = (segments = merge(segments)).length; 97 | if (cleanInside || visible) { 98 | stream.polygonStart(); 99 | if (cleanInside) { 100 | stream.lineStart(); 101 | interpolate(null, null, 1, stream); 102 | stream.lineEnd(); 103 | } 104 | if (visible) { 105 | clipRejoin(segments, compareIntersection, startInside, interpolate, stream); 106 | } 107 | stream.polygonEnd(); 108 | } 109 | activeStream = stream, segments = polygon = ring = null; 110 | } 111 | 112 | function lineStart() { 113 | clipStream.point = linePoint; 114 | if (polygon) polygon.push(ring = []); 115 | first = true; 116 | v_ = false; 117 | x_ = y_ = NaN; 118 | } 119 | 120 | // TODO rather than special-case polygons, simply handle them separately. 121 | // Ideally, coincident intersection points should be jittered to avoid 122 | // clipping issues. 123 | function lineEnd() { 124 | if (segments) { 125 | linePoint(x__, y__); 126 | if (v__ && v_) bufferStream.rejoin(); 127 | segments.push(bufferStream.result()); 128 | } 129 | clipStream.point = point; 130 | if (v_) activeStream.lineEnd(); 131 | } 132 | 133 | function linePoint(x, y) { 134 | var v = visible(x, y); 135 | if (polygon) ring.push([x, y]); 136 | if (first) { 137 | x__ = x, y__ = y, v__ = v; 138 | first = false; 139 | if (v) { 140 | activeStream.lineStart(); 141 | activeStream.point(x, y); 142 | } 143 | } else { 144 | if (v && v_) activeStream.point(x, y); 145 | else { 146 | var a = [x_ = Math.max(clipMin, Math.min(clipMax, x_)), y_ = Math.max(clipMin, Math.min(clipMax, y_))], 147 | b = [x = Math.max(clipMin, Math.min(clipMax, x)), y = Math.max(clipMin, Math.min(clipMax, y))]; 148 | if (clipLine(a, b, x0, y0, x1, y1)) { 149 | if (!v_) { 150 | activeStream.lineStart(); 151 | activeStream.point(a[0], a[1]); 152 | } 153 | activeStream.point(b[0], b[1]); 154 | if (!v) activeStream.lineEnd(); 155 | clean = false; 156 | } else if (v) { 157 | activeStream.lineStart(); 158 | activeStream.point(x, y); 159 | clean = false; 160 | } 161 | } 162 | } 163 | x_ = x, y_ = y, v_ = v; 164 | } 165 | 166 | return clipStream; 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /src/projection/index.js: -------------------------------------------------------------------------------- 1 | import clipAntimeridian from "../clip/antimeridian.js"; 2 | import clipCircle from "../clip/circle.js"; 3 | import clipRectangle from "../clip/rectangle.js"; 4 | import compose from "../compose.js"; 5 | import identity from "../identity.js"; 6 | import {cos, degrees, radians, sin, sqrt} from "../math.js"; 7 | import {rotateRadians} from "../rotation.js"; 8 | import {transformer} from "../transform.js"; 9 | import {fitExtent, fitSize, fitWidth, fitHeight} from "./fit.js"; 10 | import resample from "./resample.js"; 11 | 12 | var transformRadians = transformer({ 13 | point: function(x, y) { 14 | this.stream.point(x * radians, y * radians); 15 | } 16 | }); 17 | 18 | function transformRotate(rotate) { 19 | return transformer({ 20 | point: function(x, y) { 21 | var r = rotate(x, y); 22 | return this.stream.point(r[0], r[1]); 23 | } 24 | }); 25 | } 26 | 27 | function scaleTranslate(k, dx, dy) { 28 | function transform(x, y) { 29 | return [dx + k * x, dy - k * y]; 30 | } 31 | transform.invert = function(x, y) { 32 | return [(x - dx) / k, (dy - y) / k]; 33 | }; 34 | return transform; 35 | } 36 | 37 | function scaleTranslateRotate(k, dx, dy, alpha) { 38 | var cosAlpha = cos(alpha), 39 | sinAlpha = sin(alpha), 40 | a = cosAlpha * k, 41 | b = sinAlpha * k, 42 | ai = cosAlpha / k, 43 | bi = sinAlpha / k, 44 | ci = (sinAlpha * dy - cosAlpha * dx) / k, 45 | fi = (sinAlpha * dx + cosAlpha * dy) / k; 46 | function transform(x, y) { 47 | return [a * x - b * y + dx, dy - b * x - a * y]; 48 | } 49 | transform.invert = function(x, y) { 50 | return [ai * x - bi * y + ci, fi - bi * x - ai * y]; 51 | }; 52 | return transform; 53 | } 54 | 55 | export default function projection(project) { 56 | return projectionMutator(function() { return project; })(); 57 | } 58 | 59 | export function projectionMutator(projectAt) { 60 | var project, 61 | k = 150, // scale 62 | x = 480, y = 250, // translate 63 | lambda = 0, phi = 0, // center 64 | deltaLambda = 0, deltaPhi = 0, deltaGamma = 0, rotate, // pre-rotate 65 | alpha = 0, // post-rotate 66 | theta = null, preclip = clipAntimeridian, // pre-clip angle 67 | x0 = null, y0, x1, y1, postclip = identity, // post-clip extent 68 | delta2 = 0.5, // precision 69 | projectResample, 70 | projectTransform, 71 | projectRotateTransform, 72 | cache, 73 | cacheStream; 74 | 75 | function projection(point) { 76 | return projectRotateTransform(point[0] * radians, point[1] * radians); 77 | } 78 | 79 | function invert(point) { 80 | point = projectRotateTransform.invert(point[0], point[1]); 81 | return point && [point[0] * degrees, point[1] * degrees]; 82 | } 83 | 84 | projection.stream = function(stream) { 85 | return cache && cacheStream === stream ? cache : cache = transformRadians(transformRotate(rotate)(preclip(projectResample(postclip(cacheStream = stream))))); 86 | }; 87 | 88 | projection.preclip = function(_) { 89 | return arguments.length ? (preclip = _, theta = undefined, reset()) : preclip; 90 | }; 91 | 92 | projection.postclip = function(_) { 93 | return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip; 94 | }; 95 | 96 | projection.clipAngle = function(_) { 97 | return arguments.length ? (preclip = +_ ? clipCircle(theta = _ * radians) : (theta = null, clipAntimeridian), reset()) : theta * degrees; 98 | }; 99 | 100 | projection.clipExtent = function(_) { 101 | return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]]; 102 | }; 103 | 104 | projection.scale = function(_) { 105 | return arguments.length ? (k = +_, recenter()) : k; 106 | }; 107 | 108 | projection.translate = function(_) { 109 | return arguments.length ? (x = +_[0], y = +_[1], recenter()) : [x, y]; 110 | }; 111 | 112 | projection.center = function(_) { 113 | return arguments.length ? (lambda = _[0] % 360 * radians, phi = _[1] % 360 * radians, recenter()) : [lambda * degrees, phi * degrees]; 114 | }; 115 | 116 | projection.rotate = function(_) { 117 | return arguments.length ? (deltaLambda = _[0] % 360 * radians, deltaPhi = _[1] % 360 * radians, deltaGamma = _.length > 2 ? _[2] % 360 * radians : 0, recenter()) : [deltaLambda * degrees, deltaPhi * degrees, deltaGamma * degrees]; 118 | }; 119 | 120 | projection.angle = function(_) { 121 | return arguments.length ? (alpha = _ % 360 * radians, recenter()) : alpha * degrees; 122 | }; 123 | 124 | projection.precision = function(_) { 125 | return arguments.length ? (projectResample = resample(projectTransform, delta2 = _ * _), reset()) : sqrt(delta2); 126 | }; 127 | 128 | projection.fitExtent = function(extent, object) { 129 | return fitExtent(projection, extent, object); 130 | }; 131 | 132 | projection.fitSize = function(size, object) { 133 | return fitSize(projection, size, object); 134 | }; 135 | 136 | projection.fitWidth = function(width, object) { 137 | return fitWidth(projection, width, object); 138 | }; 139 | 140 | projection.fitHeight = function(height, object) { 141 | return fitHeight(projection, height, object); 142 | }; 143 | 144 | function recenter() { 145 | var center = scaleTranslateRotate(k, 0, 0, alpha).apply(null, project(lambda, phi)), 146 | transform = (alpha ? scaleTranslateRotate : scaleTranslate)(k, x - center[0], y - center[1], alpha); 147 | rotate = rotateRadians(deltaLambda, deltaPhi, deltaGamma); 148 | projectTransform = compose(project, transform); 149 | projectRotateTransform = compose(rotate, projectTransform); 150 | projectResample = resample(projectTransform, delta2); 151 | return reset(); 152 | } 153 | 154 | function reset() { 155 | cache = cacheStream = null; 156 | return projection; 157 | } 158 | 159 | return function() { 160 | project = projectAt.apply(this, arguments); 161 | projection.invert = project.invert && invert; 162 | return recenter(); 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /test/contains-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | array = require("d3-array"), 3 | d3 = require("../"); 4 | 5 | tape("a sphere contains any point", function(test) { 6 | test.equal(d3.geoContains({type: "Sphere"}, [0, 0]), true); 7 | test.end(); 8 | }); 9 | 10 | tape("a point contains itself (and not some other point)", function(test) { 11 | test.equal(d3.geoContains({type: "Point", coordinates: [0, 0]}, [0, 0]), true); 12 | test.equal(d3.geoContains({type: "Point", coordinates: [1, 2]}, [1, 2]), true); 13 | test.equal(d3.geoContains({type: "Point", coordinates: [0, 0]}, [0, 1]), false); 14 | test.equal(d3.geoContains({type: "Point", coordinates: [1, 1]}, [1, 0]), false); 15 | test.end(); 16 | }); 17 | 18 | tape("a MultiPoint contains any of its points", function(test) { 19 | test.equal(d3.geoContains({type: "MultiPoint", coordinates: [[0, 0], [1,2]]}, [0, 0]), true); 20 | test.equal(d3.geoContains({type: "MultiPoint", coordinates: [[0, 0], [1,2]]}, [1, 2]), true); 21 | test.equal(d3.geoContains({type: "MultiPoint", coordinates: [[0, 0], [1,2]]}, [1, 3]), false); 22 | test.end(); 23 | }); 24 | 25 | tape("a LineString contains any point on the Great Circle path", function(test) { 26 | test.equal(d3.geoContains({type: "LineString", coordinates: [[0, 0], [1,2]]}, [0, 0]), true); 27 | test.equal(d3.geoContains({type: "LineString", coordinates: [[0, 0], [1,2]]}, [1, 2]), true); 28 | test.equal(d3.geoContains({type: "LineString", coordinates: [[0, 0], [1,2]]}, d3.geoInterpolate([0, 0], [1,2])(0.3)), true); 29 | test.equal(d3.geoContains({type: "LineString", coordinates: [[0, 0], [1,2]]}, d3.geoInterpolate([0, 0], [1,2])(1.3)), false); 30 | test.equal(d3.geoContains({type: "LineString", coordinates: [[0, 0], [1,2]]}, d3.geoInterpolate([0, 0], [1,2])(-0.3)), false); 31 | test.end(); 32 | }); 33 | 34 | tape("a LineString with 2+ points contains those points", function(test) { 35 | var points = [[0, 0], [1,2], [3, 4], [5, 6]]; 36 | var feature = {type: "LineString", coordinates: points}; 37 | points.forEach(point => { 38 | test.equal(d3.geoContains(feature, point), true); 39 | }); 40 | test.end(); 41 | }); 42 | 43 | tape("a LineString contains epsilon-distant points", function(test) { 44 | var epsilon = 1e-6; 45 | var line = [[0, 0], [0, 10], [10, 10], [10, 0]]; 46 | var points = [[0, 5], [epsilon * 1, 5], [0, epsilon], [epsilon * 1, epsilon]]; 47 | points.forEach(point => { 48 | test.true(d3.geoContains({type:"LineString", coordinates: line}, point)); 49 | }); 50 | test.end(); 51 | }); 52 | 53 | tape("a LineString does not contain 10*epsilon-distant points", function(test) { 54 | var epsilon = 1e-6; 55 | var line = [[0, 0], [0, 10], [10, 10], [10, 0]]; 56 | var points = [[epsilon * 10, 5], [epsilon * 10, epsilon]]; 57 | points.forEach(point => { 58 | test.false(d3.geoContains({type:"LineString", coordinates: line}, point)); 59 | }); 60 | test.end(); 61 | }); 62 | 63 | tape("a MultiLineString contains any point on one of its components", function(test) { 64 | test.equal(d3.geoContains({type: "MultiLineString", coordinates: [[[0, 0], [1,2]], [[2, 3], [4,5]]]}, [2, 3]), true); 65 | test.equal(d3.geoContains({type: "MultiLineString", coordinates: [[[0, 0], [1,2]], [[2, 3], [4,5]]]}, [5, 6]), false); 66 | test.end(); 67 | }); 68 | 69 | tape("a Polygon contains a point", function(test) { 70 | var polygon = d3.geoCircle().radius(60)(); 71 | test.equal(d3.geoContains(polygon, [1, 1]), true); 72 | test.equal(d3.geoContains(polygon, [-180, 0]), false); 73 | test.end(); 74 | }); 75 | 76 | tape("a Polygon with a hole doesn't contain a point", function(test) { 77 | var outer = d3.geoCircle().radius(60)().coordinates[0], 78 | inner = d3.geoCircle().radius(3)().coordinates[0], 79 | polygon = {type:"Polygon", coordinates: [outer, inner]}; 80 | test.equal(d3.geoContains(polygon, [1, 1]), false); 81 | test.equal(d3.geoContains(polygon, [5, 0]), true); 82 | test.equal(d3.geoContains(polygon, [65, 0]), false); 83 | test.end(); 84 | }); 85 | 86 | tape("a MultiPolygon contains a point", function(test) { 87 | var p1 = d3.geoCircle().radius(6)().coordinates, 88 | p2 = d3.geoCircle().radius(6).center([90,0])().coordinates, 89 | polygon = {type:"MultiPolygon", coordinates: [p1, p2]}; 90 | test.equal(d3.geoContains(polygon, [1, 0]), true); 91 | test.equal(d3.geoContains(polygon, [90, 1]), true); 92 | test.equal(d3.geoContains(polygon, [90, 45]), false); 93 | test.end(); 94 | }); 95 | 96 | tape("a GeometryCollection contains a point", function(test) { 97 | var collection = { 98 | type: "GeometryCollection", geometries: [ 99 | {type: "GeometryCollection", geometries: [{type: "LineString", coordinates: [[-45, 0], [0, 0]]}]}, 100 | {type: "LineString", coordinates: [[0, 0], [45, 0]]} 101 | ] 102 | }; 103 | test.equal(d3.geoContains(collection, [-45, 0]), true); 104 | test.equal(d3.geoContains(collection, [45, 0]), true); 105 | test.equal(d3.geoContains(collection, [12, 25]), false); 106 | test.end(); 107 | }); 108 | 109 | tape("a Feature contains a point", function(test) { 110 | var feature = { 111 | type: "Feature", geometry: { 112 | type: "LineString", coordinates: [[0, 0], [45, 0]] 113 | } 114 | }; 115 | test.equal(d3.geoContains(feature, [45, 0]), true); 116 | test.equal(d3.geoContains(feature, [12, 25]), false); 117 | test.end(); 118 | }); 119 | 120 | tape("a FeatureCollection contains a point", function(test) { 121 | var feature1 = { 122 | type: "Feature", geometry: { 123 | type: "LineString", coordinates: [[0, 0], [45, 0]] 124 | } 125 | }, 126 | feature2 = { 127 | type: "Feature", geometry: { 128 | type: "LineString", coordinates: [[-45, 0], [0, 0]] 129 | } 130 | }, 131 | featureCollection = { 132 | type: "FeatureCollection", 133 | features: [ feature1, feature2 ] 134 | }; 135 | test.equal(d3.geoContains(featureCollection, [45, 0]), true); 136 | test.equal(d3.geoContains(featureCollection, [-45, 0]), true); 137 | test.equal(d3.geoContains(featureCollection, [12, 25]), false); 138 | test.end(); 139 | }); 140 | 141 | tape("null contains nothing", function(test) { 142 | test.equal(d3.geoContains(null, [0, 0]), false); 143 | test.end(); 144 | }); 145 | 146 | -------------------------------------------------------------------------------- /src/bounds.js: -------------------------------------------------------------------------------- 1 | import adder from "./adder.js"; 2 | import {areaStream, areaRingSum} from "./area.js"; 3 | import {cartesian, cartesianCross, cartesianNormalizeInPlace, spherical} from "./cartesian.js"; 4 | import {abs, degrees, epsilon, radians} from "./math.js"; 5 | import stream from "./stream.js"; 6 | 7 | var lambda0, phi0, lambda1, phi1, // bounds 8 | lambda2, // previous lambda-coordinate 9 | lambda00, phi00, // first point 10 | p0, // previous 3D point 11 | deltaSum = adder(), 12 | ranges, 13 | range; 14 | 15 | var boundsStream = { 16 | point: boundsPoint, 17 | lineStart: boundsLineStart, 18 | lineEnd: boundsLineEnd, 19 | polygonStart: function() { 20 | boundsStream.point = boundsRingPoint; 21 | boundsStream.lineStart = boundsRingStart; 22 | boundsStream.lineEnd = boundsRingEnd; 23 | deltaSum.reset(); 24 | areaStream.polygonStart(); 25 | }, 26 | polygonEnd: function() { 27 | areaStream.polygonEnd(); 28 | boundsStream.point = boundsPoint; 29 | boundsStream.lineStart = boundsLineStart; 30 | boundsStream.lineEnd = boundsLineEnd; 31 | if (areaRingSum < 0) lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90); 32 | else if (deltaSum > epsilon) phi1 = 90; 33 | else if (deltaSum < -epsilon) phi0 = -90; 34 | range[0] = lambda0, range[1] = lambda1; 35 | }, 36 | sphere: function() { 37 | lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90); 38 | } 39 | }; 40 | 41 | function boundsPoint(lambda, phi) { 42 | ranges.push(range = [lambda0 = lambda, lambda1 = lambda]); 43 | if (phi < phi0) phi0 = phi; 44 | if (phi > phi1) phi1 = phi; 45 | } 46 | 47 | function linePoint(lambda, phi) { 48 | var p = cartesian([lambda * radians, phi * radians]); 49 | if (p0) { 50 | var normal = cartesianCross(p0, p), 51 | equatorial = [normal[1], -normal[0], 0], 52 | inflection = cartesianCross(equatorial, normal); 53 | cartesianNormalizeInPlace(inflection); 54 | inflection = spherical(inflection); 55 | var delta = lambda - lambda2, 56 | sign = delta > 0 ? 1 : -1, 57 | lambdai = inflection[0] * degrees * sign, 58 | phii, 59 | antimeridian = abs(delta) > 180; 60 | if (antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) { 61 | phii = inflection[1] * degrees; 62 | if (phii > phi1) phi1 = phii; 63 | } else if (lambdai = (lambdai + 360) % 360 - 180, antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) { 64 | phii = -inflection[1] * degrees; 65 | if (phii < phi0) phi0 = phii; 66 | } else { 67 | if (phi < phi0) phi0 = phi; 68 | if (phi > phi1) phi1 = phi; 69 | } 70 | if (antimeridian) { 71 | if (lambda < lambda2) { 72 | if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda; 73 | } else { 74 | if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda; 75 | } 76 | } else { 77 | if (lambda1 >= lambda0) { 78 | if (lambda < lambda0) lambda0 = lambda; 79 | if (lambda > lambda1) lambda1 = lambda; 80 | } else { 81 | if (lambda > lambda2) { 82 | if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda; 83 | } else { 84 | if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda; 85 | } 86 | } 87 | } 88 | } else { 89 | ranges.push(range = [lambda0 = lambda, lambda1 = lambda]); 90 | } 91 | if (phi < phi0) phi0 = phi; 92 | if (phi > phi1) phi1 = phi; 93 | p0 = p, lambda2 = lambda; 94 | } 95 | 96 | function boundsLineStart() { 97 | boundsStream.point = linePoint; 98 | } 99 | 100 | function boundsLineEnd() { 101 | range[0] = lambda0, range[1] = lambda1; 102 | boundsStream.point = boundsPoint; 103 | p0 = null; 104 | } 105 | 106 | function boundsRingPoint(lambda, phi) { 107 | if (p0) { 108 | var delta = lambda - lambda2; 109 | deltaSum.add(abs(delta) > 180 ? delta + (delta > 0 ? 360 : -360) : delta); 110 | } else { 111 | lambda00 = lambda, phi00 = phi; 112 | } 113 | areaStream.point(lambda, phi); 114 | linePoint(lambda, phi); 115 | } 116 | 117 | function boundsRingStart() { 118 | areaStream.lineStart(); 119 | } 120 | 121 | function boundsRingEnd() { 122 | boundsRingPoint(lambda00, phi00); 123 | areaStream.lineEnd(); 124 | if (abs(deltaSum) > epsilon) lambda0 = -(lambda1 = 180); 125 | range[0] = lambda0, range[1] = lambda1; 126 | p0 = null; 127 | } 128 | 129 | // Finds the left-right distance between two longitudes. 130 | // This is almost the same as (lambda1 - lambda0 + 360°) % 360°, except that we want 131 | // the distance between ±180° to be 360°. 132 | function angle(lambda0, lambda1) { 133 | return (lambda1 -= lambda0) < 0 ? lambda1 + 360 : lambda1; 134 | } 135 | 136 | function rangeCompare(a, b) { 137 | return a[0] - b[0]; 138 | } 139 | 140 | function rangeContains(range, x) { 141 | return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x; 142 | } 143 | 144 | export default function(feature) { 145 | var i, n, a, b, merged, deltaMax, delta; 146 | 147 | phi1 = lambda1 = -(lambda0 = phi0 = Infinity); 148 | ranges = []; 149 | stream(feature, boundsStream); 150 | 151 | // First, sort ranges by their minimum longitudes. 152 | if (n = ranges.length) { 153 | ranges.sort(rangeCompare); 154 | 155 | // Then, merge any ranges that overlap. 156 | for (i = 1, a = ranges[0], merged = [a]; i < n; ++i) { 157 | b = ranges[i]; 158 | if (rangeContains(a, b[0]) || rangeContains(a, b[1])) { 159 | if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1]; 160 | if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0]; 161 | } else { 162 | merged.push(a = b); 163 | } 164 | } 165 | 166 | // Finally, find the largest gap between the merged ranges. 167 | // The final bounding box will be the inverse of this gap. 168 | for (deltaMax = -Infinity, n = merged.length - 1, i = 0, a = merged[n]; i <= n; a = b, ++i) { 169 | b = merged[i]; 170 | if ((delta = angle(a[1], b[0])) > deltaMax) deltaMax = delta, lambda0 = b[0], lambda1 = a[1]; 171 | } 172 | } 173 | 174 | ranges = range = null; 175 | 176 | return lambda0 === Infinity || phi0 === Infinity 177 | ? [[NaN, NaN], [NaN, NaN]] 178 | : [[lambda0, phi0], [lambda1, phi1]]; 179 | } 180 | -------------------------------------------------------------------------------- /src/clip/circle.js: -------------------------------------------------------------------------------- 1 | import {cartesian, cartesianAddInPlace, cartesianCross, cartesianDot, cartesianScale, spherical} from "../cartesian.js"; 2 | import {circleStream} from "../circle.js"; 3 | import {abs, cos, epsilon, pi, radians, sqrt} from "../math.js"; 4 | import pointEqual from "../pointEqual.js"; 5 | import clip from "./index.js"; 6 | 7 | export default function(radius) { 8 | var cr = cos(radius), 9 | delta = 6 * radians, 10 | smallRadius = cr > 0, 11 | notHemisphere = abs(cr) > epsilon; // TODO optimise for this common case 12 | 13 | function interpolate(from, to, direction, stream) { 14 | circleStream(stream, radius, delta, direction, from, to); 15 | } 16 | 17 | function visible(lambda, phi) { 18 | return cos(lambda) * cos(phi) > cr; 19 | } 20 | 21 | // Takes a line and cuts into visible segments. Return values used for polygon 22 | // clipping: 0 - there were intersections or the line was empty; 1 - no 23 | // intersections 2 - there were intersections, and the first and last segments 24 | // should be rejoined. 25 | function clipLine(stream) { 26 | var point0, // previous point 27 | c0, // code for previous point 28 | v0, // visibility of previous point 29 | v00, // visibility of first point 30 | clean; // no intersections 31 | return { 32 | lineStart: function() { 33 | v00 = v0 = false; 34 | clean = 1; 35 | }, 36 | point: function(lambda, phi) { 37 | var point1 = [lambda, phi], 38 | point2, 39 | v = visible(lambda, phi), 40 | c = smallRadius 41 | ? v ? 0 : code(lambda, phi) 42 | : v ? code(lambda + (lambda < 0 ? pi : -pi), phi) : 0; 43 | if (!point0 && (v00 = v0 = v)) stream.lineStart(); 44 | // Handle degeneracies. 45 | // TODO ignore if not clipping polygons. 46 | if (v !== v0) { 47 | point2 = intersect(point0, point1); 48 | if (!point2 || pointEqual(point0, point2) || pointEqual(point1, point2)) { 49 | point1[0] += epsilon; 50 | point1[1] += epsilon; 51 | v = visible(point1[0], point1[1]); 52 | } 53 | } 54 | if (v !== v0) { 55 | clean = 0; 56 | if (v) { 57 | // outside going in 58 | stream.lineStart(); 59 | point2 = intersect(point1, point0); 60 | stream.point(point2[0], point2[1]); 61 | } else { 62 | // inside going out 63 | point2 = intersect(point0, point1); 64 | stream.point(point2[0], point2[1]); 65 | stream.lineEnd(); 66 | } 67 | point0 = point2; 68 | } else if (notHemisphere && point0 && smallRadius ^ v) { 69 | var t; 70 | // If the codes for two points are different, or are both zero, 71 | // and there this segment intersects with the small circle. 72 | if (!(c & c0) && (t = intersect(point1, point0, true))) { 73 | clean = 0; 74 | if (smallRadius) { 75 | stream.lineStart(); 76 | stream.point(t[0][0], t[0][1]); 77 | stream.point(t[1][0], t[1][1]); 78 | stream.lineEnd(); 79 | } else { 80 | stream.point(t[1][0], t[1][1]); 81 | stream.lineEnd(); 82 | stream.lineStart(); 83 | stream.point(t[0][0], t[0][1]); 84 | } 85 | } 86 | } 87 | if (v && (!point0 || !pointEqual(point0, point1))) { 88 | stream.point(point1[0], point1[1]); 89 | } 90 | point0 = point1, v0 = v, c0 = c; 91 | }, 92 | lineEnd: function() { 93 | if (v0) stream.lineEnd(); 94 | point0 = null; 95 | }, 96 | // Rejoin first and last segments if there were intersections and the first 97 | // and last points were visible. 98 | clean: function() { 99 | return clean | ((v00 && v0) << 1); 100 | } 101 | }; 102 | } 103 | 104 | // Intersects the great circle between a and b with the clip circle. 105 | function intersect(a, b, two) { 106 | var pa = cartesian(a), 107 | pb = cartesian(b); 108 | 109 | // We have two planes, n1.p = d1 and n2.p = d2. 110 | // Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 ⨯ n2). 111 | var n1 = [1, 0, 0], // normal 112 | n2 = cartesianCross(pa, pb), 113 | n2n2 = cartesianDot(n2, n2), 114 | n1n2 = n2[0], // cartesianDot(n1, n2), 115 | determinant = n2n2 - n1n2 * n1n2; 116 | 117 | // Two polar points. 118 | if (!determinant) return !two && a; 119 | 120 | var c1 = cr * n2n2 / determinant, 121 | c2 = -cr * n1n2 / determinant, 122 | n1xn2 = cartesianCross(n1, n2), 123 | A = cartesianScale(n1, c1), 124 | B = cartesianScale(n2, c2); 125 | cartesianAddInPlace(A, B); 126 | 127 | // Solve |p(t)|^2 = 1. 128 | var u = n1xn2, 129 | w = cartesianDot(A, u), 130 | uu = cartesianDot(u, u), 131 | t2 = w * w - uu * (cartesianDot(A, A) - 1); 132 | 133 | if (t2 < 0) return; 134 | 135 | var t = sqrt(t2), 136 | q = cartesianScale(u, (-w - t) / uu); 137 | cartesianAddInPlace(q, A); 138 | q = spherical(q); 139 | 140 | if (!two) return q; 141 | 142 | // Two intersection points. 143 | var lambda0 = a[0], 144 | lambda1 = b[0], 145 | phi0 = a[1], 146 | phi1 = b[1], 147 | z; 148 | 149 | if (lambda1 < lambda0) z = lambda0, lambda0 = lambda1, lambda1 = z; 150 | 151 | var delta = lambda1 - lambda0, 152 | polar = abs(delta - pi) < epsilon, 153 | meridian = polar || delta < epsilon; 154 | 155 | if (!polar && phi1 < phi0) z = phi0, phi0 = phi1, phi1 = z; 156 | 157 | // Check that the first point is between a and b. 158 | if (meridian 159 | ? polar 160 | ? phi0 + phi1 > 0 ^ q[1] < (abs(q[0] - lambda0) < epsilon ? phi0 : phi1) 161 | : phi0 <= q[1] && q[1] <= phi1 162 | : delta > pi ^ (lambda0 <= q[0] && q[0] <= lambda1)) { 163 | var q1 = cartesianScale(u, (-w + t) / uu); 164 | cartesianAddInPlace(q1, A); 165 | return [q, spherical(q1)]; 166 | } 167 | } 168 | 169 | // Generates a 4-bit vector representing the location of a point relative to 170 | // the small circle's bounding box. 171 | function code(lambda, phi) { 172 | var r = smallRadius ? radius : pi - radius, 173 | code = 0; 174 | if (lambda < -r) code |= 1; // left 175 | else if (lambda > r) code |= 2; // right 176 | if (phi < -r) code |= 4; // below 177 | else if (phi > r) code |= 8; // above 178 | return code; 179 | } 180 | 181 | return clip(visible, clipLine, interpolate, smallRadius ? [0, -radius] : [-pi, radius - pi]); 182 | } 183 | -------------------------------------------------------------------------------- /test/path/index-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"), 3 | testContext = require("./test-context"); 4 | 5 | var equirectangular = d3_geo.geoEquirectangular() 6 | .scale(900 / Math.PI) 7 | .precision(0); 8 | 9 | function testPath(projection, object) { 10 | var context = testContext(); 11 | 12 | d3_geo.geoPath() 13 | .projection(projection) 14 | .context(context) 15 | (object); 16 | 17 | return context.result(); 18 | } 19 | 20 | tape("geoPath.projection() defaults to null", function(test) { 21 | var path = d3_geo.geoPath(); 22 | test.strictEqual(path.projection(), null); 23 | test.end(); 24 | }); 25 | 26 | tape("geoPath.context() defaults to null", function(test) { 27 | var path = d3_geo.geoPath(); 28 | test.strictEqual(path.context(), null); 29 | test.end(); 30 | }); 31 | 32 | tape("d3.geoPath(projection) sets the initial projection", function(test) { 33 | var projection = d3_geo.geoAlbers(), path = d3_geo.geoPath(projection); 34 | test.strictEqual(path.projection(), projection); 35 | test.end(); 36 | }); 37 | 38 | tape("d3.geoPath(projection, context) sets the initial projection and context", function(test) { 39 | var context = testContext(), projection = d3_geo.geoAlbers(), path = d3_geo.geoPath(projection, context); 40 | test.strictEqual(path.projection(), projection); 41 | test.strictEqual(path.context(), context); 42 | test.end(); 43 | }); 44 | 45 | tape("geoPath(Point) renders a point", function(test) { 46 | test.deepEqual(testPath(equirectangular, { 47 | type: "Point", 48 | coordinates: [-63, 18] 49 | }), [ 50 | {type: "moveTo", x: 170, y: 160}, 51 | {type: "arc", x: 165, y: 160, r: 4.5} 52 | ]); 53 | test.end(); 54 | }); 55 | 56 | tape("geoPath(MultiPoint) renders a point", function(test) { 57 | test.deepEqual(testPath(equirectangular, { 58 | type: "MultiPoint", 59 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 60 | }), [ 61 | {type: "moveTo", x: 170, y: 160}, {type: "arc", x: 165, y: 160, r: 4.5}, 62 | {type: "moveTo", x: 175, y: 160}, {type: "arc", x: 170, y: 160, r: 4.5}, 63 | {type: "moveTo", x: 175, y: 165}, {type: "arc", x: 170, y: 165, r: 4.5} 64 | ]); 65 | test.end(); 66 | }); 67 | 68 | tape("geoPath(LineString) renders a line string", function(test) { 69 | test.deepEqual(testPath(equirectangular, { 70 | type: "LineString", 71 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 72 | }), [ 73 | {type: "moveTo", x: 165, y: 160}, 74 | {type: "lineTo", x: 170, y: 160}, 75 | {type: "lineTo", x: 170, y: 165} 76 | ]); 77 | test.end(); 78 | }); 79 | 80 | tape("geoPath(Polygon) renders a polygon", function(test) { 81 | test.deepEqual(testPath(equirectangular, { 82 | type: "Polygon", 83 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 84 | }), [ 85 | {type: "moveTo", x: 165, y: 160}, 86 | {type: "lineTo", x: 170, y: 160}, 87 | {type: "lineTo", x: 170, y: 165}, 88 | {type: "closePath"} 89 | ]); 90 | test.end(); 91 | }); 92 | 93 | tape("geoPath(GeometryCollection) renders a geometry collection", function(test) { 94 | test.deepEqual(testPath(equirectangular, { 95 | type: "GeometryCollection", 96 | geometries: [{ 97 | type: "Polygon", 98 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 99 | }] 100 | }), [ 101 | {type: "moveTo", x: 165, y: 160}, 102 | {type: "lineTo", x: 170, y: 160}, 103 | {type: "lineTo", x: 170, y: 165}, 104 | {type: "closePath"} 105 | ]); 106 | test.end(); 107 | }); 108 | 109 | tape("geoPath(Feature) renders a feature", function(test) { 110 | test.deepEqual(testPath(equirectangular, { 111 | type: "Feature", 112 | geometry: { 113 | type: "Polygon", 114 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 115 | } 116 | }), [ 117 | {type: "moveTo", x: 165, y: 160}, 118 | {type: "lineTo", x: 170, y: 160}, 119 | {type: "lineTo", x: 170, y: 165}, 120 | {type: "closePath"} 121 | ]); 122 | test.end(); 123 | }); 124 | 125 | tape("geoPath(FeatureCollection) renders a feature collection", function(test) { 126 | test.deepEqual(testPath(equirectangular, { 127 | type: "FeatureCollection", 128 | features: [{ 129 | type: "Feature", 130 | geometry: { 131 | type: "Polygon", 132 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 133 | } 134 | }] 135 | }), [ 136 | {type: "moveTo", x: 165, y: 160}, 137 | {type: "lineTo", x: 170, y: 160}, 138 | {type: "lineTo", x: 170, y: 165}, 139 | {type: "closePath"} 140 | ]); 141 | test.end(); 142 | }); 143 | 144 | tape("geoPath(…) wraps longitudes outside of ±180°", function(test) { 145 | test.deepEqual(testPath(equirectangular, { 146 | type: "Point", 147 | coordinates: [180 + 1e-6, 0] 148 | }), [ 149 | {type: "moveTo", x: -415, y: 250}, 150 | {type: "arc", x: -420, y: 250, r: 4.5} 151 | ]); 152 | test.end(); 153 | }); 154 | 155 | tape("geoPath(…) observes the correct winding order of a tiny polygon", function(test) { 156 | test.deepEqual(testPath(equirectangular, { 157 | type: "Polygon", 158 | coordinates: [[ 159 | [-0.06904102953339501, 0.346043661846373], 160 | [-6.725674252975136e-15, 0.3981303360336475], 161 | [-6.742247658534323e-15, -0.08812465346531581], 162 | [-0.17301258217724075, -0.12278150669440671], 163 | [-0.06904102953339501, 0.346043661846373] 164 | ]] 165 | }), [ 166 | {type: "moveTo", x: 480, y: 248}, 167 | {type: "lineTo", x: 480, y: 248}, 168 | {type: "lineTo", x: 480, y: 250}, 169 | {type: "lineTo", x: 479, y: 251}, 170 | {type: "closePath"} 171 | ]); 172 | test.end(); 173 | }); 174 | 175 | tape("geoPath.projection(null)(…) does not transform coordinates", function(test) { 176 | test.deepEqual(testPath(null, { 177 | type: "Polygon", 178 | coordinates: [[[-63, 18], [-62, 18], [-62, 17], [-63, 18]]] 179 | }), [ 180 | {type: "moveTo", x: -63, y: 18}, 181 | {type: "lineTo", x: -62, y: 18}, 182 | {type: "lineTo", x: -62, y: 17}, 183 | {type: "closePath"} 184 | ]); 185 | test.end(); 186 | }); 187 | 188 | tape("geoPath.context(null)(null) returns null", function(test) { 189 | var path = d3_geo.geoPath(); 190 | test.strictEqual(path(), null); 191 | test.strictEqual(path(null), null); 192 | test.strictEqual(path(undefined), null); 193 | test.end(); 194 | }); 195 | 196 | tape("geoPath.context(null)(Unknown) returns null", function(test) { 197 | var path = d3_geo.geoPath(); 198 | test.strictEqual(path({type: "Unknown"}), null); 199 | test.strictEqual(path({type: "__proto__"}), null); 200 | test.end(); 201 | }); 202 | 203 | tape("geoPath(LineString) then geoPath(Point) does not treat the point as part of a line", function(test) { 204 | var context = testContext(), path = d3_geo.geoPath().projection(equirectangular).context(context); 205 | path({ 206 | type: "LineString", 207 | coordinates: [[-63, 18], [-62, 18], [-62, 17]] 208 | }); 209 | test.deepEqual(context.result(), [ 210 | {type: "moveTo", x: 165, y: 160}, 211 | {type: "lineTo", x: 170, y: 160}, 212 | {type: "lineTo", x: 170, y: 165} 213 | ]); 214 | path({ 215 | type: "Point", 216 | coordinates: [-63, 18] 217 | }); 218 | test.deepEqual(context.result(), [ 219 | {type: "moveTo", x: 170, y: 160}, 220 | {type: "arc", x: 165, y: 160, r: 4.5} 221 | ]); 222 | test.end(); 223 | }); 224 | -------------------------------------------------------------------------------- /test/path/centroid-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3_geo = require("../../"), 3 | testContext = require("./test-context"); 4 | 5 | require("../inDelta"); 6 | 7 | var equirectangular = d3_geo.geoEquirectangular() 8 | .scale(900 / Math.PI) 9 | .precision(0); 10 | 11 | function testCentroid(projection, object) { 12 | return d3_geo.geoPath() 13 | .projection(projection) 14 | .centroid(object); 15 | } 16 | 17 | tape("geoPath.centroid(…) of a point", function(test) { 18 | test.deepEqual(testCentroid(equirectangular, {type: "Point", coordinates: [0, 0]}), [480, 250]); 19 | test.end(); 20 | }); 21 | 22 | tape("geoPath.centroid(…) of an empty multipoint", function(test) { 23 | test.equal(testCentroid(equirectangular, {type: "MultiPoint", coordinates: []}).every(isNaN), true); 24 | test.end(); 25 | }); 26 | 27 | tape("geoPath.centroid(…) of a singleton multipoint", function(test) { 28 | test.deepEqual(testCentroid(equirectangular, {type: "MultiPoint", coordinates: [[0, 0]]}), [480, 250]); 29 | test.end(); 30 | }); 31 | 32 | tape("geoPath.centroid(…) of a multipoint with two points", function(test) { 33 | test.deepEqual(testCentroid(equirectangular, {type: "MultiPoint", coordinates: [[-122, 37], [-74, 40]]}), [-10, 57.5]); 34 | test.end(); 35 | }); 36 | 37 | tape("geoPath.centroid(…) of an empty linestring", function(test) { 38 | test.equal(testCentroid(equirectangular, {type: "LineString", coordinates: []}).every(isNaN), true); 39 | test.end(); 40 | }); 41 | 42 | tape("geoPath.centroid(…) of a linestring with two points", function(test) { 43 | test.deepEqual(testCentroid(equirectangular, {type: "LineString", coordinates: [[100, 0], [0, 0]]}), [730, 250]); 44 | test.deepEqual(testCentroid(equirectangular, {type: "LineString", coordinates: [[0, 0], [100, 0], [101, 0]]}), [732.5, 250]); 45 | test.end(); 46 | }); 47 | 48 | tape("geoPath.centroid(…) of a linestring with two points, one unique", function(test) { 49 | test.deepEqual(testCentroid(equirectangular, {type: "LineString", coordinates: [[-122, 37], [-122, 37]]}), [-130, 65]); 50 | test.deepEqual(testCentroid(equirectangular, {type: "LineString", coordinates: [[-74, 40], [-74, 40]]}), [110, 50]); 51 | test.end(); 52 | }); 53 | 54 | tape("geoPath.centroid(…) of a linestring with three points; two unique", function(test) { 55 | test.deepEqual(testCentroid(equirectangular, {type: "LineString", coordinates: [[-122, 37], [-74, 40], [-74, 40]]}), [-10, 57.5]); 56 | test.end(); 57 | }); 58 | 59 | tape("geoPath.centroid(…) of a linestring with three points", function(test) { 60 | test.inDelta(testCentroid(equirectangular, {type: "LineString", coordinates: [[-122, 37], [-74, 40], [-100, 0]]}), [17.389135, 103.563545], 1e-6); 61 | test.end(); 62 | }); 63 | 64 | tape("geoPath.centroid(…) of a multilinestring", function(test) { 65 | test.deepEqual(testCentroid(equirectangular, {type: "MultiLineString", coordinates: [[[100, 0], [0, 0]], [[-10, 0], [0, 0]]]}), [705, 250]); 66 | test.end(); 67 | }); 68 | 69 | tape("geoPath.centroid(…) of a single-ring polygon", function(test) { 70 | test.deepEqual(testCentroid(equirectangular, {type: "Polygon", coordinates: [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]]}), [982.5, 247.5]); 71 | test.end(); 72 | }); 73 | 74 | tape("geoPath.centroid(…) of a zero-area polygon", function(test) { 75 | test.deepEqual(testCentroid(equirectangular, {type: "Polygon", coordinates: [[[1, 0], [2, 0], [3, 0], [1, 0]]]}), [490, 250]); 76 | test.end(); 77 | }); 78 | 79 | tape("geoPath.centroid(…) of a polygon with two rings, one with zero area", function(test) { 80 | test.deepEqual(testCentroid(equirectangular, {type: "Polygon", coordinates: [ 81 | [[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]], 82 | [[100.1, 0], [100.2, 0], [100.3, 0], [100.1, 0] 83 | ]]}), [982.5, 247.5]); 84 | test.end(); 85 | }); 86 | 87 | tape("geoPath.centroid(…) of a polygon with clockwise exterior and anticlockwise interior", function(test) { 88 | test.inDelta(testCentroid(equirectangular, { 89 | type: "Polygon", 90 | coordinates: [ 91 | [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]].reverse(), 92 | [[ 0, -1], [1, -1], [1, 1], [ 0, 1], [ 0, -1]] 93 | ] 94 | }), [479.642857, 250], 1e-6); 95 | test.end(); 96 | }); 97 | 98 | tape("geoPath.centroid(…) of an empty multipolygon", function(test) { 99 | test.equal(testCentroid(equirectangular, {type: "MultiPolygon", coordinates: []}).every(isNaN), true); 100 | test.end(); 101 | }); 102 | 103 | tape("geoPath.centroid(…) of a singleton multipolygon", function(test) { 104 | test.deepEqual(testCentroid(equirectangular, {type: "MultiPolygon", coordinates: [[[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]]]}), [982.5, 247.5]); 105 | test.end(); 106 | }); 107 | 108 | tape("geoPath.centroid(…) of a multipolygon with two polygons", function(test) { 109 | test.deepEqual(testCentroid(equirectangular, {type: "MultiPolygon", coordinates: [ 110 | [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]], 111 | [[[0, 0], [1, 0], [1, -1], [0, -1], [0, 0]]] 112 | ]}), [732.5, 250]); 113 | test.end(); 114 | }); 115 | 116 | tape("geoPath.centroid(…) of a multipolygon with two polygons, one zero area", function(test) { 117 | test.deepEqual(testCentroid(equirectangular, {type: "MultiPolygon", coordinates: [ 118 | [[[100, 0], [100, 1], [101, 1], [101, 0], [100, 0]]], 119 | [[[0, 0], [1, 0], [2, 0], [0, 0]]] 120 | ]}), [982.5, 247.5]); 121 | test.end(); 122 | }); 123 | 124 | tape("geoPath.centroid(…) of a geometry collection with a single point", function(test) { 125 | test.deepEqual(testCentroid(equirectangular, {type: "GeometryCollection", geometries: [{type: "Point", coordinates: [0, 0]}]}), [480, 250]); 126 | test.end(); 127 | }); 128 | 129 | tape("geoPath.centroid(…) of a geometry collection with a point and a linestring", function(test) { 130 | test.deepEqual(testCentroid(equirectangular, {type: "GeometryCollection", geometries: [ 131 | {type: "LineString", coordinates: [[179, 0], [180, 0]]}, 132 | {type: "Point", coordinates: [0, 0]} 133 | ]}), [1377.5, 250]); 134 | test.end(); 135 | }); 136 | 137 | tape("geoPath.centroid(…) of a geometry collection with a point, linestring and polygon", function(test) { 138 | test.deepEqual(testCentroid(equirectangular, {type: "GeometryCollection", geometries: [ 139 | {type: "Polygon", coordinates: [[[-180, 0], [-180, 1], [-179, 1], [-179, 0], [-180, 0]]]}, 140 | {type: "LineString", coordinates: [[179, 0], [180, 0]]}, 141 | {type: "Point", coordinates: [0, 0]} 142 | ]}), [-417.5, 247.5]); 143 | test.end(); 144 | }); 145 | 146 | tape("geoPath.centroid(…) of a feature collection with a point", function(test) { 147 | test.deepEqual(testCentroid(equirectangular, {type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Point", coordinates: [0, 0]}}]}), [480, 250]); 148 | test.end(); 149 | }); 150 | 151 | tape("geoPath.centroid(…) of a feature collection with a point and a line string", function(test) { 152 | test.deepEqual(testCentroid(equirectangular, {type: "FeatureCollection", features: [ 153 | {type: "Feature", geometry: {type: "LineString", coordinates: [[179, 0], [180, 0]]}}, 154 | {type: "Feature", geometry: {type: "Point", coordinates: [0, 0]}} 155 | ]}), [1377.5, 250]); 156 | test.end(); 157 | }); 158 | 159 | tape("geoPath.centroid(…) of a feature collection with a point, line string and polygon", function(test) { 160 | test.deepEqual(testCentroid(equirectangular, {type: "FeatureCollection", features: [ 161 | {type: "Feature", geometry: {type: "Polygon", coordinates: [[[-180, 0], [-180, 1], [-179, 1], [-179, 0], [-180, 0]]]}}, 162 | {type: "Feature", geometry: {type: "LineString", coordinates: [[179, 0], [180, 0]]}}, 163 | {type: "Feature", geometry: {type: "Point", coordinates: [0, 0]}} 164 | ]}), [-417.5, 247.5]); 165 | test.end(); 166 | }); 167 | 168 | tape("geoPath.centroid(…) of a sphere", function(test) { 169 | test.deepEqual(testCentroid(equirectangular, {type: "Sphere"}), [480, 250]); 170 | test.end(); 171 | }); 172 | -------------------------------------------------------------------------------- /test/stream-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | d3 = require("../"); 3 | 4 | tape("geoStream(object) ignores unknown types", function(test) { 5 | d3.geoStream({type: "Unknown"}, {}); 6 | d3.geoStream({type: "Feature", geometry: {type: "Unknown"}}, {}); 7 | d3.geoStream({type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Unknown"}}]}, {}); 8 | d3.geoStream({type: "GeometryCollection", geometries: [{type: "Unknown"}]}, {}); 9 | test.end(); 10 | }); 11 | 12 | tape("geoStream(object) ignores null geometries", function(test) { 13 | d3.geoStream(null, {}); 14 | d3.geoStream({type: "Feature", geometry: null }, {}); 15 | d3.geoStream({type: "FeatureCollection", features: [{type: "Feature", geometry: null }]}, {}); 16 | d3.geoStream({type: "GeometryCollection", geometries: [null]}, {}); 17 | test.end(); 18 | }); 19 | 20 | tape("geoStream(object) returns void", function(test) { 21 | test.equal(d3.geoStream({type: "Point", coordinates: [1, 2]}, {point: function() { return true; }}), undefined); 22 | test.end(); 23 | }); 24 | 25 | tape("geoStream(object) allows empty multi-geometries", function(test) { 26 | d3.geoStream({type: "MultiPoint", coordinates: []}, {}); 27 | d3.geoStream({type: "MultiLineString", coordinates: []}, {}); 28 | d3.geoStream({type: "MultiPolygon", coordinates: []}, {}); 29 | test.end(); 30 | }); 31 | 32 | tape("geoStream(Sphere) ↦ sphere", function(test) { 33 | var calls = 0; 34 | d3.geoStream({type: "Sphere"}, { 35 | sphere: function() { 36 | test.equal(arguments.length, 0); 37 | test.equal(++calls, 1); 38 | } 39 | }); 40 | test.equal(calls, 1); 41 | test.end(); 42 | }); 43 | 44 | tape("geoStream(Point) ↦ point", function(test) { 45 | var calls = 0, coordinates = 0; 46 | d3.geoStream({type: "Point", coordinates: [1, 2, 3]}, { 47 | point: function(x, y, z) { 48 | test.equal(arguments.length, 3); 49 | test.equal(x, ++coordinates); 50 | test.equal(y, ++coordinates); 51 | test.equal(z, ++coordinates); 52 | test.equal(++calls, 1); 53 | } 54 | }); 55 | test.equal(calls, 1); 56 | test.end(); 57 | }); 58 | 59 | tape("geoStream(MultiPoint) ↦ point*", function(test) { 60 | var calls = 0, coordinates = 0; 61 | d3.geoStream({type: "MultiPoint", coordinates: [[1, 2, 3], [4, 5, 6]]}, { 62 | point: function(x, y, z) { 63 | test.equal(arguments.length, 3); 64 | test.equal(x, ++coordinates); 65 | test.equal(y, ++coordinates); 66 | test.equal(z, ++coordinates); 67 | test.equal(1 <= ++calls && calls <= 2, true); 68 | } 69 | }); 70 | test.equal(calls, 2); 71 | test.end(); 72 | }); 73 | 74 | tape("geoStream(LineString) ↦ lineStart, point{2,}, lineEnd", function(test) { 75 | var calls = 0, coordinates = 0; 76 | d3.geoStream({type: "LineString", coordinates: [[1, 2, 3], [4, 5, 6]]}, { 77 | lineStart: function() { 78 | test.equal(arguments.length, 0); 79 | test.equal(++calls, 1); 80 | }, 81 | point: function(x, y, z) { 82 | test.equal(arguments.length, 3); 83 | test.equal(x, ++coordinates); 84 | test.equal(y, ++coordinates); 85 | test.equal(z, ++coordinates); 86 | test.equal(2 <= ++calls && calls <= 3, true); 87 | }, 88 | lineEnd: function() { 89 | test.equal(arguments.length, 0); 90 | test.equal(++calls, 4); 91 | } 92 | }); 93 | test.equal(calls, 4); 94 | test.end(); 95 | }); 96 | 97 | tape("geoStream(MultiLineString) ↦ (lineStart, point{2,}, lineEnd)*", function(test) { 98 | var calls = 0, coordinates = 0; 99 | d3.geoStream({type: "MultiLineString", coordinates: [[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]}, { 100 | lineStart: function() { 101 | test.equal(arguments.length, 0); 102 | test.equal(++calls === 1 || calls === 5, true); 103 | }, 104 | point: function(x, y, z) { 105 | test.equal(arguments.length, 3); 106 | test.equal(x, ++coordinates); 107 | test.equal(y, ++coordinates); 108 | test.equal(z, ++coordinates); 109 | test.equal(2 <= ++calls && calls <= 3 || 6 <= calls && calls <= 7, true); 110 | }, 111 | lineEnd: function() { 112 | test.equal(arguments.length, 0); 113 | test.equal(++calls === 4 || calls === 8, true); 114 | } 115 | }); 116 | test.equal(calls, 8); 117 | test.end(); 118 | }); 119 | 120 | tape("geoStream(Polygon) ↦ polygonStart, lineStart, point{2,}, lineEnd, polygonEnd", function(test) { 121 | var calls = 0, coordinates = 0; 122 | d3.geoStream({type: "Polygon", coordinates: [[[1, 2, 3], [4, 5, 6], [1, 2, 3]], [[7, 8, 9], [10, 11, 12], [7, 8, 9]]]}, { 123 | polygonStart: function() { 124 | test.equal(arguments.length, 0); 125 | test.equal(++calls === 1, true); 126 | }, 127 | lineStart: function() { 128 | test.equal(arguments.length, 0); 129 | test.equal(++calls === 2 || calls === 6, true); 130 | }, 131 | point: function(x, y, z) { 132 | test.equal(arguments.length, 3); 133 | test.equal(x, ++coordinates); 134 | test.equal(y, ++coordinates); 135 | test.equal(z, ++coordinates); 136 | test.equal(3 <= ++calls && calls <= 4 || 7 <= calls && calls <= 8, true); 137 | }, 138 | lineEnd: function() { 139 | test.equal(arguments.length, 0); 140 | test.equal(++calls === 5 || calls === 9, true); 141 | }, 142 | polygonEnd: function() { 143 | test.equal(arguments.length, 0); 144 | test.equal(++calls === 10, true); 145 | } 146 | }); 147 | test.equal(calls, 10); 148 | test.end(); 149 | }); 150 | 151 | tape("geoStream(MultiPolygon) ↦ (polygonStart, lineStart, point{2,}, lineEnd, polygonEnd)*", function(test) { 152 | var calls = 0, coordinates = 0; 153 | d3.geoStream({type: "MultiPolygon", coordinates: [[[[1, 2, 3], [4, 5, 6], [1, 2, 3]]], [[[7, 8, 9], [10, 11, 12], [7, 8, 9]]]]}, { 154 | polygonStart: function() { 155 | test.equal(arguments.length, 0); 156 | test.equal(++calls === 1 || calls === 7, true); 157 | }, 158 | lineStart: function() { 159 | test.equal(arguments.length, 0); 160 | test.equal(++calls === 2 || calls === 8, true); 161 | }, 162 | point: function(x, y, z) { 163 | test.equal(arguments.length, 3); 164 | test.equal(x, ++coordinates); 165 | test.equal(y, ++coordinates); 166 | test.equal(z, ++coordinates); 167 | test.equal(3 <= ++calls && calls <= 4 || 9 <= calls && calls <= 10, true); 168 | }, 169 | lineEnd: function() { 170 | test.equal(arguments.length, 0); 171 | test.equal(++calls === 5 || calls === 11, true); 172 | }, 173 | polygonEnd: function() { 174 | test.equal(arguments.length, 0); 175 | test.equal(++calls === 6 || calls === 12, true); 176 | } 177 | }); 178 | test.equal(calls, 12); 179 | test.end(); 180 | }); 181 | 182 | tape("geoStream(Feature) ↦ .*", function(test) { 183 | var calls = 0, coordinates = 0; 184 | d3.geoStream({type: "Feature", geometry: {type: "Point", coordinates: [1, 2, 3]}}, { 185 | point: function(x, y, z) { 186 | test.equal(arguments.length, 3); 187 | test.equal(x, ++coordinates); 188 | test.equal(y, ++coordinates); 189 | test.equal(z, ++coordinates); 190 | test.equal(++calls, 1); 191 | } 192 | }); 193 | test.equal(calls, 1); 194 | test.end(); 195 | }); 196 | 197 | tape("geoStream(FeatureCollection) ↦ .*", function(test) { 198 | var calls = 0, coordinates = 0; 199 | d3.geoStream({type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Point", coordinates: [1, 2, 3]}}]}, { 200 | point: function(x, y, z) { 201 | test.equal(arguments.length, 3); 202 | test.equal(x, ++coordinates); 203 | test.equal(y, ++coordinates); 204 | test.equal(z, ++coordinates); 205 | test.equal(++calls, 1); 206 | } 207 | }); 208 | test.equal(calls, 1); 209 | test.end(); 210 | }); 211 | 212 | tape("geoStream(GeometryCollection) ↦ .*", function(test) { 213 | var calls = 0, coordinates = 0; 214 | d3.geoStream({type: "GeometryCollection", geometries: [{type: "Point", coordinates: [1, 2, 3]}]}, { 215 | point: function(x, y, z) { 216 | test.equal(arguments.length, 3); 217 | test.equal(x, ++coordinates); 218 | test.equal(y, ++coordinates); 219 | test.equal(z, ++coordinates); 220 | test.equal(++calls, 1); 221 | } 222 | }); 223 | test.equal(calls, 1); 224 | test.end(); 225 | }); 226 | -------------------------------------------------------------------------------- /test/area-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | array = require("d3-array"), 3 | d3 = require("../"); 4 | 5 | require("./inDelta"); 6 | 7 | function stripes(a, b) { 8 | return {type: "Polygon", coordinates: [a, b].map(function(d, i) { 9 | var stripe = array.range(-180, 180, 0.1).map(function(x) { return [x, d]; }); 10 | stripe.push(stripe[0]); 11 | return i ? stripe.reverse() : stripe; 12 | })}; 13 | } 14 | 15 | tape("area: Point", function(test) { 16 | test.equal(d3.geoArea({type: "Point", coordinates: [0, 0]}), 0); 17 | test.end(); 18 | }); 19 | 20 | tape("area: MultiPoint", function(test) { 21 | test.equal(d3.geoArea({type: "MultiPoint", coordinates: [[0, 1], [2, 3]]}), 0); 22 | test.end(); 23 | }); 24 | 25 | tape("area: LineString", function(test) { 26 | test.equal(d3.geoArea({type: "LineString", coordinates: [[0, 1], [2, 3]]}), 0); 27 | test.end(); 28 | }); 29 | 30 | tape("area: MultiLineString", function(test) { 31 | test.equal(d3.geoArea({type: "MultiLineString", coordinates: [[[0, 1], [2, 3]], [[4, 5], [6, 7]]]}), 0); 32 | test.end(); 33 | }); 34 | 35 | tape("area: Polygon - tiny", function(test) { 36 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[ 37 | [-64.66070178517852, 18.33986913231323], 38 | [-64.66079715091509, 18.33994007490749], 39 | [-64.66074946804680, 18.33994007490749], 40 | [-64.66070178517852, 18.33986913231323] 41 | ]]}), 4.890516e-13, 1e-13); 42 | test.end(); 43 | }); 44 | 45 | tape("area: Polygon - zero area", function(test) { 46 | test.equal(d3.geoArea({ 47 | "type": "Polygon", 48 | "coordinates": [[ 49 | [96.79142432523281, 5.262704519048153], 50 | [96.81065389253769, 5.272455576551362], 51 | [96.82988345984256, 5.272455576551362], 52 | [96.81065389253769, 5.272455576551362], 53 | [96.79142432523281, 5.262704519048153] 54 | ]] 55 | }), 0); 56 | test.end(); 57 | }); 58 | 59 | tape("area: Polygon - semilune", function(test) { 60 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [0, 90], [90, 0], [0, 0]]]}), Math.PI / 2, 1e-6); 61 | test.end(); 62 | }); 63 | 64 | tape("area: Polygon - lune", function(test) { 65 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [0, 90], [90, 0], [0, -90], [0, 0]]]}), Math.PI, 1e-6); 66 | test.end(); 67 | }); 68 | 69 | tape("area: Polygon - hemispheres North", function(test) { 70 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [-90, 0], [180, 0], [90, 0], [0, 0]]]}), 2 * Math.PI, 1e-6); 71 | test.end(); 72 | }); 73 | 74 | tape("area: Polygon - hemispheres South", function(test) { 75 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [90, 0], [180, 0], [-90, 0], [0, 0]]]}), 2 * Math.PI, 1e-6); 76 | test.end(); 77 | }); 78 | 79 | tape("area: Polygon - hemispheres East", function(test) { 80 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [0, 90], [180, 0], [0, -90], [0, 0]]]}), 2 * Math.PI, 1e-6); 81 | test.end(); 82 | }); 83 | 84 | tape("area: Polygon - hemispheres West", function(test) { 85 | test.inDelta(d3.geoArea({type: "Polygon", coordinates: [[[0, 0], [0, -90], [180, 0], [0, 90], [0, 0]]]}), 2 * Math.PI, 1e-6); 86 | test.end(); 87 | }); 88 | 89 | tape("area: Polygon - graticule outline sphere", function(test) { 90 | test.inDelta(d3.geoArea(d3.geoGraticule().extent([[-180, -90], [180, 90]]).outline()), 4 * Math.PI, 1e-5); 91 | test.end(); 92 | }); 93 | 94 | tape("area: Polygon - graticule outline hemisphere", function(test) { 95 | test.inDelta(d3.geoArea(d3.geoGraticule().extent([[-180, 0], [180, 90]]).outline()), 2 * Math.PI, 1e-5); 96 | test.end(); 97 | }); 98 | 99 | tape("area: Polygon - graticule outline semilune", function(test) { 100 | test.inDelta(d3.geoArea(d3.geoGraticule().extent([[0, 0], [90, 90]]).outline()), Math.PI / 2, 1e-5); 101 | test.end(); 102 | }); 103 | 104 | tape("area: Polygon - circles hemisphere", function(test) { 105 | test.inDelta(d3.geoArea(d3.geoCircle().radius(90)()), 2 * Math.PI, 1e-5); 106 | test.end(); 107 | }); 108 | 109 | tape("area: Polygon - circles 60°", function(test) { 110 | test.inDelta(d3.geoArea(d3.geoCircle().radius(60).precision(0.1)()), Math.PI, 1e-5); 111 | test.end(); 112 | }); 113 | 114 | tape("area: Polygon - circles 60° North", function(test) { 115 | test.inDelta(d3.geoArea(d3.geoCircle().radius(60).precision(0.1).center([0, 90])()), Math.PI, 1e-5); 116 | test.end(); 117 | }); 118 | 119 | tape("area: Polygon - circles 45°", function(test) { 120 | test.inDelta(d3.geoArea(d3.geoCircle().radius(45).precision(0.1)()), Math.PI * (2 - Math.SQRT2), 1e-5); 121 | test.end(); 122 | }); 123 | 124 | tape("area: Polygon - circles 45° North", function(test) { 125 | test.inDelta(d3.geoArea(d3.geoCircle().radius(45).precision(0.1).center([0, 90])()), Math.PI * (2 - Math.SQRT2), 1e-5); 126 | test.end(); 127 | }); 128 | 129 | tape("area: Polygon - circles 45° South", function(test) { 130 | test.inDelta(d3.geoArea(d3.geoCircle().radius(45).precision(0.1).center([0, -90])()), Math.PI * (2 - Math.SQRT2), 1e-5); 131 | test.end(); 132 | }); 133 | 134 | tape("area: Polygon - circles 135°", function(test) { 135 | test.inDelta(d3.geoArea(d3.geoCircle().radius(135).precision(0.1)()), Math.PI * (2 + Math.SQRT2), 1e-5); 136 | test.end(); 137 | }); 138 | 139 | tape("area: Polygon - circles 135° North", function(test) { 140 | test.inDelta(d3.geoArea(d3.geoCircle().radius(135).precision(0.1).center([0, 90])()), Math.PI * (2 + Math.SQRT2), 1e-5); 141 | test.end(); 142 | }); 143 | 144 | tape("area: Polygon - circles 135° South", function(test) { 145 | test.inDelta(d3.geoArea(d3.geoCircle().radius(135).precision(0.1).center([0, -90])()), Math.PI * (2 + Math.SQRT2), 1e-5); 146 | test.end(); 147 | }); 148 | 149 | tape("area: Polygon - circles tiny", function(test) { 150 | test.inDelta(d3.geoArea(d3.geoCircle().radius(1e-6).precision(0.1)()), 0, 1e-6); 151 | test.end(); 152 | }); 153 | 154 | tape("area: Polygon - circles huge", function(test) { 155 | test.inDelta(d3.geoArea(d3.geoCircle().radius(180 - 1e-6).precision(0.1)()), 4 * Math.PI, 1e-6); 156 | test.end(); 157 | }); 158 | 159 | tape("area: Polygon - circles 60° with 45° hole", function(test) { 160 | var circle = d3.geoCircle().precision(0.1); 161 | test.inDelta(d3.geoArea({ 162 | type: "Polygon", 163 | coordinates: [ 164 | circle.radius(60)().coordinates[0], 165 | circle.radius(45)().coordinates[0].reverse() 166 | ] 167 | }), Math.PI * (Math.SQRT2 - 1), 1e-5); 168 | test.end(); 169 | }); 170 | 171 | tape("area: Polygon - circles 45° holes at [0°, 0°] and [0°, 90°]", function(test) { 172 | var circle = d3.geoCircle().precision(0.1).radius(45); 173 | test.inDelta(d3.geoArea({ 174 | type: "Polygon", 175 | coordinates: [ 176 | circle.center([0, 0])().coordinates[0].reverse(), 177 | circle.center([0, 90])().coordinates[0].reverse() 178 | ] 179 | }), Math.PI * 2 * Math.SQRT2, 1e-5); 180 | test.end(); 181 | }); 182 | 183 | tape("area: Polygon - circles 45° holes at [0°, 90°] and [0°, 0°]", function(test) { 184 | var circle = d3.geoCircle().precision(0.1).radius(45); 185 | test.inDelta(d3.geoArea({ 186 | type: "Polygon", 187 | coordinates: [ 188 | circle.center([0, 90])().coordinates[0].reverse(), 189 | circle.center([0, 0])().coordinates[0].reverse() 190 | ] 191 | }), Math.PI * 2 * Math.SQRT2, 1e-5); 192 | test.end(); 193 | }); 194 | 195 | tape("area: Polygon - stripes 45°, -45°", function(test) { 196 | test.inDelta(d3.geoArea(stripes(45, -45)), Math.PI * 2 * Math.SQRT2, 1e-5); 197 | test.end(); 198 | }); 199 | 200 | tape("area: Polygon - stripes -45°, 45°", function(test) { 201 | test.inDelta(d3.geoArea(stripes(-45, 45)), Math.PI * 2 * (2 - Math.SQRT2), 1e-5); 202 | test.end(); 203 | }); 204 | 205 | tape("area: Polygon - stripes 45°, 30°", function(test) { 206 | test.inDelta(d3.geoArea(stripes(45, 30)), Math.PI * (Math.SQRT2 - 1), 1e-5); 207 | test.end(); 208 | }); 209 | 210 | tape("area: MultiPolygon two hemispheres", function(test) { 211 | test.equal(d3.geoArea({type: "MultiPolygon", coordinates: [ 212 | [[[0, 0], [-90, 0], [180, 0], [90, 0], [0, 0]]], 213 | [[[0, 0], [90, 0], [180, 0], [-90, 0], [0, 0]]] 214 | ]}), 4 * Math.PI); 215 | test.end(); 216 | }); 217 | 218 | tape("area: Sphere", function(test) { 219 | test.equal(d3.geoArea({type: "Sphere"}), 4 * Math.PI); 220 | test.end(); 221 | }); 222 | 223 | tape("area: GeometryCollection", function(test) { 224 | test.equal(d3.geoArea({type: "GeometryCollection", geometries: [{type: "Sphere"}]}), 4 * Math.PI); 225 | test.end(); 226 | }); 227 | 228 | tape("area: FeatureCollection", function(test) { 229 | test.equal(d3.geoArea({type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Sphere"}}]}), 4 * Math.PI); 230 | test.end(); 231 | }); 232 | 233 | tape("area: Feature", function(test) { 234 | test.equal(d3.geoArea({type: "Feature", geometry: {type: "Sphere"}}), 4 * Math.PI); 235 | test.end(); 236 | }); 237 | -------------------------------------------------------------------------------- /test/graticule-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | array = require("d3-array"), 3 | d3 = require("../"); 4 | 5 | require("./inDelta"); 6 | 7 | tape("graticule.extent(…) sets extentMinor and extentMajor", function(test) { 8 | var g = d3.geoGraticule().extent([[-90, -45], [90, 45]]); 9 | test.deepEqual(g.extentMinor(), [[-90, -45], [90, 45]]); 10 | test.deepEqual(g.extentMajor(), [[-90, -45], [90, 45]]); 11 | test.end(); 12 | }); 13 | 14 | tape("graticule.extent() gets extentMinor", function(test) { 15 | var g = d3.geoGraticule().extentMinor([[-90, -45], [90, 45]]); 16 | test.deepEqual(g.extent(), [[-90, -45], [90, 45]]); 17 | test.end(); 18 | }); 19 | 20 | tape("graticule.extentMajor() default longitude ranges from 180°W (inclusive) to 180°E (exclusive)", function(test) { 21 | var extent = d3.geoGraticule().extentMajor(); 22 | test.equal(extent[0][0], -180); 23 | test.equal(extent[1][0], +180); 24 | test.end(); 25 | }); 26 | 27 | tape("graticule.extentMajor() default latitude ranges from 90°S (exclusive) to 90°N (exclusive)", function(test) { 28 | var extent = d3.geoGraticule().extentMajor(); 29 | test.equal(extent[0][1], -90 + 1e-6); 30 | test.equal(extent[1][1], +90 - 1e-6); 31 | test.end(); 32 | }); 33 | 34 | tape("graticule.extentMajor(…) coerces input values to numbers", function(test) { 35 | var g = d3.geoGraticule().extentMajor([["-90", "-45"], ["+90", "+45"]]), 36 | extent = g.extentMajor(); 37 | test.strictEqual(extent[0][0], -90); 38 | test.strictEqual(extent[0][1], -45); 39 | test.strictEqual(extent[1][0], +90); 40 | test.strictEqual(extent[1][1], +45); 41 | test.end(); 42 | }); 43 | 44 | tape("graticule.extentMinor() default longitude ranges from 180°W (inclusive) to 180°E (exclusive)", function(test) { 45 | var extent = d3.geoGraticule().extentMinor(); 46 | test.equal(extent[0][0], -180); 47 | test.equal(extent[1][0], +180); 48 | test.end(); 49 | }); 50 | 51 | tape("graticule.extentMinor() default latitude ranges from 80°S (inclusive) to 80°N (inclusive)", function(test) { 52 | var extent = d3.geoGraticule().extentMinor(); 53 | test.equal(extent[0][1], -80 - 1e-6); 54 | test.equal(extent[1][1], +80 + 1e-6); 55 | test.end(); 56 | }); 57 | 58 | tape("graticule.extentMinor(…) coerces input values to numbers", function(test) { 59 | var g = d3.geoGraticule().extentMinor([["-90", "-45"], ["+90", "+45"]]), 60 | extent = g.extentMinor(); 61 | test.strictEqual(extent[0][0], -90); 62 | test.strictEqual(extent[0][1], -45); 63 | test.strictEqual(extent[1][0], +90); 64 | test.strictEqual(extent[1][1], +45); 65 | test.end(); 66 | }); 67 | 68 | tape("graticule.step(…) sets the minor and major step", function(test) { 69 | var g = d3.geoGraticule().step([22.5, 22.5]); 70 | test.deepEqual(g.stepMinor(), [22.5, 22.5]); 71 | test.deepEqual(g.stepMajor(), [22.5, 22.5]); 72 | test.end(); 73 | }); 74 | 75 | tape("graticule.step() gets the minor step", function(test) { 76 | var g = d3.geoGraticule().stepMinor([22.5, 22.5]); 77 | test.deepEqual(g.step(), [22.5, 22.5]); 78 | test.end(); 79 | }); 80 | 81 | tape("graticule.stepMinor() defaults to 10°, 10°", function(test) { 82 | test.deepEqual(d3.geoGraticule().stepMinor(), [10, 10]); 83 | test.end(); 84 | }); 85 | 86 | tape("graticule.stepMinor(…) coerces input values to numbers", function(test) { 87 | var g = d3.geoGraticule().stepMinor(["45", "11.25"]), 88 | step = g.stepMinor(); 89 | test.strictEqual(step[0], 45); 90 | test.strictEqual(step[1], 11.25); 91 | test.end(); 92 | }); 93 | 94 | tape("graticule.stepMajor() defaults to 90°, 360°", function(test) { 95 | test.deepEqual(d3.geoGraticule().stepMajor(), [90, 360]); 96 | test.end(); 97 | }); 98 | 99 | tape("graticule.stepMajor(…) coerces input values to numbers", function(test) { 100 | var g = d3.geoGraticule().stepMajor(["45", "11.25"]), 101 | step = g.stepMajor(); 102 | test.strictEqual(step[0], 45); 103 | test.strictEqual(step[1], 11.25); 104 | test.end(); 105 | }); 106 | 107 | tape("graticule.lines() default longitude ranges from 180°W (inclusive) to 180°E (exclusive)", function(test) { 108 | var lines = d3.geoGraticule().lines() 109 | .filter(function(line) { return line.coordinates[0][0] === line.coordinates[1][0]; }) 110 | .sort(function(a, b) { return a.coordinates[0][0] - b.coordinates[0][0]; }); 111 | test.equal(lines[0].coordinates[0][0], -180); 112 | test.equal(lines[lines.length - 1].coordinates[0][0], +170); 113 | test.end(); 114 | }); 115 | 116 | tape("graticule.lines() default latitude ranges from 90°S (exclusive) to 90°N (exclusive)", function(test) { 117 | var lines = d3.geoGraticule().lines() 118 | .filter(function(line) { return line.coordinates[0][1] === line.coordinates[1][1]; }) 119 | .sort(function(a, b) { return a.coordinates[0][1] - b.coordinates[0][1]; }); 120 | test.equal(lines[0].coordinates[0][1], -80); 121 | test.equal(lines[lines.length - 1].coordinates[0][1], +80); 122 | test.end(); 123 | }); 124 | 125 | tape("graticule.lines() default minor longitude lines extend from 80°S to 80°N", function(test) { 126 | var lines = d3.geoGraticule().lines() 127 | .filter(function(line) { return line.coordinates[0][0] === line.coordinates[1][0]; }) 128 | .filter(function(line) { return Math.abs(line.coordinates[0][0] % 90) > 1e-6; }); 129 | lines.forEach(function(line) { 130 | test.deepEqual(array.extent(line.coordinates, function(p) { return p[1]; }), [-80 - 1e-6, +80 + 1e-6]); 131 | }); 132 | test.end(); 133 | }); 134 | 135 | tape("graticule.lines() default major longitude lines extend from 90°S to 90°N", function(test) { 136 | var lines = d3.geoGraticule().lines() 137 | .filter(function(line) { return line.coordinates[0][0] === line.coordinates[1][0]; }) 138 | .filter(function(line) { return Math.abs(line.coordinates[0][0] % 90) < 1e-6; }); 139 | lines.forEach(function(line) { 140 | test.deepEqual(array.extent(line.coordinates, function(p) { return p[1]; }), [-90 + 1e-6, +90 - 1e-6]); 141 | }); 142 | test.end(); 143 | }); 144 | 145 | tape("graticule.lines() default latitude lines extend from 180°W to 180°E", function(test) { 146 | var lines = d3.geoGraticule().lines() 147 | .filter(function(line) { return line.coordinates[0][1] === line.coordinates[1][1]; }); 148 | lines.forEach(function(line) { 149 | test.deepEqual(array.extent(line.coordinates, function(p) { return p[0]; }), [-180, +180]); 150 | }); 151 | test.end(); 152 | }); 153 | 154 | tape("graticule.lines() returns an array of LineStrings", function(test) { 155 | test.deepEqual(d3.geoGraticule() 156 | .extent([[-90, -45], [90, 45]]) 157 | .step([45, 45]) 158 | .precision(3) 159 | .lines(), [ 160 | {type: "LineString", coordinates: [[-90,-45],[-90,45]]}, // meridian 161 | {type: "LineString", coordinates: [[-45,-45],[-45,45]]}, // meridian 162 | {type: "LineString", coordinates: [[0,-45],[0,45]]}, // meridian 163 | {type: "LineString", coordinates: [[45,-45],[45,45]]}, // meridian 164 | {type: "LineString", coordinates: [[-90,-45],[-87,-45],[-84,-45],[-81,-45],[-78,-45],[-75,-45],[-72,-45],[-69,-45],[-66,-45],[-63,-45],[-60,-45],[-57,-45],[-54,-45],[-51,-45],[-48,-45],[-45,-45],[-42,-45],[-39,-45],[-36,-45],[-33,-45],[-30,-45],[-27,-45],[-24,-45],[-21,-45],[-18,-45],[-15,-45],[-12,-45],[-9,-45],[-6,-45],[-3,-45],[0,-45],[3,-45],[6,-45],[9,-45],[12,-45],[15,-45],[18,-45],[21,-45],[24,-45],[27,-45],[30,-45],[33,-45],[36,-45],[39,-45],[42,-45],[45,-45],[48,-45],[51,-45],[54,-45],[57,-45],[60,-45],[63,-45],[66,-45],[69,-45],[72,-45],[75,-45],[78,-45],[81,-45],[84,-45],[87,-45],[90,-45]]}, 165 | {type: "LineString", coordinates: [[-90,0],[-87,0],[-84,0],[-81,0],[-78,0],[-75,0],[-72,0],[-69,0],[-66,0],[-63,0],[-60,0],[-57,0],[-54,0],[-51,0],[-48,0],[-45,0],[-42,0],[-39,0],[-36,0],[-33,0],[-30,0],[-27,0],[-24,0],[-21,0],[-18,0],[-15,0],[-12,0],[-9,0],[-6,0],[-3,0],[0,0],[3,0],[6,0],[9,0],[12,0],[15,0],[18,0],[21,0],[24,0],[27,0],[30,0],[33,0],[36,0],[39,0],[42,0],[45,0],[48,0],[51,0],[54,0],[57,0],[60,0],[63,0],[66,0],[69,0],[72,0],[75,0],[78,0],[81,0],[84,0],[87,0],[90,0]]} 166 | ]); 167 | test.end(); 168 | }); 169 | 170 | tape("graticule() returns a MultiLineString of all lines", function(test) { 171 | var g = d3.geoGraticule() 172 | .extent([[-90, -45], [90, 45]]) 173 | .step([45, 45]) 174 | .precision(3); 175 | test.deepEqual(g(), { 176 | type: "MultiLineString", 177 | coordinates: g.lines().map(function(line) { return line.coordinates; }) 178 | }); 179 | test.end(); 180 | }); 181 | 182 | tape("graticule.outline() returns a Polygon encompassing the major extent", function(test) { 183 | test.deepEqual(d3.geoGraticule() 184 | .extentMajor([[-90, -45], [90, 45]]) 185 | .precision(3) 186 | .outline(), { 187 | type: "Polygon", 188 | coordinates: [[ 189 | [-90,-45],[-90,45], // meridian 190 | [-87,45],[-84,45],[-81,45],[-78,45],[-75,45],[-72,45],[-69,45],[-66,45],[-63,45],[-60,45],[-57,45],[-54,45],[-51,45],[-48,45],[-45,45],[-42,45],[-39,45],[-36,45],[-33,45],[-30,45],[-27,45],[-24,45],[-21,45],[-18,45],[-15,45],[-12,45],[-9,45],[-6,45],[-3,45],[0,45],[3,45],[6,45],[9,45],[12,45],[15,45],[18,45],[21,45],[24,45],[27,45],[30,45],[33,45],[36,45],[39,45],[42,45],[45,45],[48,45],[51,45],[54,45],[57,45],[60,45],[63,45],[66,45],[69,45],[72,45],[75,45],[78,45],[81,45],[84,45],[87,45], 191 | [90,45],[90,-45], // meridian 192 | [87,-45],[84,-45],[81,-45],[78,-45],[75,-45],[72,-45],[69,-45],[66,-45],[63,-45],[60,-45],[57,-45],[54,-45],[51,-45],[48,-45],[45,-45],[42,-45],[39,-45],[36,-45],[33,-45],[30,-45],[27,-45],[24,-45],[21,-45],[18,-45],[15,-45],[12,-45],[9,-45],[6,-45],[3,-45],[0,-45],[-3,-45],[-6,-45],[-9,-45],[-12,-45],[-15,-45],[-18,-45],[-21,-45],[-24,-45],[-27,-45],[-30,-45],[-33,-45],[-36,-45],[-39,-45],[-42,-45],[-45,-45],[-48,-45],[-51,-45],[-54,-45],[-57,-45],[-60,-45],[-63,-45],[-66,-45],[-69,-45],[-72,-45],[-75,-45],[-78,-45],[-81,-45],[-84,-45],[-87,-45],[-90,-45] 193 | ]] 194 | }); 195 | test.end(); 196 | }); 197 | --------------------------------------------------------------------------------