From 4a9fb163f0e9d509645aff0ceb5b716c0a595896 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 12 Mar 2017 16:32:57 -0700 Subject: [PATCH] Fix rotated Mercator automatic clip extent. Fixes #89. Also fixes automatic clip extent for transverse Mercator, which was likewise busted when rotated, but also busted when intersecting with a user- specified clip extent. --- src/projection/mercator.js | 18 +++---- test/projection/mercator-test.js | 49 +++++++++++++------ test/projection/transverseMercator-test.js | 56 ++++++++++++++++++++++ 3 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 test/projection/transverseMercator-test.js diff --git a/src/projection/mercator.js b/src/projection/mercator.js index d32eb02..7908dd6 100644 --- a/src/projection/mercator.js +++ b/src/projection/mercator.js @@ -1,5 +1,6 @@ -import projection from "./index"; import {atan, exp, halfPi, log, pi, tan, tau} from "../math"; +import rotation from "../rotation"; +import projection from "./index"; export function mercatorRaw(lambda, phi) { return [lambda, log(tan((halfPi + phi) / 2))]; @@ -22,10 +23,6 @@ export function mercatorProjection(project) { clipExtent = m.clipExtent, x0 = null, y0, x1, y1; // clip extent - m.center = function(_) { - return arguments.length ? (center(_), reclip()) : center(); - }; - m.scale = function(_) { return arguments.length ? (scale(_), reclip()) : scale(); }; @@ -34,16 +31,21 @@ export function mercatorProjection(project) { return arguments.length ? (translate(_), reclip()) : translate(); }; + m.center = function(_) { + return arguments.length ? (center(_), reclip()) : center(); + }; + m.clipExtent = function(_) { 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]]; }; function reclip() { var k = pi * scale(), - t = m([0, 0]); + t = m(rotation(m.rotate()).invert([0, 0])); return clipExtent(x0 == null - ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] - : [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]]); + ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] : project === mercatorRaw + ? [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]] + : [[x0, Math.max(t[1] - k, y0)], [x1, Math.min(t[1] + k, y1)]]); } return reclip(); diff --git a/test/projection/mercator-test.js b/test/projection/mercator-test.js index ba98b8f..f81c0cf 100644 --- a/test/projection/mercator-test.js +++ b/test/projection/mercator-test.js @@ -4,36 +4,55 @@ var tape = require("tape"), require("../pathEqual"); tape("mercator.clipExtent(null) sets the default automatic clip extent", function(test) { - var mercator = d3.geoMercator().translate([0, 0]).scale(1).clipExtent(null).precision(0); - test.pathEqual(d3.geoPath(mercator)({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"); - test.equal(mercator.clipExtent(), null); + var projection = d3.geoMercator().translate([0, 0]).scale(1).clipExtent(null).precision(0); + 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"); + test.equal(projection.clipExtent(), null); test.end(); }); tape("mercator.center(center) sets the correct automatic clip extent", function(test) { - var mercator = d3.geoMercator().translate([0, 0]).scale(1).center([10, 10]).precision(0); - test.pathEqual(d3.geoPath(mercator)({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"); - test.equal(mercator.clipExtent(), null); + var projection = d3.geoMercator().translate([0, 0]).scale(1).center([10, 10]).precision(0); + 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"); + test.equal(projection.clipExtent(), null); test.end(); }); tape("mercator.clipExtent(extent) intersects the specified clip extent with the automatic clip extent", function(test) { - var mercator = d3.geoMercator().translate([0, 0]).scale(1).clipExtent([[-10, -10], [10, 10]]).precision(0); - test.pathEqual(d3.geoPath(mercator)({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"); - test.deepEqual(mercator.clipExtent(), [[-10, -10], [10, 10]]); + var projection = d3.geoMercator().translate([0, 0]).scale(1).clipExtent([[-10, -10], [10, 10]]).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); test.end(); }); tape("mercator.clipExtent(extent).scale(scale) updates the intersected clip extent", function(test) { - var mercator = d3.geoMercator().translate([0, 0]).clipExtent([[-10, -10], [10, 10]]).scale(1).precision(0); - test.pathEqual(d3.geoPath(mercator)({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"); - test.deepEqual(mercator.clipExtent(), [[-10, -10], [10, 10]]); + var projection = d3.geoMercator().translate([0, 0]).clipExtent([[-10, -10], [10, 10]]).scale(1).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); test.end(); }); tape("mercator.clipExtent(extent).translate(translate) updates the intersected clip extent", function(test) { - var mercator = d3.geoMercator().scale(1).clipExtent([[-10, -10], [10, 10]]).translate([0, 0]).precision(0); - test.pathEqual(d3.geoPath(mercator)({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"); - test.deepEqual(mercator.clipExtent(), [[-10, -10], [10, 10]]); + var projection = d3.geoMercator().scale(1).clipExtent([[-10, -10], [10, 10]]).translate([0, 0]).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); + test.end(); +}); + +tape("mercator.rotate(…) does not affect the automatic clip extent", function(test) { + var projection = d3.geoMercator(), object = { + type: "MultiPoint", + coordinates: [ + [-82.35024908550241, 29.649391549778745], + [-82.35014449996858, 29.65075946917633], + [-82.34916073446641, 29.65070265688781], + [-82.3492653331286, 29.64933474064504] + ] + }; + projection.fitExtent([[0, 0], [960, 600]], object); + test.deepEqual(projection.scale(), 20969742.365692537); + test.deepEqual(projection.translate(), [30139734.76760269, 11371473.949706702]); + projection.rotate([0, 95]).fitExtent([[0, 0], [960, 600]], object); + test.deepEqual(projection.scale(), 35781690.650920525); + test.deepEqual(projection.translate(), [75115911.95344563, 2586046.4116968135]); test.end(); }); diff --git a/test/projection/transverseMercator-test.js b/test/projection/transverseMercator-test.js new file mode 100644 index 0000000..60013b3 --- /dev/null +++ b/test/projection/transverseMercator-test.js @@ -0,0 +1,56 @@ +var tape = require("tape"), + d3 = require("../../"); + +tape("transverseMercator.clipExtent(null) sets the default automatic clip extent", function(test) { + var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).clipExtent(null).precision(0); + 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"); + test.equal(projection.clipExtent(), null); + test.end(); +}); + +tape("transverseMercator.center(center) sets the correct automatic clip extent", function(test) { + var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).center([10, 10]).precision(0); + 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"); + test.equal(projection.clipExtent(), null); + test.end(); +}); + +tape("transverseMercator.clipExtent(extent) intersects the specified clip extent with the automatic clip extent", function(test) { + var projection = d3.geoTransverseMercator().translate([0, 0]).scale(1).clipExtent([[-10, -10], [10, 10]]).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); + test.end(); +}); + +tape("transverseMercator.clipExtent(extent).scale(scale) updates the intersected clip extent", function(test) { + var projection = d3.geoTransverseMercator().translate([0, 0]).clipExtent([[-10, -10], [10, 10]]).scale(1).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); + test.end(); +}); + +tape("transverseMercator.clipExtent(extent).translate(translate) updates the intersected clip extent", function(test) { + var projection = d3.geoTransverseMercator().scale(1).clipExtent([[-10, -10], [10, 10]]).translate([0, 0]).precision(0); + 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"); + test.deepEqual(projection.clipExtent(), [[-10, -10], [10, 10]]); + test.end(); +}); + +tape("transverseMercator.rotate(…) does not affect the automatic clip extent", function(test) { + var projection = d3.geoTransverseMercator(), object = { + type: "MultiPoint", + coordinates: [ + [-82.35024908550241, 29.649391549778745], + [-82.35014449996858, 29.65075946917633], + [-82.34916073446641, 29.65070265688781], + [-82.3492653331286, 29.64933474064504] + ] + }; + projection.fitExtent([[0, 0], [960, 600]], object); + test.deepEqual(projection.scale(), 15724992.330511674); + test.deepEqual(projection.translate(), [20418843.897824813, 21088401.790971387]); + projection.rotate([0, 95]).fitExtent([[0, 0], [960, 600]], object); + test.deepEqual(projection.scale(), 15724992.330511674); + test.deepEqual(projection.translate(), [20418843.897824813, 47161426.43770847]); + test.end(); +});