From eed52552a9061148c2f995158e90878761f94d1e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 7 Apr 2016 18:03:57 +0200 Subject: [PATCH 1/3] Batch polygon and circle fills and strokes --- src/ol/color.js | 10 ++ src/ol/render/canvas/replay.js | 59 ++++++- test/spec/ol/color.test.js | 14 ++ test/spec/ol/renderer/canvas/replay.test.js | 148 +++++++++++++++++- .../layer/expected/vector-canvas-opaque.png | Bin 0 -> 2232 bytes test_rendering/spec/ol/layer/vector.test.js | 35 ++++- 6 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png diff --git a/src/ol/color.js b/src/ol/color.js index 338a23446b..e27041a307 100644 --- a/src/ol/color.js +++ b/src/ol/color.js @@ -190,6 +190,16 @@ ol.color.fromStringInternal_ = function(s) { }; +/** + * @param {ol.ColorLike|string} color Color. + * @return {boolean} Is rgba. + */ +ol.color.isRgba = function(color) { + return Array.isArray(color) && color.length == 4 || + typeof color == 'string' && ol.color.rgbaColorRe_.test(color); +}; + + /** * @param {ol.Color} color Color. * @return {boolean} Is valid. diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 1de3481ca2..0891bc12e9 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -71,6 +71,12 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) { */ this.maxExtent = maxExtent; + /** + * @protected + * @type {boolean} + */ + this.transparency = false; + /** * @private * @type {ol.Extent} @@ -257,6 +263,10 @@ ol.render.canvas.Replay.prototype.replay_ = function( var localTransform = this.tmpLocalTransform_; var localTransformInv = this.tmpLocalTransformInv_; var prevX, prevY, roundX, roundY; + var pendingFill = 0; + var pendingStroke = 0; + var batchSize = + this.instructions != instructions || this.transparency ? 0 : 200; while (i < ii) { var instruction = instructions[i]; var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]); @@ -276,7 +286,17 @@ ol.render.canvas.Replay.prototype.replay_ = function( } break; case ol.render.canvas.Instruction.BEGIN_PATH: - context.beginPath(); + if (pendingFill > batchSize) { + context.fill(); + pendingFill = 0; + } + if (pendingStroke > batchSize) { + context.stroke(); + pendingStroke = 0; + } + if (!pendingFill && !pendingStroke) { + context.beginPath(); + } ++i; break; case ol.render.canvas.Instruction.CIRCLE: @@ -290,6 +310,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( var dx = x2 - x1; var dy = y2 - y1; var r = Math.sqrt(dx * dx + dy * dy); + context.moveTo(x2, y2); context.arc(x1, y1, r, 0, 2 * Math.PI, true); ++i; break; @@ -438,7 +459,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ++i; break; case ol.render.canvas.Instruction.FILL: - context.fill(); + if (batchSize) { + pendingFill++; + } else { + context.fill(); + } ++i; break; case ol.render.canvas.Instruction.MOVE_TO_LINE_TO: @@ -475,6 +500,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ol.colorlike.isColorLike(instruction[1]), '2nd instruction should be a string, ' + 'CanvasPattern, or CanvasGradient'); + if (pendingFill) { + context.fill(); + pendingFill = 0; + } + context.fillStyle = /** @type {ol.ColorLike} */ (instruction[1]); ++i; break; @@ -494,6 +524,10 @@ ol.render.canvas.Replay.prototype.replay_ = function( var usePixelRatio = instruction[7] !== undefined ? instruction[7] : true; var lineWidth = /** @type {number} */ (instruction[2]); + if (pendingStroke) { + context.stroke(); + pendingStroke = 0; + } context.strokeStyle = /** @type {string} */ (instruction[1]); context.lineWidth = usePixelRatio ? lineWidth * pixelRatio : lineWidth; context.lineCap = /** @type {string} */ (instruction[3]); @@ -519,7 +553,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ++i; break; case ol.render.canvas.Instruction.STROKE: - context.stroke(); + if (batchSize) { + pendingStroke++; + } else { + context.stroke(); + } ++i; break; default: @@ -528,6 +566,12 @@ ol.render.canvas.Replay.prototype.replay_ = function( break; } } + if (pendingFill) { + context.fill(); + } + if (pendingStroke) { + context.stroke(); + } // assert that all instructions were consumed goog.DEBUG && console.assert(i == instructions.length, 'all instructions should be consumed'); @@ -547,7 +591,7 @@ ol.render.canvas.Replay.prototype.replay = function( context, pixelRatio, transform, viewRotation, skippedFeaturesHash) { var instructions = this.instructions; this.replay_(context, pixelRatio, transform, viewRotation, - skippedFeaturesHash, instructions, undefined); + skippedFeaturesHash, instructions, undefined, undefined); }; @@ -1413,6 +1457,10 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var fillStyleColor = fillStyle.getColor(); state.fillStyle = ol.colorlike.asColorLike(fillStyleColor ? fillStyleColor : ol.render.canvas.defaultFillStyle); + if (!this.transparency && ol.color.isRgba(state.fillStyle)) { + this.transparency = ol.color.asArray( + /** @type {ol.Color|string} */ (state.fillStyle))[0] != 1; + } } else { state.fillStyle = undefined; } @@ -1420,6 +1468,9 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var strokeStyleColor = strokeStyle.getColor(); state.strokeStyle = ol.color.asString(strokeStyleColor ? strokeStyleColor : ol.render.canvas.defaultStrokeStyle); + if (!this.transparency && ol.color.isRgba(state.strokeStyle)) { + this.transparency = ol.color.asArray(state.strokeStyle)[3] != 1; + } var strokeStyleLineCap = strokeStyle.getLineCap(); state.lineCap = strokeStyleLineCap !== undefined ? strokeStyleLineCap : ol.render.canvas.defaultLineCap; diff --git a/test/spec/ol/color.test.js b/test/spec/ol/color.test.js index 0df4432ed0..9c0a4e9af4 100644 --- a/test/spec/ol/color.test.js +++ b/test/spec/ol/color.test.js @@ -103,6 +103,20 @@ describe('ol.color', function() { }); + describe('ol.color.isRgba', function() { + it('identifies rgba arrays', function() { + expect(ol.color.isRgba([255, 255, 255, 1])).to.be(true); + expect(ol.color.isRgba([255, 255, 255])).to.be(false); + }); + it('identifies rgba strings', function() { + expect(ol.color.isRgba('rgba(255,255,255,1)')).to.be(true); + expect(ol.color.isRgba('rgb(255,255,255)')).to.be(false); + expect(ol.color.isRgba('#FFF')).to.be(false); + expect(ol.color.isRgba('#FFFFFF')).to.be(false); + expect(ol.color.isRgba('red')).to.be(false); + }); + }); + describe('ol.color.isValid', function() { it('identifies valid colors', function() { diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js index bace66747d..1f30b88ebc 100644 --- a/test/spec/ol/renderer/canvas/replay.test.js +++ b/test/spec/ol/renderer/canvas/replay.test.js @@ -1,10 +1,138 @@ goog.provide('ol.test.renderer.canvas.Replay'); -goog.require('ol.render.canvas.LineStringReplay'); -goog.require('ol.render.canvas.PolygonReplay'); -goog.require('ol.render.canvas.Replay'); -goog.require('ol.style.Stroke'); +describe('ol.render.canvas.ReplayGroup', function() { + describe('#replay', function() { + + var context, replay, fillCount, strokeCount, beginPathCount; + var feature1, feature2, feature3, style1, style2, style3, transform; + + beforeEach(function() { + transform = goog.vec.Mat4.createNumber(); + replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1); + feature1 = new ol.Feature(new ol.geom.Polygon( + [[[-90, -45], [-90, 0], [0, 0], [0, -45], [-90, -45]]])); + feature2 = new ol.Feature(new ol.geom.Polygon( + [[[90, 45], [90, 0], [0, 0], [0, 45], [90, 45]]])); + feature3 = new ol.Feature(new ol.geom.Polygon( + [[[-90, -45], [-90, 45], [90, 45], [90, -45], [-90, -45]]])); + style1 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'black'}), + stroke: new ol.style.Stroke({color: 'white', width: 1}) + }); + style2 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'white'}), + stroke: new ol.style.Stroke({color: 'black', width: 1}) + }); + style3 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'rgba(255,255,255,0.8)'}), + stroke: new ol.style.Stroke({color: 'rgba(0,0,0,0.8)', width: 1}) + }); + fillCount = 0; + strokeCount = 0; + beginPathCount = 0; + context = { + fill: function() { + fillCount++; + }, + stroke: function() { + strokeCount++; + }, + beginPath: function() { + beginPathCount++; + }, + clip: function() { + beginPathCount--; + }, + save: function() {}, + moveTo: function() {}, + lineTo: function() {}, + closePath: function() {}, + setLineDash: function() {}, + restore: function() {} + } + + }) + + it('batches fill and stroke instructions for same style', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style1, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for different styles', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(2); + expect(strokeCount).to.be(2); + expect(beginPathCount).to.be(2); + }); + + it('batches fill and stroke instructions for changing styles', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style2, 1); + ol.renderer.vector.renderFeature(replay, feature3, style1, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(3); + expect(strokeCount).to.be(3); + expect(beginPathCount).to.be(3); + }); + + it('batches fill and stroke instructions for skipped feature at the beginning', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style2, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature1)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for skipped feature at the end', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature3)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for skipped features', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature1)] = true; + skippedUids[goog.getUid(feature2)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('does not batch when transparent fills/strokes are used', function() { + ol.renderer.vector.renderFeature(replay, feature1, style3, 1); + ol.renderer.vector.renderFeature(replay, feature2, style3, 1); + ol.renderer.vector.renderFeature(replay, feature3, style3, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(3); + expect(strokeCount).to.be(3); + expect(beginPathCount).to.be(3); + }) + }); + +}); describe('ol.render.canvas.Replay', function() { @@ -127,3 +255,15 @@ describe('ol.render.canvas.PolygonReplay', function() { }); }); + +goog.require('goog.vec.Mat4'); +goog.require('ol.Feature'); +goog.require('ol.geom.Polygon'); +goog.require('ol.render.canvas.LineStringReplay'); +goog.require('ol.render.canvas.PolygonReplay'); +goog.require('ol.render.canvas.Replay'); +goog.require('ol.render.canvas.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); diff --git a/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png b/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png new file mode 100644 index 0000000000000000000000000000000000000000..5781ffd7ede52dca51c87e4667cbae9b85fa2008 GIT binary patch literal 2232 zcmV;p2uJscP)^YNm`Q^sGFvO5H2#hdFCVwi!^CnoG)$q@;NUM0`+${G>#R^Zi?gD z=?76s``?q~)L*qm>vgVmnB=_p30=skAphVz6bs^ux_5=k@K~toHWq zgzgUuVt41+XMWn5XXcsNbDV}Mqg3C-BdLD%MB}hH`X(N^3ts2o`G*L+NZ{4RVRbBa z4xWE#4IqJcnG0Pt|L@lMLEtXKYF9CUyPSqfswk@lKzlU+MpS5kn>srCqitA#jp*!) zwqXHo=IC5B+C~7l>H5Y{Od0_1RRdr|Z8(4g-XYR@kMQ&>K0%}@A!5C|BAq$6L?udogF4GgJ4ME1&4gHWRo@Kp!EOKOyikf&VF? zUhn7~-(D#{>keR|FWYRI<_!YRk;iXT!Y+Y#3Ft!vo+t1Z5~?TBmu;?u-MRrNT>UfC z{AQ)AUQLWc;|uZgtY9j0u}01~*B!2cug2!X#E#_2ee7|k{rUv3b% z)wWzs0E$7~Lg4?B%kMd>tM}PD8@pCl?<1GrBk+P4)Ganest$1XnW_5;d`Uq6DUK)` zRf*AT6S@2mf&Xq3SgH8YO+c(E4IHmpo^ zUX-qSq0D{N0cfUH^8`L#%Au5}_T`fUaUUs9?JEf3sis^tfM$9&TdrR*k#Ht% zcm$py@JK0N%s+jBz}+`O~q2)L_$LShQQO5oBrh&cWx7SNafSwT0hlHp1%K`PY98gaSPw$sdZ&EIA zxNPRQkA{7YU&iL806XylOuCs)0xzffCcb6kY*58aRJ;ZzW9X zys%;^Zz4h9<-$IXEXRrD8|Nk6%nt~>l#k9~0qz|d+h*$6VN>^t05r>sbGA6EAT5qL@Vt0(=>v60#Av|40N9UnmR&?Sm8mCL21bd;%_$>~iM&Zoly%unU~ ziPcyEnrloRzh&#_JpjRv5_sNbg)Go!E{8bZksg0I=s9I=xO}wg$ppu79J7JvQ)@>{ zq_ZT{a7*vZ7F%1-p8o6C47{8EoM2dh66!n^SrCgKU|R2?Tt>!f*FsRml5R#)Zh>c# zz|WPlWBR<$2)s~i88r;Rwt>mV3A_@Jn5{}m|8oI-$i~@H73)ZkKWq}D%LLZs@?qiX z=hssDu6~|!LlsTnI|6#Bu-V!^=bn3AK1|?$1lWlmpn(1*f#)l=s3iN{oeUGUF_Y68%#^3MwB6Q!K4tF0Zmx%_3ShpipvzMl|yvDorp zy~)g??g&@^sMLK`1?V_yGm9&&{nHPItW(uwKHr@saJt;R)dvXPO5iNH{8626u$(T{ zbn~h@K*w3z;(N93w)pm}e#LC@owuz^O#qrQt(4o)JyH*EVq!GgB%$7?jj+i)FOk-} z34D#fAJ+0ShxXrJ_=be~3vHy&9M+sqU-%AzFB15QNb5ae%SV-&tIrbnV%xyv<6+C! z9H1$`)kNnz&(U-;+>1CK01MU8JHA~GsCMb9m(1t Date: Mon, 18 Apr 2016 10:41:00 +0200 Subject: [PATCH 2/3] Introduce new overlaps option for Vector and VectorTile sources Instead of deciding whether to batch fills and strokes by looking at the opacity of the style, we now rely on user input. --- examples/osm-vector-tiles.js | 1 + examples/topojson.js | 3 +- externs/olx.js | 24 +++++++++++ src/ol/color.js | 10 ----- src/ol/render/canvas/replay.js | 48 ++++++++++++--------- src/ol/renderer/canvas/vectorlayer.js | 2 +- src/ol/renderer/canvas/vectortilelayer.js | 2 +- src/ol/renderer/dom/vectorlayer.js | 2 +- src/ol/source/imagevector.js | 2 +- src/ol/source/vector.js | 14 ++++++ src/ol/source/vectortile.js | 14 ++++++ test.html | 33 ++++++++++++++ test/spec/ol/color.test.js | 14 ------ test/spec/ol/renderer/canvas/replay.test.js | 21 ++++----- test_rendering/spec/ol/layer/vector.test.js | 3 ++ 15 files changed, 131 insertions(+), 62 deletions(-) create mode 100644 test.html diff --git a/examples/osm-vector-tiles.js b/examples/osm-vector-tiles.js index db47ac2054..ed965ec7c7 100644 --- a/examples/osm-vector-tiles.js +++ b/examples/osm-vector-tiles.js @@ -93,6 +93,7 @@ var map = new ol.Map({ new ol.layer.VectorTile({ source: new ol.source.VectorTile({ format: format, + overlaps: false, tileGrid: tileGrid, url: 'http://{a-c}.tile.openstreetmap.us/' + 'vectiles-land-usages/{z}/{x}/{y}.topojson' diff --git a/examples/topojson.js b/examples/topojson.js index 551636788a..2373203887 100644 --- a/examples/topojson.js +++ b/examples/topojson.js @@ -29,7 +29,8 @@ var style = new ol.style.Style({ var vector = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'data/topojson/world-110m.json', - format: new ol.format.TopoJSON() + format: new ol.format.TopoJSON(), + overlaps: false }), style: function(feature) { // don't want to render the full world polygon, which repeats all countries diff --git a/externs/olx.js b/externs/olx.js index b009fdaaee..5d3c1fea97 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4366,6 +4366,7 @@ olx.source.TileImageOptions.prototype.wrapX; * cacheSize: (number|undefined), * format: (ol.format.Feature|undefined), * logo: (string|olx.LogoOptions|undefined), + * overlaps: (boolean|undefined), * projection: ol.ProjectionLike, * state: (ol.source.State|undefined), * tileClass: (function(new: ol.VectorTile, ol.TileCoord, @@ -4415,6 +4416,17 @@ olx.source.VectorTileOptions.prototype.format; olx.source.VectorTileOptions.prototype.logo; +/** + * This source may have overlapping geometries. Default is `true`. Setting this + * to `false` (e.g. for sources with polygons that represent adminstrative + * boundaries or TopoJSON sources) allows the renderer to optimise fill and + * stroke operations. + * @type {boolean|undefined} + * @api + */ +olx.source.VectorTileOptions.prototype.overlaps; + + /** * Projection. * @type {ol.ProjectionLike} @@ -5797,6 +5809,7 @@ olx.source.TileWMSOptions.prototype.wrapX; * format: (ol.format.Feature|undefined), * loader: (ol.FeatureLoader|undefined), * logo: (string|olx.LogoOptions|undefined), + * overlaps: (boolean|undefined), * strategy: (ol.LoadingStrategy|undefined), * url: (string|ol.FeatureUrlFunction|undefined), * useSpatialIndex: (boolean|undefined), @@ -5849,6 +5862,17 @@ olx.source.VectorOptions.prototype.loader; olx.source.VectorOptions.prototype.logo; +/** + * This source may have overlapping geometries. Default is `true`. Setting this + * to `false` (e.g. for sources with polygons that represent adminstrative + * boundaries or TopoJSON sources) allows the renderer to optimise fill and + * stroke operations. + * @type {boolean|undefined} + * @api + */ +olx.source.VectorOptions.prototype.overlaps; + + /** * The loading strategy to use. By default an {@link ol.loadingstrategy.all} * strategy is used, a one-off strategy which loads all features at once. diff --git a/src/ol/color.js b/src/ol/color.js index e27041a307..338a23446b 100644 --- a/src/ol/color.js +++ b/src/ol/color.js @@ -190,16 +190,6 @@ ol.color.fromStringInternal_ = function(s) { }; -/** - * @param {ol.ColorLike|string} color Color. - * @return {boolean} Is rgba. - */ -ol.color.isRgba = function(color) { - return Array.isArray(color) && color.length == 4 || - typeof color == 'string' && ol.color.rgbaColorRe_.test(color); -}; - - /** * @param {ol.Color} color Color. * @return {boolean} Is valid. diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 0891bc12e9..92b4a758b5 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -52,10 +52,11 @@ ol.render.canvas.Instruction = { * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay can have overlapping geometries. * @protected * @struct */ -ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) { +ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, overlaps) { ol.render.VectorContext.call(this); /** @@ -75,7 +76,7 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) { * @protected * @type {boolean} */ - this.transparency = false; + this.overlaps = overlaps; /** * @private @@ -266,7 +267,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( var pendingFill = 0; var pendingStroke = 0; var batchSize = - this.instructions != instructions || this.transparency ? 0 : 200; + this.instructions != instructions || this.overlaps ? 0 : 200; while (i < ii) { var instruction = instructions[i]; var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]); @@ -691,11 +692,12 @@ ol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() { * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay can have overlapping geometries. * @protected * @struct */ -ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution) { - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution); +ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution, overlaps) { + ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, overlaps); /** * @private @@ -957,12 +959,13 @@ ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) { * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay can have overlapping geometries. * @protected * @struct */ -ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution) { +ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, overlaps) { - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution); + ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, overlaps); /** * @private @@ -1191,12 +1194,13 @@ ol.render.canvas.LineStringReplay.prototype.setFillStrokeStyle = function(fillSt * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay can have overlapping geometries. * @protected * @struct */ -ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution) { +ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution, overlaps) { - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution); + ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, overlaps); /** * @private @@ -1457,10 +1461,6 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var fillStyleColor = fillStyle.getColor(); state.fillStyle = ol.colorlike.asColorLike(fillStyleColor ? fillStyleColor : ol.render.canvas.defaultFillStyle); - if (!this.transparency && ol.color.isRgba(state.fillStyle)) { - this.transparency = ol.color.asArray( - /** @type {ol.Color|string} */ (state.fillStyle))[0] != 1; - } } else { state.fillStyle = undefined; } @@ -1468,9 +1468,6 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var strokeStyleColor = strokeStyle.getColor(); state.strokeStyle = ol.color.asString(strokeStyleColor ? strokeStyleColor : ol.render.canvas.defaultStrokeStyle); - if (!this.transparency && ol.color.isRgba(state.strokeStyle)) { - this.transparency = ol.color.asArray(state.strokeStyle)[3] != 1; - } var strokeStyleLineCap = strokeStyle.getLineCap(); state.lineCap = strokeStyleLineCap !== undefined ? strokeStyleLineCap : ol.render.canvas.defaultLineCap; @@ -1553,12 +1550,13 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyles_ = function() { * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay can have overlapping geometries. * @protected * @struct */ -ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution) { +ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution, overlaps) { - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution); + ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, overlaps); /** * @private @@ -1862,10 +1860,12 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) { * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Max extent. * @param {number} resolution Resolution. + * @param {boolean} overlaps The replay group can have overlapping geometries. * @param {number=} opt_renderBuffer Optional rendering buffer. * @struct */ -ol.render.canvas.ReplayGroup = function(tolerance, maxExtent, resolution, opt_renderBuffer) { +ol.render.canvas.ReplayGroup = function( + tolerance, maxExtent, resolution, overlaps, opt_renderBuffer) { ol.render.ReplayGroup.call(this); /** @@ -1880,6 +1880,12 @@ ol.render.canvas.ReplayGroup = function(tolerance, maxExtent, resolution, opt_re */ this.maxExtent_ = maxExtent; + /** + * @private + * @type {boolean} + */ + this.overlaps_ = overlaps; + /** * @private * @type {number} @@ -1999,7 +2005,7 @@ ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType) replayType + ' constructor missing from ol.render.canvas.BATCH_CONSTRUCTORS_'); replay = new Constructor(this.tolerance_, this.maxExtent_, - this.resolution_); + this.resolution_, this.overlaps_); replays[replayType] = replay; } return replay; @@ -2113,7 +2119,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( * @private * @type {Object.} + * number, boolean)>} */ ol.render.canvas.BATCH_CONSTRUCTORS_ = { 'Image': ol.render.canvas.ImageReplay, diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index f48a23ee2a..e5240cc8d3 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -271,7 +271,7 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, lay var replayGroup = new ol.render.canvas.ReplayGroup( ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, - resolution, vectorLayer.getRenderBuffer()); + resolution, vectorSource.getOverlaps(), vectorLayer.getRenderBuffer()); vectorSource.loadFeatures(extent, resolution, projection); /** * @param {ol.Feature} feature Feature. diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 3f41dc906c..0680020a75 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -207,7 +207,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, } replayState.dirty = false; var replayGroup = new ol.render.canvas.ReplayGroup(0, extent, - tileResolution, layer.getRenderBuffer()); + tileResolution, source.getOverlaps(), layer.getRenderBuffer()); var squaredTolerance = ol.renderer.vector.getSquaredTolerance( tileResolution, pixelRatio); diff --git a/src/ol/renderer/dom/vectorlayer.js b/src/ol/renderer/dom/vectorlayer.js index 47ddc19eb0..ef98e95b2c 100644 --- a/src/ol/renderer/dom/vectorlayer.js +++ b/src/ol/renderer/dom/vectorlayer.js @@ -259,7 +259,7 @@ ol.renderer.dom.VectorLayer.prototype.prepareFrame = function(frameState, layerS var replayGroup = new ol.render.canvas.ReplayGroup( ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, - resolution, vectorLayer.getRenderBuffer()); + resolution, vectorSource.getOverlaps(), vectorLayer.getRenderBuffer()); vectorSource.loadFeatures(extent, resolution, projection); /** * @param {ol.Feature} feature Feature. diff --git a/src/ol/source/imagevector.js b/src/ol/source/imagevector.js index ae5a5f8937..2eac2bccb0 100644 --- a/src/ol/source/imagevector.js +++ b/src/ol/source/imagevector.js @@ -113,7 +113,7 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol var replayGroup = new ol.render.canvas.ReplayGroup( ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, - resolution, this.renderBuffer_); + resolution, this.source_.getOverlaps(), this.renderBuffer_); this.source_.loadFeatures(extent, resolution, projection); diff --git a/src/ol/source/vector.js b/src/ol/source/vector.js index a12c7bb089..4930fe15c6 100644 --- a/src/ol/source/vector.js +++ b/src/ol/source/vector.js @@ -94,6 +94,12 @@ ol.source.Vector = function(opt_options) { */ this.format_ = options.format; + /** + * @private + * @type {boolean} + */ + this.overlaps_ = options.overlaps == undefined ? true : options.overlaps; + /** * @private * @type {string|ol.FeatureUrlFunction|undefined} @@ -695,6 +701,14 @@ ol.source.Vector.prototype.getFormat = function() { }; +/** + * @return {boolean} The source can have overlapping geometries. + */ +ol.source.Vector.prototype.getOverlaps = function() { + return this.overlaps_; +}; + + /** * Get the url associated with this source. * diff --git a/src/ol/source/vectortile.js b/src/ol/source/vectortile.js index 49a34b6ca4..b9e003ddb6 100644 --- a/src/ol/source/vectortile.js +++ b/src/ol/source/vectortile.js @@ -52,6 +52,12 @@ ol.source.VectorTile = function(options) { */ this.format_ = options.format ? options.format : null; + /** + * @private + * @type {boolean} + */ + this.overlaps_ = options.overlaps || true; + /** * @protected * @type {function(new: ol.VectorTile, ol.TileCoord, ol.Tile.State, string, @@ -63,6 +69,14 @@ ol.source.VectorTile = function(options) { ol.inherits(ol.source.VectorTile, ol.source.UrlTile); +/** + * @return {boolean} The source can have overlapping geometries. + */ +ol.source.VectorTile.prototype.getOverlaps = function() { + return this.overlaps_; +}; + + /** * @inheritDoc */ diff --git a/test.html b/test.html new file mode 100644 index 0000000000..ab049dd51a --- /dev/null +++ b/test.html @@ -0,0 +1,33 @@ + + + + + + OpenLayers 3 example + + + +

