diff --git a/Makefile b/Makefile
index 85de984aa..b91af8ade 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,7 @@ all: \
.INTERMEDIATE iD.js: \
js/lib/bootstrap-tooltip.js \
js/lib/d3.v3.js \
+ js/lib/d3.clip.js \
js/lib/d3.combobox.js \
js/lib/d3.geo.tile.js \
js/lib/d3.keybinding.js \
diff --git a/index.html b/index.html
index 86350b43a..43de200cc 100644
--- a/index.html
+++ b/index.html
@@ -26,6 +26,7 @@
+
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index fc55ff10d..cbfc2b844 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -89,7 +89,7 @@ iD.Map = function(context) {
surface
.call(points, graph, all, filter)
.call(vertices, graph, all, filter)
- .call(lines, graph, all, filter)
+ .call(lines, graph, all, filter, dimensions)
.call(areas, graph, all, filter)
.call(midpoints, graph, all, filter, extent)
.call(labels, graph, all, filter, dimensions, !difference);
diff --git a/js/id/svg.js b/js/id/svg.js
index 36d1d060d..114907876 100644
--- a/js/id/svg.js
+++ b/js/id/svg.js
@@ -13,22 +13,30 @@ iD.svg = {
};
},
- LineString: function(projection, graph) {
+ LineString: function(projection, graph, dimensions) {
var cache = {};
return function(entity) {
if (cache[entity.id] !== undefined) {
return cache[entity.id];
}
- if (entity.nodes.length === 0) {
+ var clip = d3.clip.cohenSutherland()
+ .bounds([0, 0, dimensions[0], dimensions[1]]);
+
+ var segments = clip(graph.childNodes(entity).map(function(n) {
+ return projection(n.loc);
+ }));
+
+ if (segments.length === 0) {
return (cache[entity.id] = null);
}
return (cache[entity.id] =
- 'M' + graph.childNodes(entity).map(function(n) {
- var pt = projection(n.loc);
- return pt[0] + ',' + pt[1];
- }).join('L'));
+ segments.map(function(points) {
+ return 'M' + points.map(function(p) {
+ return p[0] + ',' + p[1];
+ }).join('L');
+ }).join(''));
};
},
diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js
index a94ad3a25..81d9346f9 100644
--- a/js/id/svg/lines.js
+++ b/js/id/svg/lines.js
@@ -33,8 +33,12 @@ iD.svg.Lines = function(projection) {
return as - bs;
}
- return function drawLines(surface, graph, entities, filter) {
+ return function drawLines(surface, graph, entities, filter, dimensions) {
function drawPaths(group, lines, filter, klass, lineString) {
+ lines = lines.filter(function(line) {
+ return lineString(line);
+ });
+
var tagClasses = iD.svg.TagClasses();
if (klass === 'stroke') {
@@ -82,7 +86,7 @@ iD.svg.Lines = function(projection) {
lines.sort(waystack);
- var lineString = iD.svg.LineString(projection, graph);
+ var lineString = iD.svg.LineString(projection, graph, dimensions);
var shadow = surface.select('.layer-shadow'),
casing = surface.select('.layer-casing'),
diff --git a/js/lib/d3.clip.js b/js/lib/d3.clip.js
new file mode 100644
index 000000000..4f67dcca7
--- /dev/null
+++ b/js/lib/d3.clip.js
@@ -0,0 +1,137 @@
+d3.clip = {};
+
+d3.clip.cohenSutherland = function() {
+ var xmin = 0, xmax = 0, ymin = 0, ymax = 0;
+
+ var x = function(d) {
+ return d[0];
+ };
+
+ var y = function(d) {
+ return d[1];
+ };
+
+ var INSIDE = 0; // 0000
+ var LEFT = 1; // 0001
+ var RIGHT = 2; // 0010
+ var BOTTOM = 4; // 0100
+ var TOP = 8; // 1000
+
+ function outCode(x, y) {
+ var code = INSIDE;
+
+ if (x < xmin)
+ code |= LEFT;
+ else if (x > xmax)
+ code |= RIGHT;
+
+ if (y < ymin)
+ code |= BOTTOM;
+ else if (y > ymax)
+ code |= TOP;
+
+ return code;
+ }
+
+ function clip(data) {
+ var segments = [],
+ points = [],
+ i = 0,
+ n = data.length,
+ x0, y0, x1, y1, c0, c1, _x0, _y0, _x1, _y1, _c0, _c1,
+ fx = d3.functor(x),
+ fy = d3.functor(y);
+
+ function segment() {
+ segments.push(points);
+ points = [];
+ }
+
+ if (n) {
+ x0 = +fx.call(this, data[0], 0);
+ y0 = +fy.call(this, data[0], 0);
+ c0 = outCode(x0, y0);
+ if (!c0) points.push([x0, y0]);
+ }
+
+ while (++i < n) {
+ x1 = +fx.call(this, data[i], i);
+ y1 = +fy.call(this, data[i], i);
+ c1 = outCode(x1, y1);
+
+ _x0 = x0;
+ _y0 = y0;
+ _x1 = x1;
+ _y1 = y1;
+ _c0 = c0;
+ _c1 = c1;
+
+ while (true) {
+ if (!(_c0 | _c1)) {
+ if (c0) points.push([_x0, _y0]);
+ points.push([_x1, _y1]);
+ if (c1) segment();
+ break;
+ } else if (_c0 & _c1) {
+ break;
+ } else {
+ var _x, _y, outcodeOut = _c0 ? _c0 : _c1;
+
+ if (outcodeOut & TOP) {
+ _x = _x0 + (_x1 - _x0) * (ymax - _y0) / (_y1 - _y0);
+ _y = ymax;
+ } else if (outcodeOut & BOTTOM) {
+ _x = _x0 + (_x1 - _x0) * (ymin - _y0) / (_y1 - _y0);
+ _y = ymin;
+ } else if (outcodeOut & RIGHT) {
+ _y = _y0 + (_y1 - _y0) * (xmax - _x0) / (_x1 - _x0);
+ _x = xmax;
+ } else if (outcodeOut & LEFT) {
+ _y = _y0 + (_y1 - _y0) * (xmin - _x0) / (_x1 - _x0);
+ _x = xmin;
+ }
+
+ if (outcodeOut == _c0) {
+ _x0 = _x;
+ _y0 = _y;
+ _c0 = outCode(_x0, _y0);
+ } else {
+ _x1 = _x;
+ _y1 = _y;
+ _c1 = outCode(_x1, _y1);
+ }
+ }
+ }
+
+ x0 = x1;
+ y0 = y1;
+ c0 = c1;
+ }
+
+ if (points.length) segment();
+ return segments;
+ }
+
+ clip.x = function(_) {
+ if (!arguments.length) return x;
+ x = _;
+ return clip;
+ };
+
+ clip.y = function(_) {
+ if (!arguments.length) return y;
+ y = _;
+ return clip;
+ };
+
+ clip.bounds = function(_) {
+ if (!arguments.length) return [xmin, ymin, xmax, ymax];
+ xmin = _[0];
+ ymin = _[1];
+ xmax = _[2];
+ ymax = _[3];
+ return clip;
+ };
+
+ return clip;
+};
diff --git a/test/index.html b/test/index.html
index eb6be5487..4663282dc 100644
--- a/test/index.html
+++ b/test/index.html
@@ -19,6 +19,7 @@
+
@@ -168,6 +169,7 @@
+
diff --git a/test/spec/lib/d3.clip.js b/test/spec/lib/d3.clip.js
new file mode 100644
index 000000000..99205da97
--- /dev/null
+++ b/test/spec/lib/d3.clip.js
@@ -0,0 +1,41 @@
+describe('d3.clip.cohenSutherland', function() {
+ var clip;
+
+ beforeEach(function() {
+ clip = d3.clip.cohenSutherland()
+ .bounds([0, 0, 10, 10]);
+ });
+
+ it('clips an empty array', function() {
+ expect(clip([])).to.eql([]);
+ });
+
+ it('clips a point inside bounds', function() {
+ expect(clip([[0, 0]])).to.eql([[[0, 0]]]);
+ });
+
+ it('clips a point outside bounds', function() {
+ expect(clip([[-1, -1]])).to.eql([]);
+ });
+
+ it('clips a single segment inside bounds', function() {
+ expect(clip([[0, 0], [10, 10]])).to.eql([[[0, 0], [10, 10]]]);
+ });
+
+ it('clips a single segment leaving bounds', function() {
+ expect(clip([[5, 5], [15, 15]])).to.eql([[[5, 5], [10, 10]]]);
+ });
+
+ it('clips a single segment entering bounds', function() {
+ expect(clip([[15, 15], [5, 5]])).to.eql([[[10, 10], [5, 5]]]);
+ });
+
+ it('clips a single segment entering and leaving bounds', function() {
+ expect(clip([[0, 15], [15, 0]])).to.eql([[[5, 10], [10, 5]]]);
+ });
+
+ it('clips multiple segments', function() {
+ expect(clip([[15, 15], [5, 5], [15, 15], [5, 5]])).to.
+ eql([[[10, 10], [5, 5], [10, 10]], [[10, 10], [5, 5]]]);
+ });
+});