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]]]); + }); +});