From 67aa1a6dc91adc43e7a124f0518d054530ccc69b Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Aug 2017 21:06:22 +0200 Subject: [PATCH 1/3] Cache coordinates for reuse on replay, not on geometry --- src/ol/geom/simplegeometry.js | 18 ------------------ src/ol/render/canvas/replay.js | 11 ++++++++++- src/ol/render/feature.js | 18 ------------------ 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/ol/geom/simplegeometry.js b/src/ol/geom/simplegeometry.js index f8821f1192..ecd5626144 100644 --- a/src/ol/geom/simplegeometry.js +++ b/src/ol/geom/simplegeometry.js @@ -41,12 +41,6 @@ ol.geom.SimpleGeometry = function() { */ this.flatCoordinates = null; - /** - * @private - * @type {Array.|Array.>|Array.>>} - */ - this.renderCoordinates_ = null; - }; ol.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry); @@ -147,18 +141,6 @@ ol.geom.SimpleGeometry.prototype.getLayout = function() { }; -/** - * @return {Array.|Array.>|Array.>>} - * Render coordinates. - */ -ol.geom.SimpleGeometry.prototype.getRenderCoordinates = function() { - if (!this.renderCoordinates_) { - this.renderCoordinates_ = []; - } - return this.renderCoordinates_; -}; - - /** * @inheritDoc */ diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 309ce70586..406a77585d 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -88,6 +88,12 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, overlaps) { */ this.coordinates = []; + /** + * @private + * @type {ol.Coordinate|Array.|Array.>} + */ + this.coordinateCache_ = {}; + /** * @private * @type {!ol.Transform} @@ -393,7 +399,10 @@ ol.render.canvas.Replay.prototype.replay_ = function( var coords; if (instruction.length == 6) { var fn = instruction[5]; - coords = fn(pixelCoordinates, d, dd, 2, geometry.getRenderCoordinates()); + if (!(d in this.coordinateCache_)) { + this.coordinateCache_[d] = []; + } + coords = fn(pixelCoordinates, d, dd, 2, this.coordinateCache_[d]); } else { coords = pixelCoordinates.slice(d, dd); } diff --git a/src/ol/render/feature.js b/src/ol/render/feature.js index 8bbec70671..702dbeb2c2 100644 --- a/src/ol/render/feature.js +++ b/src/ol/render/feature.js @@ -43,12 +43,6 @@ ol.render.Feature = function(type, flatCoordinates, ends, properties, id) { */ this.flatCoordinates_ = flatCoordinates; - /** - * @private - * @type {Array.|Array.>|Array.>>} - */ - this.renderCoordinates_ = null; - /** * @private * @type {Array.|Array.>} @@ -117,18 +111,6 @@ ol.render.Feature.prototype.getOrientedFlatCoordinates = function() { }; -/** - * @return {Array.|Array.>|Array.>>} - * Render coordinates. - */ -ol.render.Feature.prototype.getRenderCoordinates = function() { - if (!this.renderCoordinates_) { - this.renderCoordinates_ = []; - } - return this.renderCoordinates_; -}; - - /** * @return {Array.} Flat coordinates. */ From 617151c8ff6483a9da3a86a489600be6aa4beed3 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Aug 2017 22:52:08 +0200 Subject: [PATCH 2/3] Simplify the renderer function API --- examples/street-labels.js | 3 +- examples/vector-label-decluttering.js | 6 +-- externs/olx.js | 2 + src/ol/render/canvas/replay.js | 36 +++++++------- src/ol/typedefs.js | 8 ++- test/spec/ol/renderer/canvas/replay.test.js | 54 +++++++++++++-------- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/examples/street-labels.js b/examples/street-labels.js index 5aa5c71789..4a909ac6d3 100644 --- a/examples/street-labels.js +++ b/examples/street-labels.js @@ -25,7 +25,8 @@ function collectDrawData(letter, x, y, angle) { } var style = new ol.style.Style({ - renderer: function(coords, geometry, feature) { + renderer: function(coords, context) { + var feature = context.feature; var text = feature.get('name'); if (text) { // Only create label when geometry has a long and straight segment diff --git a/examples/vector-label-decluttering.js b/examples/vector-label-decluttering.js index 9a50d04dd9..574fd6db97 100644 --- a/examples/vector-label-decluttering.js +++ b/examples/vector-label-decluttering.js @@ -70,9 +70,9 @@ var styles = [ }) }), new ol.style.Style({ - renderer: function(coord, geometry, feature, state) { + renderer: function(coords, state) { var pixelRatio = state.pixelRatio; - var text = feature.get('name'); + var text = state.feature.get('name'); var canvas = textCache[text]; if (!canvas) { // Draw the label to its own canvas and cache it. @@ -87,7 +87,7 @@ var styles = [ context.fillText(text, 0, 0); } // The 3rd value of the coordinate is the measure of the extent width - var extentWidth = geometry.getCoordinates()[2] / resolution * pixelRatio; + var extentWidth = state.geometry.getCoordinates()[2] / resolution * pixelRatio; if (extentWidth > canvas.width) { // Only consider labels not wider than their country's bounding box createLabel(canvas, text, coord); diff --git a/externs/olx.js b/externs/olx.js index 7f31fbdcea..19adf59a9e 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4402,6 +4402,8 @@ olx.render; /** * @typedef {{context: CanvasRenderingContext2D, + * feature: (ol.Feature|ol.render.Feature), + * geometry: ol.geom.SimpleGeometry, * pixelRatio: number, * resolution: number, * rotation: number}} diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 406a77585d..08713d9595 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -90,7 +90,7 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, overlaps) { /** * @private - * @type {ol.Coordinate|Array.|Array.>} + * @type {Object.|Array.>>} */ this.coordinateCache_ = {}; @@ -326,16 +326,14 @@ ol.render.canvas.Replay.prototype.replay_ = function( var prevX, prevY, roundX, roundY; var pendingFill = 0; var pendingStroke = 0; + var coordinateCache = this.coordinateCache_; - /** - * @type {olx.render.State} - */ - var state = { + var state = /** @type {olx.render.State} */ ({ context: context, pixelRatio: pixelRatio, resolution: this.resolution, rotation: viewRotation - }; + }); // When the batch size gets too big, performance decreases. 200 is a good // balance between batch size and number of fill/stroke instructions. @@ -344,7 +342,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( while (i < ii) { var instruction = instructions[i]; var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]); - var feature, fill, stroke, text, x, y; + var /** @type {ol.Feature|ol.render.Feature} */ feature, fill, stroke, text, x, y; switch (type) { case ol.render.canvas.Instruction.BEGIN_GEOMETRY: feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]); @@ -396,17 +394,21 @@ ol.render.canvas.Replay.prototype.replay_ = function( dd = instruction[2]; var geometry = /** @type {ol.geom.SimpleGeometry} */ (instruction[3]); var renderer = instruction[4]; - var coords; - if (instruction.length == 6) { - var fn = instruction[5]; - if (!(d in this.coordinateCache_)) { - this.coordinateCache_[d] = []; - } - coords = fn(pixelCoordinates, d, dd, 2, this.coordinateCache_[d]); - } else { - coords = pixelCoordinates.slice(d, dd); + var fn = instruction.length == 6 ? instruction[5] : undefined; + state.geometry = geometry; + state.feature = feature; + if (!(i in coordinateCache)) { + coordinateCache[i] = []; } - renderer(coords, geometry, feature, state); + var coords = coordinateCache[i]; + if (fn) { + fn(pixelCoordinates, d, dd, 2, coords); + } else { + coords[0] = pixelCoordinates[d]; + coords[1] = pixelCoordinates[d + 1]; + coords.length = 2; + } + renderer(coords, state); ++i; break; case ol.render.canvas.Instruction.DRAW_IMAGE: diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index ec24160a3e..92e0a6cdbd 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -627,14 +627,12 @@ ol.StyleGeometryFunction; /** - * Custom renderer function. Takes 4 arguments: + * Custom renderer function. Takes two arguments: * * 1. The pixel coordinates of the geometry in GeoJSON notation. - * 2. The original {@link ol.geom.SimpleGeometry}. - * 3. The underlying {@link ol.Feature} or {@link ol.render.Feature}. - * 4. The {@link olx.render.State} of the layer renderer. + * 2. The {@link olx.render.State} of the layer renderer. * - * @typedef {function(Array,ol.geom.SimpleGeometry,(ol.Feature|ol.render.Feature),olx.render.State)} + * @typedef {function((ol.Coordinate|Array|Array.>),olx.render.State)} */ ol.StyleRenderFunction; diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js index 6398f5b55b..65ddd21f59 100644 --- a/test/spec/ol/renderer/canvas/replay.test.js +++ b/test/spec/ol/renderer/canvas/replay.test.js @@ -207,9 +207,19 @@ describe('ol.render.canvas.ReplayGroup', function() { }); it('calls the renderer function configured for the style', function() { - var spy = sinon.spy(); + var calls = []; var style = new ol.style.Style({ - renderer: spy + renderer: function(coords, state) { + calls.push({ + coords: coords, + geometry: state.geometry, + feature: state.feature, + context: state.context, + pixelRatio: state.pixelRatio, + rotation: state.rotation, + resolution: state.resolution + }); + } }); var point = new ol.Feature(new ol.geom.Point([45, 90])); var multipoint = new ol.Feature(new ol.geom.MultiPoint( @@ -233,24 +243,28 @@ describe('ol.render.canvas.ReplayGroup', function() { ol.renderer.vector.renderFeature(replay, geometrycollection, style, 1); ol.transform.scale(transform, 0.1, 0.1); replay.replay(context, 1, transform, 0, {}); - expect(spy.callCount).to.be(9); - expect(spy.firstCall.args.length).to.be(4); - expect(spy.firstCall.args[1]).to.be(point.getGeometry()); - expect(spy.firstCall.args[2]).to.be(point); - expect(spy.firstCall.args[3].context).to.be(context); - expect(spy.firstCall.args[3].pixelRatio).to.be(1); - expect(spy.firstCall.args[3].rotation).to.be(0); - expect(spy.firstCall.args[3].resolution).to.be(1); - expect(spy.getCall(0).args[0]).to.eql([4.5, 9]); - expect(spy.getCall(1).args[0][0]).to.eql([4.5, 9]); - expect(spy.getCall(2).args[0][0]).to.eql([4.5, 9]); - expect(spy.getCall(3).args[0][0][0]).to.eql([4.5, 9]); - expect(spy.getCall(4).args[0][0][0]).to.eql([-9, -4.5]); - expect(spy.getCall(5).args[0][0][0][0]).to.eql([-9, -4.5]); - expect(spy.getCall(6).args[2]).to.be(geometrycollection); - expect(spy.getCall(6).args[1].getCoordinates()).to.eql([45, 90]); - expect(spy.getCall(7).args[1].getCoordinates()[0]).to.eql([45, 90]); - expect(spy.getCall(8).args[1].getCoordinates()[0][0]).to.eql([-90, -45]); + expect(calls.length).to.be(9); + expect(calls[0].geometry).to.be(point.getGeometry()); + expect(calls[0].feature).to.be(point); + expect(calls[0].context).to.be(context); + expect(calls[0].pixelRatio).to.be(1); + expect(calls[0].rotation).to.be(0); + expect(calls[0].resolution).to.be(1); + expect(calls[0].coords).to.eql([4.5, 9]); + expect(calls[1].feature).to.be(multipoint); + expect(calls[1].coords[0]).to.eql([4.5, 9]); + expect(calls[2].feature).to.be(linestring); + expect(calls[2].coords[0]).to.eql([4.5, 9]); + expect(calls[3].feature).to.be(multilinestring); + expect(calls[3].coords[0][0]).to.eql([4.5, 9]); + expect(calls[4].feature).to.be(polygon); + expect(calls[4].coords[0][0]).to.eql([-9, -4.5]); + expect(calls[5].feature).to.be(multipolygon); + expect(calls[5].coords[0][0][0]).to.eql([-9, -4.5]); + expect(calls[6].feature).to.be(geometrycollection); + expect(calls[6].geometry.getCoordinates()).to.eql([45, 90]); + expect(calls[7].geometry.getCoordinates()[0]).to.eql([45, 90]); + expect(calls[8].geometry.getCoordinates()[0][0]).to.eql([-90, -45]); }); }); From 35f22fb52c5ac553219a8da887ebce00a91d5f31 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Aug 2017 22:54:14 +0200 Subject: [PATCH 3/3] Only create custom style for text we actually render --- examples/street-labels.js | 28 ++++----- examples/vector-label-decluttering.js | 89 ++++++++++++--------------- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/examples/street-labels.js b/examples/street-labels.js index 4a909ac6d3..6e2859e1c3 100644 --- a/examples/street-labels.js +++ b/examples/street-labels.js @@ -28,20 +28,18 @@ var style = new ol.style.Style({ renderer: function(coords, context) { var feature = context.feature; var text = feature.get('name'); - if (text) { - // Only create label when geometry has a long and straight segment - var path = labelSegment(coords, Math.PI / 8, measureText(text)); - if (path) { - extent = ol.extent.createEmpty(); - letters = []; - textPath(text, path, measureText, collectDrawData); - ol.extent.buffer(extent, 5 * pixelRatio, extent); - var bounds = { - bottomLeft: ol.extent.getBottomLeft(extent), - topRight: ol.extent.getTopRight(extent) - }; - labelEngine.ingestLabel(bounds, feature.getId(), 1, letters, text, false); - } + // Only create label when geometry has a long and straight segment + var path = labelSegment(coords, Math.PI / 8, measureText(text)); + if (path) { + extent = ol.extent.createEmpty(); + letters = []; + textPath(text, path, measureText, collectDrawData); + ol.extent.buffer(extent, 5 * pixelRatio, extent); + var bounds = { + bottomLeft: ol.extent.getBottomLeft(extent), + topRight: ol.extent.getTopRight(extent) + }; + labelEngine.ingestLabel(bounds, feature.getId(), 1, letters, text, false); } } }); @@ -70,7 +68,7 @@ fetch('https://overpass-api.de/api/interpreter', { var vectorLayer = new ol.layer.Vector({ source: source, style: function(feature) { - if (feature.getGeometry().getType() == 'LineString') { + if (feature.getGeometry().getType() == 'LineString' && feature.get('text')) { return style; } } diff --git a/examples/vector-label-decluttering.js b/examples/vector-label-decluttering.js index 574fd6db97..d3c366449a 100644 --- a/examples/vector-label-decluttering.js +++ b/examples/vector-label-decluttering.js @@ -4,7 +4,6 @@ goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.extent'); goog.require('ol.format.GeoJSON'); -goog.require('ol.geom.Point'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); goog.require('ol.style.Fill'); @@ -58,26 +57,43 @@ function sortByWidth(a, b) { return ol.extent.getWidth(b.getExtent()) - ol.extent.getWidth(a.getExtent()); } -var resolution; // This is set by the map's precompose listener -var styles = [ - new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 255, 255, 0.6)' - }), - stroke: new ol.style.Stroke({ - color: '#319FD3', - width: 1 - }) +var labelStyle = new ol.style.Style({ + renderer: function(coords, state) { + var text = state.feature.get('name'); + createLabel(textCache[text], text, coords); + } +}); +var countryStyle = new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.6)' }), - new ol.style.Style({ - renderer: function(coords, state) { - var pixelRatio = state.pixelRatio; - var text = state.feature.get('name'); - var canvas = textCache[text]; - if (!canvas) { + stroke: new ol.style.Stroke({ + color: '#319FD3', + width: 1 + }) +}); +var styleWithLabel = [countryStyle, labelStyle]; +var styleWithoutLabel = [countryStyle]; + +var pixelRatio; // This is set by the map's precompose listener +var vectorLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + url: 'data/geojson/countries.geojson', + format: new ol.format.GeoJSON() + }), + style: function(feature, resolution) { + var text = feature.get('name'); + var width = textMeasureContext.measureText(text).width; + var geometry = feature.getGeometry(); + if (geometry.getType() == 'MultiPolygon') { + geometry = geometry.getPolygons().sort(sortByWidth)[0]; + } + var extentWidth = ol.extent.getWidth(geometry.getExtent()); + if (extentWidth / resolution > width) { + // Only consider label when it fits its geometry's extent + if (!(text in textCache)) { // Draw the label to its own canvas and cache it. - var width = textMeasureContext.measureText(text).width; - canvas = textCache[text] = document.createElement('CANVAS'); + var canvas = textCache[text] = document.createElement('CANVAS'); canvas.width = width * pixelRatio; canvas.height = height * pixelRatio; var context = canvas.getContext('2d'); @@ -86,38 +102,15 @@ var styles = [ context.strokeText(text, 0, 0); context.fillText(text, 0, 0); } - // The 3rd value of the coordinate is the measure of the extent width - var extentWidth = state.geometry.getCoordinates()[2] / resolution * pixelRatio; - if (extentWidth > canvas.width) { - // Only consider labels not wider than their country's bounding box - createLabel(canvas, text, coord); - } - }, - // Geometry function to determine label positions - geometry: function(feature) { - var geometry = feature.getGeometry(); - if (geometry.getType() == 'MultiPolygon') { - var geometries = geometry.getPolygons(); - geometry = geometries.sort(sortByWidth)[0]; - } - var coordinates = geometry.getInteriorPoint().getCoordinates(); - var extentWidth = ol.extent.getWidth(geometry.getExtent()); - // We are using the extentWidth as measure value of the geometry - coordinates.push(extentWidth); - return new ol.geom.Point(coordinates, 'XYM'); + labelStyle.setGeometry(geometry.getInteriorPoint()); + return styleWithLabel; + } else { + return styleWithoutLabel; } - }) -]; - -var vectorLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - url: 'data/geojson/countries.geojson', - format: new ol.format.GeoJSON() - }), - style: styles + } }); vectorLayer.on('precompose', function(e) { - resolution = e.frameState.viewState.resolution; + pixelRatio = e.frameState.pixelRatio; labelEngine.destroy(); }); vectorLayer.on('postcompose', function(e) {