Clip lines with Cohen-Sutherland algorithm

This yields a 10x paint performance increase at
#map=18.88/38.85208/-76.72632, as measured with
Chrome Canary's "Continuous Page Repainting" mode.

Fixes #885.
This commit is contained in:
John Firebaugh
2013-03-03 18:21:21 -08:00
parent f757bc965f
commit 60a290282c
8 changed files with 203 additions and 9 deletions
+1
View File
@@ -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 \
+1
View File
@@ -26,6 +26,7 @@
<script src='js/lib/d3.size.js'></script>
<script src='js/lib/d3.trigger.js'></script>
<script src='js/lib/d3.keybinding.js'></script>
<script src='js/lib/d3.clip.js'></script>
<script src='js/lib/d3-compat.js'></script>
<script src='js/lib/bootstrap-tooltip.js'></script>
<script src='js/lib/rtree.js'></script>
+1 -1
View File
@@ -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);
+14 -6
View File
@@ -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(''));
};
},
+6 -2
View File
@@ -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'),
+137
View File
@@ -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;
};
+2
View File
@@ -19,6 +19,7 @@
<script src='../js/lib/lodash.js'></script>
<script src='../js/lib/d3.v3.js'></script>
<script src='../js/lib/sha.js'></script>
<script src='../js/lib/d3.clip.js'></script>
<script src='../js/lib/d3.combobox.js'></script>
<script src='../js/lib/d3.geo.tile.js'></script>
<script src='../js/lib/d3.keybinding.js'></script>
@@ -168,6 +169,7 @@
<script src="spec/spec_helpers.js"></script>
<!-- include spec files here... -->
<script src="spec/lib/d3.clip.js"></script>
<script src="spec/lib/d3.keybinding.js"></script>
<script src="spec/lib/locale.js"></script>
+41
View File
@@ -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]]]);
});
});