My Map

+
+ + + + diff --git a/test/spec/ol/color.test.js b/test/spec/ol/color.test.js index 9c0a4e9af4..0df4432ed0 100644 --- a/test/spec/ol/color.test.js +++ b/test/spec/ol/color.test.js @@ -103,20 +103,6 @@ describe('ol.color', function() { }); - describe('ol.color.isRgba', function() { - it('identifies rgba arrays', function() { - expect(ol.color.isRgba([255, 255, 255, 1])).to.be(true); - expect(ol.color.isRgba([255, 255, 255])).to.be(false); - }); - it('identifies rgba strings', function() { - expect(ol.color.isRgba('rgba(255,255,255,1)')).to.be(true); - expect(ol.color.isRgba('rgb(255,255,255)')).to.be(false); - expect(ol.color.isRgba('#FFF')).to.be(false); - expect(ol.color.isRgba('#FFFFFF')).to.be(false); - expect(ol.color.isRgba('red')).to.be(false); - }); - }); - describe('ol.color.isValid', function() { it('identifies valid colors', function() { diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js index 1f30b88ebc..496e8de250 100644 --- a/test/spec/ol/renderer/canvas/replay.test.js +++ b/test/spec/ol/renderer/canvas/replay.test.js @@ -5,11 +5,11 @@ describe('ol.render.canvas.ReplayGroup', function() { describe('#replay', function() { var context, replay, fillCount, strokeCount, beginPathCount; - var feature1, feature2, feature3, style1, style2, style3, transform; + var feature1, feature2, feature3, style1, style2, transform; beforeEach(function() { transform = goog.vec.Mat4.createNumber(); - replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1); + replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1, false); feature1 = new ol.Feature(new ol.geom.Polygon( [[[-90, -45], [-90, 0], [0, 0], [0, -45], [-90, -45]]])); feature2 = new ol.Feature(new ol.geom.Polygon( @@ -24,10 +24,6 @@ describe('ol.render.canvas.ReplayGroup', function() { fill: new ol.style.Fill({color: 'white'}), stroke: new ol.style.Stroke({color: 'black', width: 1}) }); - style3 = new ol.style.Style({ - fill: new ol.style.Fill({color: 'rgba(255,255,255,0.8)'}), - stroke: new ol.style.Stroke({color: 'rgba(0,0,0,0.8)', width: 1}) - }); fillCount = 0; strokeCount = 0; beginPathCount = 0; @@ -121,10 +117,11 @@ describe('ol.render.canvas.ReplayGroup', function() { expect(beginPathCount).to.be(1); }); - it('does not batch when transparent fills/strokes are used', function() { - ol.renderer.vector.renderFeature(replay, feature1, style3, 1); - ol.renderer.vector.renderFeature(replay, feature2, style3, 1); - ol.renderer.vector.renderFeature(replay, feature3, style3, 1); + it('does not batch when overlaps is set to true', function() { + replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1, true); + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style1, 1); replay.replay(context, 1, transform, 0, {}); expect(fillCount).to.be(3); expect(strokeCount).to.be(3); @@ -141,7 +138,7 @@ describe('ol.render.canvas.Replay', function() { it('creates a new replay batch', function() { var tolerance = 10; var extent = [-180, -90, 180, 90]; - var replay = new ol.render.canvas.Replay(tolerance, extent, 1); + var replay = new ol.render.canvas.Replay(tolerance, extent, 1, true); expect(replay).to.be.a(ol.render.canvas.Replay); }); @@ -151,7 +148,7 @@ describe('ol.render.canvas.Replay', function() { var replay; beforeEach(function() { - replay = new ol.render.canvas.Replay(1, [-180, -90, 180, 90], 1); + replay = new ol.render.canvas.Replay(1, [-180, -90, 180, 90], 1, true); }); it('appends coordinates that are within the max extent', function() { diff --git a/test_rendering/spec/ol/layer/vector.test.js b/test_rendering/spec/ol/layer/vector.test.js index 81eddfe38a..8a2fb7224d 100644 --- a/test_rendering/spec/ol/layer/vector.test.js +++ b/test_rendering/spec/ol/layer/vector.test.js @@ -87,6 +87,9 @@ describe('ol.rendering.layer.Vector', function() { it('renders fill/stroke batches correctly with the canvas renderer', function(done) { map = createMap('canvas'); + source = new ol.source.Vector({ + overlaps: false + }); addPolygon(100); addCircle(200); addPolygon(250); From 395793b921a4e50d46a1f8f9fbfe9ed283d0fd99 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 28 Apr 2016 16:29:29 +0200 Subject: [PATCH 3/3] Additional tests and code comments --- externs/olx.js | 4 +- src/ol/render/canvas/replay.js | 4 +- src/ol/source/vectortile.js | 2 +- test.html | 33 ---- test/spec/ol/renderer/canvas/replay.test.js | 40 ++--- .../layer/expected/vector-canvas-stroke.png | Bin 0 -> 504 bytes test_rendering/spec/ol/layer/vector.test.js | 150 ++++++++++++++++-- 7 files changed, 164 insertions(+), 69 deletions(-) delete mode 100644 test.html create mode 100644 test_rendering/spec/ol/layer/expected/vector-canvas-stroke.png diff --git a/externs/olx.js b/externs/olx.js index 5d3c1fea97..d9ed973096 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4418,7 +4418,7 @@ olx.source.VectorTileOptions.prototype.logo; /** * This source may have overlapping geometries. Default is `true`. Setting this - * to `false` (e.g. for sources with polygons that represent adminstrative + * to `false` (e.g. for sources with polygons that represent administrative * boundaries or TopoJSON sources) allows the renderer to optimise fill and * stroke operations. * @type {boolean|undefined} @@ -5864,7 +5864,7 @@ olx.source.VectorOptions.prototype.logo; /** * This source may have overlapping geometries. Default is `true`. Setting this - * to `false` (e.g. for sources with polygons that represent adminstrative + * to `false` (e.g. for sources with polygons that represent administrative * boundaries or TopoJSON sources) allows the renderer to optimise fill and * stroke operations. * @type {boolean|undefined} diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 92b4a758b5..1d859128ed 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -266,6 +266,8 @@ ol.render.canvas.Replay.prototype.replay_ = function( var prevX, prevY, roundX, roundY; var pendingFill = 0; var pendingStroke = 0; + // When the batch size gets too big, performance decreases. 200 is a good + // balance between batch size and number of fill/stroke instructions. var batchSize = this.instructions != instructions || this.overlaps ? 0 : 200; while (i < ii) { @@ -1267,8 +1269,6 @@ ol.render.canvas.PolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCo closePathInstruction); offset = end; } - // FIXME is it quicker to fill and stroke each polygon individually, - // FIXME or all polygons together? var fillInstruction = [ol.render.canvas.Instruction.FILL]; this.hitDetectionInstructions.push(fillInstruction); if (state.fillStyle !== undefined) { diff --git a/src/ol/source/vectortile.js b/src/ol/source/vectortile.js index b9e003ddb6..a8f0b42590 100644 --- a/src/ol/source/vectortile.js +++ b/src/ol/source/vectortile.js @@ -56,7 +56,7 @@ ol.source.VectorTile = function(options) { * @private * @type {boolean} */ - this.overlaps_ = options.overlaps || true; + this.overlaps_ = options.overlaps == undefined ? true : options.overlaps; /** * @protected diff --git a/test.html b/test.html deleted file mode 100644 index ab049dd51a..0000000000 --- a/test.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - OpenLayers 3 example - - - -

My Map

-
- - - - diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js index 496e8de250..1585fed1d5 100644 --- a/test/spec/ol/renderer/canvas/replay.test.js +++ b/test/spec/ol/renderer/canvas/replay.test.js @@ -1,5 +1,17 @@ goog.provide('ol.test.renderer.canvas.Replay'); +goog.require('ol.transform'); +goog.require('ol.Feature'); +goog.require('ol.geom.Polygon'); +goog.require('ol.render.canvas.LineStringReplay'); +goog.require('ol.render.canvas.PolygonReplay'); +goog.require('ol.render.canvas.Replay'); +goog.require('ol.render.canvas.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + describe('ol.render.canvas.ReplayGroup', function() { describe('#replay', function() { @@ -8,7 +20,7 @@ describe('ol.render.canvas.ReplayGroup', function() { var feature1, feature2, feature3, style1, style2, transform; beforeEach(function() { - transform = goog.vec.Mat4.createNumber(); + transform = ol.transform.create(); replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1, false); feature1 = new ol.Feature(new ol.geom.Polygon( [[[-90, -45], [-90, 0], [0, 0], [0, -45], [-90, -45]]])); @@ -46,9 +58,9 @@ describe('ol.render.canvas.ReplayGroup', function() { closePath: function() {}, setLineDash: function() {}, restore: function() {} - } + }; - }) + }); it('batches fill and stroke instructions for same style', function() { ol.renderer.vector.renderFeature(replay, feature1, style1, 1); @@ -85,7 +97,7 @@ describe('ol.render.canvas.ReplayGroup', function() { ol.renderer.vector.renderFeature(replay, feature2, style2, 1); ol.renderer.vector.renderFeature(replay, feature3, style2, 1); var skippedUids = {}; - skippedUids[goog.getUid(feature1)] = true; + skippedUids[ol.getUid(feature1)] = true; replay.replay(context, 1, transform, 0, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); @@ -97,7 +109,7 @@ describe('ol.render.canvas.ReplayGroup', function() { ol.renderer.vector.renderFeature(replay, feature2, style1, 1); ol.renderer.vector.renderFeature(replay, feature3, style2, 1); var skippedUids = {}; - skippedUids[goog.getUid(feature3)] = true; + skippedUids[ol.getUid(feature3)] = true; replay.replay(context, 1, transform, 0, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); @@ -109,8 +121,8 @@ describe('ol.render.canvas.ReplayGroup', function() { ol.renderer.vector.renderFeature(replay, feature2, style1, 1); ol.renderer.vector.renderFeature(replay, feature3, style2, 1); var skippedUids = {}; - skippedUids[goog.getUid(feature1)] = true; - skippedUids[goog.getUid(feature2)] = true; + skippedUids[ol.getUid(feature1)] = true; + skippedUids[ol.getUid(feature2)] = true; replay.replay(context, 1, transform, 0, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); @@ -126,7 +138,7 @@ describe('ol.render.canvas.ReplayGroup', function() { expect(fillCount).to.be(3); expect(strokeCount).to.be(3); expect(beginPathCount).to.be(3); - }) + }); }); }); @@ -252,15 +264,3 @@ describe('ol.render.canvas.PolygonReplay', function() { }); }); - -goog.require('goog.vec.Mat4'); -goog.require('ol.Feature'); -goog.require('ol.geom.Polygon'); -goog.require('ol.render.canvas.LineStringReplay'); -goog.require('ol.render.canvas.PolygonReplay'); -goog.require('ol.render.canvas.Replay'); -goog.require('ol.render.canvas.ReplayGroup'); -goog.require('ol.renderer.vector'); -goog.require('ol.style.Fill'); -goog.require('ol.style.Stroke'); -goog.require('ol.style.Style'); diff --git a/test_rendering/spec/ol/layer/expected/vector-canvas-stroke.png b/test_rendering/spec/ol/layer/expected/vector-canvas-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..9627e9c34fb3af67d9e850d0bb5f83aece242007 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8VBGKN;uumf=k1;I6PpYq+7rE# zoEg0}%NEG$s1`Km7@wZIVQmKcZg$%vi5-pdpWN@2KeJYKo|8Vuug3Mn({{2mP z^qf&&bGFXbcm197m*i~XdvGTI*IBj&5)DZVU5pxL92X+WBYz3!uQa?Cn{wcG^R40_ zvzzmd-S{SBB+2q)L!~;e5#t7Fh+?=dbjDh~3pdpKwZ9u5m$?1p)$3U0gxdLkjs{EL zz5?WEC#6wN;jIkA(P9RX^NayD><%C2)H2OFC3^Auo>QWY#bsO)_5M8fK7Zr8 zvHRUyZYIVbe=HgLBENrQv`D*qoBhFz(tP=b