Commit 9eb878ec authored by Jason Davies's avatar Jason Davies
Browse files

Fix clipping bug for complex polygons.

Intersection points are sorted along the clipping region edge relative
to a particular point.  In the case of antimeridian clipping, this point
is the South pole.  Previously, the first intersection was assumed to be
an entering intersection, but this is not always the case.

The fix is to see whether the clip region start point (the point
relative to which sorting occurs) is inside or outside the polygon being
drawn.  If it is inside, then the first intersection point must be
outside, and so forth.

The same applies to d3.geo.clipExtent.  Provision was already made for
this, but this has now been optimised to use a single point instead of
picking one of the four corners.

Another optimisation was to reuse this clip region start point to
determine whether to interpolate all the way around the clip region
edge.  Previously, this was done by testing an arbitrary point in the
clip region.

The above fix seemed to have broken one of the tests, and this has been
fixed by modifying the point-in-polygon routine slightly to handle
points that might lie exactly on the polygon edge.

Finally, I noticed a regression with the recent clipExtent fix, where a
polygon incorrectly being marked as “clean” (no intersections) on a
per-ring instead of per-polygon basis.
parent a76aed58
......@@ -2611,7 +2611,7 @@ d3 = function() {
function d3_true() {
return true;
}
function d3_geo_clipPolygon(segments, compare, inside, interpolate, listener) {
function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) {
var subject = [], clip = [];
segments.forEach(function(segment) {
if ((n = segment.length - 1) <= 0) return;
......@@ -2664,8 +2664,8 @@ d3 = function() {
d3_geo_clipPolygonLinkCircular(subject);
d3_geo_clipPolygonLinkCircular(clip);
if (!subject.length) return;
if (inside) for (var i = 1, e = !inside(clip[0].point), n = clip.length; i < n; ++i) {
clip[i].entry = e = !e;
for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) {
clip[i].entry = entry = !entry;
}
var start = subject[0], current, points, point;
while (1) {
......@@ -2708,9 +2708,9 @@ d3 = function() {
a.next = b = array[0];
b.prev = a;
}
function d3_geo_clip(pointVisible, clipLine, interpolate, clipPoint) {
function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {
return function(rotate, listener) {
var line = clipLine(listener), rotatedClipPoint = rotate.invert(clipPoint[0], clipPoint[1]);
var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]);
var clip = {
point: point,
lineStart: lineStart,
......@@ -2728,9 +2728,10 @@ d3 = function() {
clip.lineStart = lineStart;
clip.lineEnd = lineEnd;
segments = d3.merge(segments);
var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon);
if (segments.length) {
d3_geo_clipPolygon(segments, d3_geo_clipSort, null, interpolate, listener);
} else if (d3_geo_pointInPolygon(rotatedClipPoint, polygon)) {
d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener);
} else if (clipStartInside) {
listener.lineStart();
interpolate(null, null, 1, listener);
listener.lineEnd();
......@@ -2841,7 +2842,7 @@ d3 = function() {
var intersection = d3_geo_cartesianCross(meridianNormal, arc);
d3_geo_cartesianNormalize(intersection);
var φarc = (antimeridian ^ >= 0 ? -1 : 1) * d3_asin(intersection[2]);
if (parallel > φarc) {
if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) {
winding += antimeridian ^ >= 0 ? 1 : -1;
}
}
......@@ -2851,7 +2852,7 @@ d3 = function() {
}
return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1;
}
var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, 0 ]);
var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]);
function d3_geo_clipAntimeridianLine(listener) {
var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean;
return {
......@@ -2920,7 +2921,7 @@ d3 = function() {
}
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, [ radius, 0 ]);
return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]);
function visible(λ, φ) {
return Math.cos(λ) * Math.cos(φ) > cr;
}
......@@ -3043,11 +3044,12 @@ d3 = function() {
listener = bufferListener;
segments = [];
polygon = [];
clean = true;
},
polygonEnd: function() {
listener = listener_;
segments = d3.merge(segments);
var inside = clean && insidePolygon([ x0, y0 ]), visible = segments.length;
var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length;
if (inside || visible) {
listener.polygonStart();
if (inside) {
......@@ -3056,17 +3058,13 @@ d3 = function() {
listener.lineEnd();
}
if (visible) {
d3_geo_clipPolygon(segments, compare, pointInside, interpolate, listener);
d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener);
}
listener.polygonEnd();
}
segments = polygon = ring = null;
}
};
function pointInside(point) {
var a = corner(point, -1), i = insidePolygon([ a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0 ]);
return i;
}
function insidePolygon(p) {
var wn = 0, n = polygon.length, y = p[1];
for (var i = 0; i < n; ++i) {
......@@ -3105,7 +3103,7 @@ d3 = function() {
function lineStart() {
clip.point = linePoint;
if (polygon) polygon.push(ring = []);
first = clean = true;
first = true;
v_ = false;
x_ = y_ = NaN;
}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,7 +7,7 @@ var d3_geo_clipAntimeridian = d3_geo_clip(
d3_true,
d3_geo_clipAntimeridianLine,
d3_geo_clipAntimeridianInterpolate,
[-π, 0]);
[-π, -π / 2]);
// Takes a line and cuts into visible segments. Return values:
// 0: there were intersections or the line was empty.
......
......@@ -12,7 +12,7 @@ function d3_geo_clipCircle(radius) {
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, [radius, 0]);
return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [0, -radius] : [-π, radius - π]);
function visible(λ, φ) {
return Math.cos(λ) * Math.cos(φ) > cr;
......
......@@ -43,11 +43,13 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
listener = bufferListener;
segments = [];
polygon = [];
clean = true;
},
polygonEnd: function() {
listener = listener_;
segments = d3.merge(segments);
var inside = clean && insidePolygon([x0, y0]),
var clipStartInside = insidePolygon([x0, y1]),
inside = clean && clipStartInside,
visible = segments.length;
if (inside || visible) {
listener.polygonStart();
......@@ -57,7 +59,7 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
listener.lineEnd();
}
if (visible) {
d3_geo_clipPolygon(segments, compare, pointInside, interpolate, listener);
d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener);
}
listener.polygonEnd();
}
......@@ -65,12 +67,6 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
}
};
function pointInside(point) {
var a = corner(point, -1),
i = insidePolygon([a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0]);
return i;
}
function insidePolygon(p) {
var wn = 0, // the winding number counter
n = polygon.length,
......@@ -123,7 +119,7 @@ function d3_geo_clipExtent(x0, y0, x1, y1) {
function lineStart() {
clip.point = linePoint;
if (polygon) polygon.push(ring = []);
first = clean = true;
first = true;
v_ = false;
x_ = y_ = NaN;
}
......
......@@ -4,7 +4,7 @@ import "spherical";
// General spherical polygon clipping algorithm: takes a polygon, cuts it into
// visible line segments and rejoins the segments by interpolating along the
// clip edge.
function d3_geo_clipPolygon(segments, compare, inside, interpolate, listener) {
function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) {
var subject = [],
clip = [];
......@@ -39,8 +39,8 @@ function d3_geo_clipPolygon(segments, compare, inside, interpolate, listener) {
d3_geo_clipPolygonLinkCircular(clip);
if (!subject.length) return;
if (inside) for (var i = 1, e = !inside(clip[0].point), n = clip.length; i < n; ++i) {
clip[i].entry = (e = !e);
for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) {
clip[i].entry = entry = !entry;
}
var start = subject[0],
......
......@@ -3,10 +3,10 @@ import "../core/noop";
import "../math/trigonometry";
import "clip-polygon";
function d3_geo_clip(pointVisible, clipLine, interpolate, clipPoint) {
function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {
return function(rotate, listener) {
var line = clipLine(listener),
rotatedClipPoint = rotate.invert(clipPoint[0], clipPoint[1]);
rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]);
var clip = {
point: point,
......@@ -26,9 +26,10 @@ function d3_geo_clip(pointVisible, clipLine, interpolate, clipPoint) {
clip.lineEnd = lineEnd;
segments = d3.merge(segments);
var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon);
if (segments.length) {
d3_geo_clipPolygon(segments, d3_geo_clipSort, null, interpolate, listener);
} else if (d3_geo_pointInPolygon(rotatedClipPoint, polygon)) {
d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener);
} else if (clipStartInside) {
listener.lineStart();
interpolate(null, null, 1, listener);
listener.lineEnd();
......
......@@ -44,7 +44,7 @@ function d3_geo_pointInPolygon(point, polygon) {
var intersection = d3_geo_cartesianCross(meridianNormal, arc);
d3_geo_cartesianNormalize(intersection);
var φarc = (antimeridian ^ >= 0 ? -1 : 1) * d3_asin(intersection[2]);
if (parallel > φarc) {
if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) {
winding += antimeridian ^ >= 0 ? 1 : -1;
}
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment