Skip to content

Commit 6f7fd2c

Browse files
committed
[web] Optimize drawing of styled layers
1 parent a58eaa9 commit 6f7fd2c

File tree

3 files changed

+119
-71
lines changed

3 files changed

+119
-71
lines changed

src/gui/mapshaper-canvas.js

Lines changed: 84 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,58 @@ function DisplayCanvas() {
2323
_ext = extent;
2424
};
2525

26+
/*
27+
// Original function, not optimized
2628
_self.drawPathShapes = function(shapes, arcs, style) {
27-
var start = getPathStart(style, _ext),
28-
draw = getShapePencil(arcs, _ext),
29-
end = getPathEnd(style);
29+
var startPath = getPathStart(_ext),
30+
drawPath = getShapePencil(arcs, _ext),
31+
styler = style.styler || null;
3032
for (var i=0, n=shapes.length; i<n; i++) {
31-
start(_ctx, i);
32-
draw(shapes[i], _ctx);
33-
end(_ctx);
33+
if (styler) styler(style, i);
34+
startPath(_ctx, style);
35+
drawPath(shapes[i], _ctx);
36+
endPath(_ctx, style);
3437
}
3538
};
39+
*/
40+
41+
// Optimized to draw paths in same-style batches (faster Canvas drawing)
42+
_self.drawPathShapes = function(shapes, arcs, style) {
43+
var styleIndex = {};
44+
var batchSize = 1500;
45+
var startPath = getPathStart(_ext);
46+
var drawPath = getShapePencil(arcs, _ext);
47+
var key, item;
48+
var styler = style.styler || null;
49+
for (var i=0; i<shapes.length; i++) {
50+
if (styler) styler(style, i);
51+
key = getStyleKey(style);
52+
if (key in styleIndex === false) {
53+
styleIndex[key] = {
54+
style: utils.defaults({}, style),
55+
shapes: []
56+
};
57+
}
58+
item = styleIndex[key];
59+
item.shapes.push(shapes[i]);
60+
if (item.shapes.length >= batchSize) {
61+
drawPaths(item.shapes, startPath, drawPath, item.style);
62+
item.shapes = [];
63+
}
64+
}
65+
Object.keys(styleIndex).forEach(function(key) {
66+
var item = styleIndex[key];
67+
drawPaths(item.shapes, startPath, drawPath, item.style);
68+
});
69+
};
70+
71+
function drawPaths(shapes, startPath, drawPath, style) {
72+
startPath(_ctx, style);
73+
for (var i=0, n=shapes.length; i<n; i++) {
74+
drawPath(shapes[i], _ctx);
75+
}
76+
endPath(_ctx, style);
77+
}
3678

3779
_self.drawSquareDots = function(shapes, style) {
3880
var t = getScaledTransform(_ext),
@@ -59,69 +101,50 @@ function DisplayCanvas() {
59101
}
60102
};
61103

104+
// TODO: consider using drawPathShapes(), which draws paths in batches
105+
// for faster Canvas rendering. Downside: changes stacking order, which
106+
// is bad if circles are graduated.
62107
_self.drawPoints = function(shapes, style) {
63108
var t = getScaledTransform(_ext),
64109
pixRatio = gui.getPixelRatio(),
65-
start = getPathStart(style, _ext),
66-
end = getPathEnd(style),
110+
startPath = getPathStart(_ext),
111+
styler = style.styler || null,
67112
shp, p;
68113

69114
for (var i=0, n=shapes.length; i<n; i++) {
70115
shp = shapes[i];
71-
start(_ctx, i);
116+
if (styler) styler(style, i);
117+
startPath(_ctx, style);
72118
if (!shp || style.radius > 0 === false) continue;
73119
for (var j=0, m=shp ? shp.length : 0; j<m; j++) {
74120
p = shp[j];
75121
drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, style.radius * pixRatio, _ctx);
76122
}
77-
end(_ctx);
123+
endPath(_ctx, style);
78124
}
79125
};
80126

81-
_self.drawArcs = function(arcs, flags, style) {
82-
var darkStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[1]},
83-
lightStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[0]};
84-
setArcVisibility(flags, arcs);
85-
// TODO: don't show light arcs if reference layer is visible
86-
if (lightStyle.strokeColor) {
87-
drawFlaggedArcs(2, flags, lightStyle, arcs);
88-
}
89-
drawFlaggedArcs(3, flags, darkStyle, arcs);
90-
};
91-
92-
function setArcVisibility(flags, arcs) {
93-
var minPathLen = 0.5 * _ext.getPixelSize(),
94-
geoBounds = _ext.getBounds(),
95-
geoBBox = geoBounds.toArray(),
96-
allIn = geoBounds.contains(arcs.getBounds()),
97-
visible;
98-
// don't continue dropping paths if user zooms out farther than full extent
99-
if (_ext.scale() < 1) minPathLen *= _ext.scale();
100-
for (var i=0, n=arcs.size(); i<n; i++) {
101-
visible = !arcs.arcIsSmaller(i, minPathLen) && (allIn ||
102-
arcs.arcIntersectsBBox(i, geoBBox));
103-
// mark visible arcs by setting second flag bit to 1
104-
flags[i] = (flags[i] & 1) | (visible ? 2 : 0);
105-
}
106-
}
107-
108-
function drawFlaggedArcs(flag, flags, style, arcs) {
109-
var start = getPathStart(style, _ext),
110-
end = getPathEnd(style),
127+
_self.drawArcs = function(arcs, style, filter) {
128+
var startPath = getPathStart(_ext),
111129
t = getScaledTransform(_ext),
112130
ctx = _ctx,
113131
n = 25, // render paths in batches of this size (an optimization)
114132
count = 0;
115-
start(ctx);
133+
startPath(ctx, style);
116134
for (i=0, n=arcs.size(); i<n; i++) {
117-
if (flags[i] != flag) continue;
135+
if (filter && !filter(i)) continue;
118136
if (++count % n === 0) {
119-
end(ctx);
120-
start(ctx);
137+
endPath(ctx, style);
138+
startPath(ctx, style);
121139
}
122-
drawPath(arcs.getArcIter(i), t, ctx);
140+
drawPath(arcs.getArcIter(i), t, ctx, 0.6);
123141
}
124-
end(ctx);
142+
endPath(ctx, style);
143+
};
144+
145+
function getStyleKey(style) {
146+
return (style.strokeWidth > 0 ? style.strokeColor + '~' + style.strokeWidth +
147+
'~' : '') + (style.fillColor || '') + (style.opacity < 1 ? '~' + style.opacity : '');
125148
}
126149

127150
return _self;
@@ -147,10 +170,10 @@ function drawSquare(x, y, size, ctx) {
147170
}
148171
}
149172

150-
function drawPath(vec, t, ctx) {
151-
var minLen = gui.getPixelRatio() > 1 ? 1 : 0.6,
152-
x, y, xp, yp;
173+
function drawPath(vec, t, ctx, minLen) {
174+
var x, y, xp, yp;
153175
if (!vec.hasNext()) return;
176+
minLen = minLen >= 0 ? minLen : 0.4;
154177
x = xp = vec.x * t.mx + t.bx;
155178
y = yp = vec.y * t.my + t.by;
156179
ctx.moveTo(x, y);
@@ -163,19 +186,16 @@ function drawPath(vec, t, ctx) {
163186
yp = y;
164187
}
165188
}
166-
if (x != xp || y != yp) {
167-
ctx.lineTo(x, y);
168-
}
169189
}
170190

171191
function getShapePencil(arcs, ext) {
172192
var t = getScaledTransform(ext);
193+
var iter = new internal.ShapeIter(arcs);
173194
return function(shp, ctx) {
174-
var iter = new internal.ShapeIter(arcs);
175-
if (!shp) return;
176-
for (var i=0; i<shp.length; i++) {
195+
for (var i=0, n=shp ? shp.length : 0; i<n; i++) {
177196
iter.init(shp[i]);
178-
drawPath(iter, t, ctx);
197+
// 0.2 trades visible seams for performance
198+
drawPath(iter, t, ctx, 0.2);
179199
}
180200
};
181201
}
@@ -198,17 +218,13 @@ function getDotScale(ext) {
198218
return Math.pow(getLineScale(ext), 0.6);
199219
}
200220

201-
function getPathStart(style, ext) {
202-
var styler = style.styler || null,
203-
pixRatio = gui.getPixelRatio(),
221+
function getPathStart(ext) {
222+
var pixRatio = gui.getPixelRatio(),
204223
lineScale = getLineScale(ext);
205224

206-
return function(ctx, i) {
225+
return function(ctx, style) {
207226
var strokeWidth;
208227
ctx.beginPath();
209-
if (styler) {
210-
styler(style, i);
211-
}
212228
if (style.opacity >= 0) {
213229
ctx.globalAlpha = style.opacity;
214230
}
@@ -229,11 +245,9 @@ function getPathStart(style, ext) {
229245
};
230246
}
231247

232-
function getPathEnd(style) {
233-
return function(ctx) {
234-
if (style.fillColor) ctx.fill();
235-
if (style.strokeWidth > 0) ctx.stroke();
236-
if (style.opacity >= 0) ctx.globalAlpha = 1;
237-
ctx.closePath();
238-
};
248+
function endPath(ctx, style) {
249+
if (style.fillColor) ctx.fill();
250+
if (style.strokeWidth > 0) ctx.stroke();
251+
if (style.opacity >= 0) ctx.globalAlpha = 1;
252+
ctx.closePath();
239253
}

src/gui/mapshaper-maplayer.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,27 @@ function DisplayLayer(lyr, dataset, ext) {
4444
this.drawStructure = function(canv, style) {
4545
var obj = this.getDisplayLayer(ext);
4646
var arcs = obj.dataset.arcs;
47+
var darkStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[1]},
48+
lightStyle = {strokeWidth: style.strokeWidth, strokeColor: style.strokeColors[0]};
49+
var filter;
50+
4751
if (arcs && _arcFlags) {
48-
canv.drawArcs(arcs, _arcFlags, style);
52+
if (lightStyle.strokeColor) {
53+
filter = getArcFilter(arcs, ext, _arcFlags, 0);
54+
canv.drawArcs(arcs, lightStyle, filter);
55+
}
56+
if (darkStyle.strokeColor) {
57+
filter = getArcFilter(arcs, ext, _arcFlags, 1);
58+
canv.drawArcs(arcs, darkStyle, filter);
59+
}
4960
}
5061
if (obj.layer.geometry_type == 'point') {
5162
canv.drawSquareDots(obj.layer.shapes, style);
5263
}
5364
};
5465

5566
this.drawShapes = function(canv, style) {
67+
// TODO: add filter for out-of-view shapes
5668
var obj = this.getDisplayLayer(ext);
5769
var lyr = style.ids ? filterLayer(obj.layer, style.ids) : obj.layer;
5870
if (lyr.geometry_type == 'point') {
@@ -66,6 +78,27 @@ function DisplayLayer(lyr, dataset, ext) {
6678
}
6779
};
6880

81+
function getArcFilter(arcs, ext, flags, flag) {
82+
var minPathLen = 0.5 * ext.getPixelSize(),
83+
geoBounds = ext.getBounds(),
84+
geoBBox = geoBounds.toArray(),
85+
allIn = geoBounds.contains(arcs.getBounds()),
86+
visible;
87+
// don't continue dropping paths if user zooms out farther than full extent
88+
if (ext.scale() < 1) minPathLen *= ext.scale();
89+
return function(i) {
90+
var visible = true;
91+
if (flags[i] != flag) {
92+
visible = false;
93+
} else if (arcs.arcIsSmaller(i, minPathLen)) {
94+
visible = false;
95+
} else if (!allIn && !arcs.arcIntersectsBBox(i, geoBBox)) {
96+
visible = false;
97+
}
98+
return visible;
99+
};
100+
}
101+
69102
function filterLayer(lyr, ids) {
70103
if (lyr.shapes) {
71104
shapes = ids.map(function(id) {

src/mapshaper-common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var internal = {
77
QUIET: false,
88
TRACING: false,
99
VERBOSE: false,
10+
T: T,
1011
defs: {}
1112
};
1213

0 commit comments

Comments
 (0)