Fix small-circle clipping of lines.
This corrects the handling of lines that are long enough to have two visible or invisible endpoints, but still cross the small circle and thus have an invisible or visible intermediate segment. Fixes #1127.
This commit is contained in:
Родитель
6b04c1f76f
Коммит
d93f45ee82
|
@ -2245,16 +2245,16 @@ d3 = function() {
|
|||
};
|
||||
return circle.angle(90);
|
||||
};
|
||||
function d3_geo_circleInterpolate(radians, precision) {
|
||||
var cr = Math.cos(radians), sr = Math.sin(radians);
|
||||
function d3_geo_circleInterpolate(radius, precision) {
|
||||
var cr = Math.cos(radius), sr = Math.sin(radius);
|
||||
return function(from, to, direction, listener) {
|
||||
if (from != null) {
|
||||
from = d3_geo_circleAngle(cr, from);
|
||||
to = d3_geo_circleAngle(cr, to);
|
||||
if (direction > 0 ? from < to : from > to) from += direction * 2 * π;
|
||||
} else {
|
||||
from = radians + direction * 2 * π;
|
||||
to = radians;
|
||||
from = radius + direction * 2 * π;
|
||||
to = radius;
|
||||
}
|
||||
var point;
|
||||
for (var step = direction * precision, t = from; direction > 0 ? t > to : t < to; t -= step) {
|
||||
|
@ -2737,21 +2737,21 @@ d3 = function() {
|
|||
listener.point(to[0], to[1]);
|
||||
}
|
||||
}
|
||||
function d3_geo_clipCircle(degrees) {
|
||||
var radians = degrees * d3_radians, cr = Math.cos(radians), interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians);
|
||||
function d3_geo_clipCircle(radius) {
|
||||
var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = Math.abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians);
|
||||
return d3_geo_clip(visible, clipLine, interpolate);
|
||||
function visible(λ, φ) {
|
||||
return Math.cos(λ) * Math.cos(φ) > cr;
|
||||
}
|
||||
function clipLine(listener) {
|
||||
var point0, v0, v00, clean;
|
||||
var point0, c0, v0, v00, clean;
|
||||
return {
|
||||
lineStart: function() {
|
||||
v00 = v0 = false;
|
||||
clean = 1;
|
||||
},
|
||||
point: function(λ, φ) {
|
||||
var point1 = [ λ, φ ], point2, v = visible(λ, φ);
|
||||
var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0;
|
||||
if (!point0 && (v00 = v0 = v)) listener.lineStart();
|
||||
if (v !== v0) {
|
||||
point2 = intersect(point0, point1);
|
||||
|
@ -2763,7 +2763,7 @@ d3 = function() {
|
|||
}
|
||||
if (v !== v0) {
|
||||
clean = 0;
|
||||
if (v0 = v) {
|
||||
if (v) {
|
||||
listener.lineStart();
|
||||
point2 = intersect(point1, point0);
|
||||
listener.point(point2[0], point2[1]);
|
||||
|
@ -2773,9 +2773,27 @@ d3 = function() {
|
|||
listener.lineEnd();
|
||||
}
|
||||
point0 = point2;
|
||||
} else if (notHemisphere && point0 && smallRadius ^ v) {
|
||||
var t;
|
||||
if (!(c & c0) && (t = intersect(point1, point0, true))) {
|
||||
clean = 0;
|
||||
if (smallRadius) {
|
||||
listener.lineStart();
|
||||
listener.point(t[0][0], t[0][1]);
|
||||
listener.point(t[1][0], t[1][1]);
|
||||
listener.lineEnd();
|
||||
} else {
|
||||
listener.point(t[1][0], t[1][1]);
|
||||
listener.lineEnd();
|
||||
listener.lineStart();
|
||||
listener.point(t[0][0], t[0][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) listener.point(point1[0], point1[1]);
|
||||
point0 = point1;
|
||||
if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) {
|
||||
listener.point(point1[0], point1[1]);
|
||||
}
|
||||
point0 = point1, v0 = v, c0 = c;
|
||||
},
|
||||
lineEnd: function() {
|
||||
if (v0) listener.lineEnd();
|
||||
|
@ -2786,15 +2804,33 @@ d3 = function() {
|
|||
}
|
||||
};
|
||||
}
|
||||
function intersect(a, b) {
|
||||
function intersect(a, b, two) {
|
||||
var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b);
|
||||
var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2;
|
||||
if (!determinant) return a;
|
||||
if (!determinant) return !two && a;
|
||||
var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2);
|
||||
d3_geo_cartesianAdd(A, B);
|
||||
var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t = Math.sqrt(w * w - uu * (d3_geo_cartesianDot(A, A) - 1)), q = d3_geo_cartesianScale(u, (-w - t) / uu);
|
||||
var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1);
|
||||
if (t2 < 0) return;
|
||||
var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu);
|
||||
d3_geo_cartesianAdd(q, A);
|
||||
return d3_geo_spherical(q);
|
||||
q = d3_geo_spherical(q);
|
||||
if (!two) return q;
|
||||
var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z;
|
||||
if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z;
|
||||
var δλ = λ1 - λ0, polar = Math.abs(δλ - π) < ε, meridian = polar || δλ < ε;
|
||||
if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z;
|
||||
if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (Math.abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) {
|
||||
var q1 = d3_geo_cartesianScale(u, (-w + t) / uu);
|
||||
d3_geo_cartesianAdd(q1, A);
|
||||
return [ q, d3_geo_spherical(q1) ];
|
||||
}
|
||||
}
|
||||
function code(λ, φ) {
|
||||
var r = smallRadius ? radius : π - radius, code = 0;
|
||||
if (λ < -r) code |= 1; else if (λ > r) code |= 2;
|
||||
if (φ < -r) code |= 4; else if (φ > r) code |= 8;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
function d3_geo_clipView(x0, y0, x1, y1) {
|
||||
|
@ -3046,7 +3082,7 @@ d3 = function() {
|
|||
};
|
||||
projection.clipAngle = function(_) {
|
||||
if (!arguments.length) return clipAngle;
|
||||
preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle(clipAngle = +_);
|
||||
preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians);
|
||||
return projection;
|
||||
};
|
||||
projection.clipExtent = function(_) {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -48,17 +48,17 @@ d3.geo.circle = function() {
|
|||
|
||||
// Interpolates along a circle centered at [0°, 0°], with a given radius and
|
||||
// precision.
|
||||
function d3_geo_circleInterpolate(radians, precision) {
|
||||
var cr = Math.cos(radians),
|
||||
sr = Math.sin(radians);
|
||||
function d3_geo_circleInterpolate(radius, precision) {
|
||||
var cr = Math.cos(radius),
|
||||
sr = Math.sin(radius);
|
||||
return function(from, to, direction, listener) {
|
||||
if (from != null) {
|
||||
from = d3_geo_circleAngle(cr, from);
|
||||
to = d3_geo_circleAngle(cr, to);
|
||||
if (direction > 0 ? from < to: from > to) from += direction * 2 * π;
|
||||
} else {
|
||||
from = radians + direction * 2 * π;
|
||||
to = radians;
|
||||
from = radius + direction * 2 * π;
|
||||
to = radius;
|
||||
}
|
||||
var point;
|
||||
for (var step = direction * precision, t = from; direction > 0 ? t > to : t < to; t -= step) {
|
||||
|
|
|
@ -4,11 +4,12 @@ import "clip";
|
|||
import "circle";
|
||||
import "spherical";
|
||||
|
||||
// Clip features against a circle centered at [0°, 0°], with a given radius.
|
||||
function d3_geo_clipCircle(degrees) {
|
||||
var radians = degrees * d3_radians,
|
||||
cr = Math.cos(radians),
|
||||
interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians);
|
||||
// Clip features against a small circle centered at [0°, 0°].
|
||||
function d3_geo_clipCircle(radius) {
|
||||
var cr = Math.cos(radius),
|
||||
smallRadius = cr > 0,
|
||||
notHemisphere = Math.abs(cr) > ε, // TODO optimise for this common case
|
||||
interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians);
|
||||
|
||||
return d3_geo_clip(visible, clipLine, interpolate);
|
||||
|
||||
|
@ -16,7 +17,6 @@ function d3_geo_clipCircle(degrees) {
|
|||
return Math.cos(λ) * Math.cos(φ) > cr;
|
||||
}
|
||||
|
||||
// TODO handle two invisible endpoints with visible intermediate segment.
|
||||
// Takes a line and cuts into visible segments. Return values used for
|
||||
// polygon clipping:
|
||||
// 0: there were intersections or the line was empty.
|
||||
|
@ -24,9 +24,10 @@ function d3_geo_clipCircle(degrees) {
|
|||
// 2: there were intersections, and the first and last segments should be
|
||||
// rejoined.
|
||||
function clipLine(listener) {
|
||||
var point0,
|
||||
v0,
|
||||
v00,
|
||||
var point0, // previous point
|
||||
c0, // code for previous point
|
||||
v0, // visibility of previous point
|
||||
v00, // visibility of first point
|
||||
clean; // no intersections
|
||||
return {
|
||||
lineStart: function() {
|
||||
|
@ -36,9 +37,13 @@ function d3_geo_clipCircle(degrees) {
|
|||
point: function(λ, φ) {
|
||||
var point1 = [λ, φ],
|
||||
point2,
|
||||
v = visible(λ, φ);
|
||||
v = visible(λ, φ),
|
||||
c = smallRadius
|
||||
? v ? 0 : code(λ, φ)
|
||||
: v ? code(λ + (λ < 0 ? π : -π), φ) : 0;
|
||||
if (!point0 && (v00 = v0 = v)) listener.lineStart();
|
||||
// handle degeneracies
|
||||
// Handle degeneracies.
|
||||
// TODO ignore if not clipping polygons.
|
||||
if (v !== v0) {
|
||||
point2 = intersect(point0, point1);
|
||||
if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) {
|
||||
|
@ -49,7 +54,7 @@ function d3_geo_clipCircle(degrees) {
|
|||
}
|
||||
if (v !== v0) {
|
||||
clean = 0;
|
||||
if (v0 = v) {
|
||||
if (v) {
|
||||
// outside going in
|
||||
listener.lineStart();
|
||||
point2 = intersect(point1, point0);
|
||||
|
@ -61,9 +66,29 @@ function d3_geo_clipCircle(degrees) {
|
|||
listener.lineEnd();
|
||||
}
|
||||
point0 = point2;
|
||||
} else if (notHemisphere && point0 && smallRadius ^ v) {
|
||||
var t;
|
||||
// If the codes for two points are different, or are both zero,
|
||||
// and there this segment intersects with the small circle.
|
||||
if (!(c & c0) && (t = intersect(point1, point0, true))) {
|
||||
clean = 0;
|
||||
if (smallRadius) {
|
||||
listener.lineStart();
|
||||
listener.point(t[0][0], t[0][1]);
|
||||
listener.point(t[1][0], t[1][1]);
|
||||
listener.lineEnd();
|
||||
} else {
|
||||
listener.point(t[1][0], t[1][1]);
|
||||
listener.lineEnd();
|
||||
listener.lineStart();
|
||||
listener.point(t[0][0], t[0][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) listener.point(point1[0], point1[1]);
|
||||
point0 = point1;
|
||||
if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) {
|
||||
listener.point(point1[0], point1[1]);
|
||||
}
|
||||
point0 = point1, v0 = v, c0 = c;
|
||||
},
|
||||
lineEnd: function() {
|
||||
if (v0) listener.lineEnd();
|
||||
|
@ -76,18 +101,20 @@ function d3_geo_clipCircle(degrees) {
|
|||
}
|
||||
|
||||
// Intersects the great circle between a and b with the clip circle.
|
||||
function intersect(a, b) {
|
||||
function intersect(a, b, two) {
|
||||
var pa = d3_geo_cartesian(a),
|
||||
pb = d3_geo_cartesian(b);
|
||||
|
||||
// We have two planes, n1.p = d1 and n2.p = d2.
|
||||
// Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 x n2).
|
||||
// Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 ⨯ n2).
|
||||
var n1 = [1, 0, 0], // normal
|
||||
n2 = d3_geo_cartesianCross(pa, pb),
|
||||
n2n2 = d3_geo_cartesianDot(n2, n2),
|
||||
n1n2 = n2[0], // d3_geo_cartesianDot(n1, n2),
|
||||
determinant = n2n2 - n1n2 * n1n2;
|
||||
|
||||
// Two polar points.
|
||||
if (!determinant) return a;
|
||||
if (!determinant) return !two && a;
|
||||
|
||||
var c1 = cr * n2n2 / determinant,
|
||||
c2 = -cr * n1n2 / determinant,
|
||||
|
@ -95,13 +122,55 @@ function d3_geo_clipCircle(degrees) {
|
|||
A = d3_geo_cartesianScale(n1, c1),
|
||||
B = d3_geo_cartesianScale(n2, c2);
|
||||
d3_geo_cartesianAdd(A, B);
|
||||
// Now solve |p(t)|^2 = 1.
|
||||
|
||||
// Solve |p(t)|^2 = 1.
|
||||
var u = n1xn2,
|
||||
w = d3_geo_cartesianDot(A, u),
|
||||
uu = d3_geo_cartesianDot(u, u),
|
||||
t = Math.sqrt(w * w - uu * (d3_geo_cartesianDot(A, A) - 1)),
|
||||
t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1);
|
||||
|
||||
if (t2 < 0) return;
|
||||
|
||||
var t = Math.sqrt(t2),
|
||||
q = d3_geo_cartesianScale(u, (-w - t) / uu);
|
||||
d3_geo_cartesianAdd(q, A);
|
||||
return d3_geo_spherical(q);
|
||||
q = d3_geo_spherical(q);
|
||||
if (!two) return q;
|
||||
|
||||
// Two intersection points.
|
||||
var λ0 = a[0],
|
||||
λ1 = b[0],
|
||||
φ0 = a[1],
|
||||
φ1 = b[1],
|
||||
z;
|
||||
if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z;
|
||||
var δλ = λ1 - λ0,
|
||||
polar = Math.abs(δλ - π) < ε,
|
||||
meridian = polar || δλ < ε;
|
||||
|
||||
if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z;
|
||||
|
||||
// Check that the first point is between a and b.
|
||||
if (meridian
|
||||
? polar
|
||||
? φ0 + φ1 > 0 ^ q[1] < (Math.abs(q[0] - λ0) < ε ? φ0 : φ1)
|
||||
: φ0 <= q[1] && q[1] <= φ1
|
||||
: δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) {
|
||||
var q1 = d3_geo_cartesianScale(u, (-w + t) / uu);
|
||||
d3_geo_cartesianAdd(q1, A);
|
||||
return [q, d3_geo_spherical(q1)];
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a 4-bit vector representing the location of a point relative to
|
||||
// the small circle's bounding box.
|
||||
function code(λ, φ) {
|
||||
var r = smallRadius ? radius : π - radius,
|
||||
code = 0;
|
||||
if (λ < -r) code |= 1; // left
|
||||
else if (λ > r) code |= 2; // right
|
||||
if (φ < -r) code |= 4; // below
|
||||
else if (φ > r) code |= 8; // above
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ function d3_geo_projectionMutator(projectAt) {
|
|||
|
||||
projection.clipAngle = function(_) {
|
||||
if (!arguments.length) return clipAngle;
|
||||
preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle(clipAngle = +_);
|
||||
preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians);
|
||||
return projection;
|
||||
};
|
||||
|
||||
|
|
|
@ -589,6 +589,42 @@ suite.addBatch({
|
|||
}
|
||||
}
|
||||
},
|
||||
"clipAngle(30)": {
|
||||
topic: function() {
|
||||
return d3.geo.path()
|
||||
.context(testContext)
|
||||
.projection(d3.geo.equirectangular()
|
||||
.scale(900 / Math.PI)
|
||||
.precision(0)
|
||||
.clipAngle(30));
|
||||
},
|
||||
"clips lines with two invisible endpoints and visible middle": function(path) {
|
||||
path({type: "LineString", coordinates: [[-45, 0], [45, 0]]});
|
||||
assert.deepEqual(testContext.buffer(), [
|
||||
{type: "moveTo", x: 330, y: 250},
|
||||
{type: "lineTo", x: 630, y: 250}
|
||||
]);
|
||||
}
|
||||
},
|
||||
"clipAngle(150)": {
|
||||
topic: function() {
|
||||
return d3.geo.path()
|
||||
.context(testContext)
|
||||
.projection(d3.geo.equirectangular()
|
||||
.scale(900 / Math.PI)
|
||||
.precision(0)
|
||||
.clipAngle(150));
|
||||
},
|
||||
"clips lines with two visible endpoints and invisible middle": function(path) {
|
||||
path({type: "LineString", coordinates: [[135, 0], [-135, 0]]});
|
||||
assert.deepEqual(testContext.buffer(), [
|
||||
{type: "moveTo", x: 1155, y: 250},
|
||||
{type: "lineTo", x: 1230, y: 250},
|
||||
{type: "moveTo", x: -270, y: 250},
|
||||
{type: "lineTo", x: -195, y: 250}
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
"antimeridian cutting": {
|
||||
"rotate([98, 0])": {
|
||||
|
|
Загрузка…
Ссылка в новой задаче