Skip to content

Commit

Permalink
Fix rotated Mercator automatic clip extent.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mbostock committed Mar 13, 2017
1 parent a515530 commit 4a9fb16
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 23 deletions.
18 changes: 10 additions & 8 deletions src/projection/mercator.js
Original file line number Diff line number Diff line change
@@ -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))];
Expand All @@ -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();
};
Expand All @@ -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();
Expand Down
49 changes: 34 additions & 15 deletions test/projection/mercator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
56 changes: 56 additions & 0 deletions test/projection/transverseMercator-test.js
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit 4a9fb16

Please sign in to comment.