From dde81d85851a7a483053b64e0cadf811b1662037 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 22 Nov 2017 20:58:27 +0100 Subject: [PATCH] Add renderMode option to ol.layer.Vector This also deprecates ol.source.ImageVector. --- changelog/upgrade-notes.md | 29 ++++ examples/drag-and-drop-image-vector.html | 4 +- examples/drag-and-drop-image-vector.js | 12 +- examples/image-vector-layer.html | 4 +- examples/image-vector-layer.js | 45 +++-- externs/olx.js | 17 +- src/ol/layer/VectorRenderType.js | 16 ++ src/ol/layer/vector.js | 15 ++ src/ol/layer/vectortile.js | 37 ++-- src/ol/renderer/canvas/imagelayer.js | 77 +++++++- src/ol/renderer/canvas/vectorlayer.js | 11 +- src/ol/renderer/webgl/imagelayer.js | 3 +- src/ol/source/imagevector.js | 4 + test/rendering/ol/layer/vector.test.js | 212 +++++++++++++++++++++++ 14 files changed, 408 insertions(+), 78 deletions(-) create mode 100644 src/ol/layer/VectorRenderType.js diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index c43d94ccbd..75db24d11b 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -6,6 +6,35 @@ To update your applications, simply replace `exceedLength` with `overflow`. +#### Deprecation of `ol.source.ImageVector` + +Rendering vector sources as image is now directly supported by `ol.layer.Vector` with the new `renderMode: 'image'` configuration option. Change code like this: + +```js +new ol.layer.Image({ + source: new ol.source.ImageVector({ + style: myStyle, + source: new ol.source.Vector({ + url: 'my/data.json', + format: new ol.format.GeoJSON() + }) + }) +}); +``` +to: + +```js +new ol.layer.Vector({ + renderMode: 'image', + style: myStyle, + source: new ol.source.Vector({ + url: 'my/data.json', + format: new ol.format.GeoJSON() + }) +}); +``` + + ### v4.5.0 #### Removed GeoJSON crs workaround for GeoServer diff --git a/examples/drag-and-drop-image-vector.html b/examples/drag-and-drop-image-vector.html index d4284b1f6c..a4f89bc8b6 100644 --- a/examples/drag-and-drop-image-vector.html +++ b/examples/drag-and-drop-image-vector.html @@ -1,9 +1,9 @@ --- layout: example.html title: Drag-and-Drop Image Vector -shortdesc: Example of using the drag-and-drop interaction with a ol.source.ImageVector. +shortdesc: Example of using the drag-and-drop interaction with image vector rendering. docs: > - Example of using the drag-and-drop interaction with a ol.source.ImageVector. Drag and drop GPX, GeoJSON, IGC, KML, or TopoJSON files on to the map. Each file is rendered to an image on the client. + Example of using the drag-and-drop interaction with an `ol.layer.Vector` with `renderMode: 'image'``. Drag and drop GPX, GeoJSON, IGC, KML, or TopoJSON files on to the map. Each file is rendered to an image on the client. tags: "drag-and-drop-image-vector, gpx, geojson, igc, kml, topojson, vector, image" cloak: As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5: Your Bing Maps Key from http://www.bingmapsportal.com/ here diff --git a/examples/drag-and-drop-image-vector.js b/examples/drag-and-drop-image-vector.js index ab6239a970..a8fb251725 100644 --- a/examples/drag-and-drop-image-vector.js +++ b/examples/drag-and-drop-image-vector.js @@ -7,10 +7,9 @@ goog.require('ol.format.KML'); goog.require('ol.format.TopoJSON'); goog.require('ol.interaction'); goog.require('ol.interaction.DragAndDrop'); -goog.require('ol.layer.Image'); +goog.require('ol.layer.Vector'); goog.require('ol.layer.Tile'); goog.require('ol.source.BingMaps'); -goog.require('ol.source.ImageVector'); goog.require('ol.source.Vector'); goog.require('ol.style.Circle'); goog.require('ol.style.Fill'); @@ -115,11 +114,10 @@ dragAndDropInteraction.on('addfeatures', function(event) { var vectorSource = new ol.source.Vector({ features: event.features }); - map.addLayer(new ol.layer.Image({ - source: new ol.source.ImageVector({ - source: vectorSource, - style: styleFunction - }) + map.addLayer(new ol.layer.Vector({ + renderMode: 'image', + source: vectorSource, + style: styleFunction })); map.getView().fit(vectorSource.getExtent()); }); diff --git a/examples/image-vector-layer.html b/examples/image-vector-layer.html index 8050783b8c..b2d63eb9fe 100644 --- a/examples/image-vector-layer.html +++ b/examples/image-vector-layer.html @@ -3,9 +3,7 @@ layout: example.html title: Image Vector Layer shortdesc: Example of an image vector layer. docs: > -

This example uses a ol.source.ImageVector source. That source gets vector features from the - ol.source.Vector it's configured with, and draw these features to an HTML5 canvas element that - is then used as the image of an image layer.

+

This example uses ol.layer.Vector with `renderMode: 'image'`. This mode results in faster rendering during interaction and animations, at the cost of less accurate rendeering.

tags: "vector, image" ---
diff --git a/examples/image-vector-layer.js b/examples/image-vector-layer.js index 4f1e9ced77..3a5561a72f 100644 --- a/examples/image-vector-layer.js +++ b/examples/image-vector-layer.js @@ -1,38 +1,37 @@ goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.format.GeoJSON'); -goog.require('ol.layer.Image'); -goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); -goog.require('ol.source.ImageVector'); -goog.require('ol.source.OSM'); goog.require('ol.source.Vector'); goog.require('ol.style.Fill'); goog.require('ol.style.Stroke'); goog.require('ol.style.Style'); +goog.require('ol.style.Text'); +var style = 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 + }), + text: new ol.style.Text() +}); + var map = new ol.Map({ layers: [ - new ol.layer.Tile({ - source: new ol.source.OSM() - }), - new ol.layer.Image({ - source: new ol.source.ImageVector({ - source: new ol.source.Vector({ - url: 'data/geojson/countries.geojson', - format: new ol.format.GeoJSON() - }), - style: 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 - }) - }) - }) + new ol.layer.Vector({ + renderMode: 'image', + source: new ol.source.Vector({ + url: 'data/geojson/countries.geojson', + format: new ol.format.GeoJSON() + }), + style: function(feature) { + style.getText().setText(feature.get('name')); + return style; + } }) ], target: 'map', diff --git a/externs/olx.js b/externs/olx.js index 332870a03e..80ad93a5df 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4262,6 +4262,7 @@ olx.layer.TileOptions.prototype.zIndex; * maxResolution: (number|undefined), * opacity: (number|undefined), * renderBuffer: (number|undefined), + * renderMode: (ol.layer.VectorRenderType|string|undefined), * source: (ol.source.Vector|undefined), * map: (ol.PluggableMap|undefined), * declutter: (boolean|undefined), @@ -4274,6 +4275,19 @@ olx.layer.TileOptions.prototype.zIndex; olx.layer.VectorOptions; +/** + * Render mode for vectors: + * * `'image'`: Vectors are rendered as images. Great performance, but + * point symbols and texts are always rotated with the view and pixels are + * scaled during zoom animations. + * * `'vector'`: Vectors are rendered as vectors. Most accurate rendering + * even during animations, but slower performance. + * @type {ol.layer.VectorRenderType|string|undefined} + * @api + */ +olx.layer.VectorOptions.prototype.renderMode; + + /** * Render order. Function to be used when sorting features before rendering. By * default features are drawn in the order that they are created. Use `null` to @@ -4455,6 +4469,7 @@ olx.layer.VectorTileOptions.prototype.renderBuffer; */ olx.layer.VectorTileOptions.prototype.renderMode; + /** * Render order. Function to be used when sorting features before rendering. By * default features are drawn in the order that they are created. @@ -4579,7 +4594,7 @@ olx.layer.VectorTileOptions.prototype.visible; * @type {number|undefined} * @api */ -olx.layer.VectorOptions.prototype.zIndex; +olx.layer.VectorTileOptions.prototype.zIndex; /** diff --git a/src/ol/layer/VectorRenderType.js b/src/ol/layer/VectorRenderType.js new file mode 100644 index 0000000000..872c6adbf0 --- /dev/null +++ b/src/ol/layer/VectorRenderType.js @@ -0,0 +1,16 @@ +goog.provide('ol.layer.VectorRenderType'); + +/** + * @enum {string} + * Render mode for vector layers: + * * `'image'`: Vector tiles are rendered as images. Great performance, but + * point symbols and texts are always rotated with the view and pixels are + * scaled during zoom animations. + * * `'vector'`: Vector tiles are rendered as vectors. Most accurate rendering + * even during animations, but slower performance. + * @api + */ +ol.layer.VectorRenderType = { + IMAGE: 'image', + VECTOR: 'vector' +}; diff --git a/src/ol/layer/vector.js b/src/ol/layer/vector.js index a2e60decd8..34674ff02a 100644 --- a/src/ol/layer/vector.js +++ b/src/ol/layer/vector.js @@ -3,6 +3,7 @@ goog.provide('ol.layer.Vector'); goog.require('ol'); goog.require('ol.LayerType'); goog.require('ol.layer.Layer'); +goog.require('ol.layer.VectorRenderType'); goog.require('ol.obj'); goog.require('ol.style.Style'); @@ -75,6 +76,12 @@ ol.layer.Vector = function(opt_options) { this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ? options.updateWhileInteracting : false; + /** + * @private + * @type {ol.layer.VectorTileRenderType|string} + */ + this.renderMode_ = options.renderMode || ol.layer.VectorRenderType.VECTOR; + /** * The layer type. * @protected @@ -197,6 +204,14 @@ ol.layer.Vector.prototype.setStyle = function(style) { }; +/** + * @return {ol.layer.VectorRenderType|string} The render mode. + */ +ol.layer.Vector.prototype.getRenderMode = function() { + return this.renderMode_; +}; + + /** * @enum {string} * @private diff --git a/src/ol/layer/vectortile.js b/src/ol/layer/vectortile.js index fdb536f554..5fc5028689 100644 --- a/src/ol/layer/vectortile.js +++ b/src/ol/layer/vectortile.js @@ -24,6 +24,17 @@ goog.require('ol.obj'); ol.layer.VectorTile = function(opt_options) { var options = opt_options ? opt_options : {}; + var renderMode = options.renderMode || ol.layer.VectorTileRenderType.HYBRID; + ol.asserts.assert(renderMode == undefined || + renderMode == ol.layer.VectorTileRenderType.IMAGE || + renderMode == ol.layer.VectorTileRenderType.HYBRID || + renderMode == ol.layer.VectorTileRenderType.VECTOR, + 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` + if (options.declutter && renderMode == ol.layer.VectorTileRenderType.IMAGE) { + renderMode = ol.layer.VectorTileRenderType.HYBRID; + } + options.renderMode = renderMode; + var baseOptions = ol.obj.assign({}, options); delete baseOptions.preload; @@ -34,24 +45,6 @@ ol.layer.VectorTile = function(opt_options) { this.setUseInterimTilesOnError(options.useInterimTilesOnError ? options.useInterimTilesOnError : true); - var renderMode = options.renderMode; - - ol.asserts.assert(renderMode == undefined || - renderMode == ol.layer.VectorTileRenderType.IMAGE || - renderMode == ol.layer.VectorTileRenderType.HYBRID || - renderMode == ol.layer.VectorTileRenderType.VECTOR, - 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` - - if (options.declutter && renderMode == ol.layer.VectorTileRenderType.IMAGE) { - renderMode = ol.layer.VectorTileRenderType.HYBRID; - } - - /** - * @private - * @type {ol.layer.VectorTileRenderType|string} - */ - this.renderMode_ = renderMode || ol.layer.VectorTileRenderType.HYBRID; - /** * The layer type. * @protected @@ -74,14 +67,6 @@ ol.layer.VectorTile.prototype.getPreload = function() { }; -/** - * @return {ol.layer.VectorTileRenderType|string} The render mode. - */ -ol.layer.VectorTile.prototype.getRenderMode = function() { - return this.renderMode_; -}; - - /** * Whether we use interim tiles on error. * @return {boolean} Use interim tiles on error. diff --git a/src/ol/renderer/canvas/imagelayer.js b/src/ol/renderer/canvas/imagelayer.js index c38b73d3d1..b9a159548c 100644 --- a/src/ol/renderer/canvas/imagelayer.js +++ b/src/ol/renderer/canvas/imagelayer.js @@ -1,9 +1,13 @@ goog.provide('ol.renderer.canvas.ImageLayer'); goog.require('ol'); +goog.require('ol.ImageCanvas'); goog.require('ol.LayerType'); goog.require('ol.ViewHint'); goog.require('ol.extent'); +goog.require('ol.layer.VectorRenderType'); +goog.require('ol.obj'); +goog.require('ol.plugins'); goog.require('ol.renderer.Type'); goog.require('ol.renderer.canvas.IntermediateCanvas'); goog.require('ol.transform'); @@ -31,6 +35,12 @@ ol.renderer.canvas.ImageLayer = function(imageLayer) { */ this.imageTransform_ = ol.transform.create(); + /** + * @private + * @type {ol.renderer.canvas.VectorLayer} + */ + this.vectorRenderer_ = null; + }; ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.IntermediateCanvas); @@ -42,7 +52,9 @@ ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.IntermediateCanvas * @return {boolean} The renderer can render the layer. */ ol.renderer.canvas.ImageLayer['handles'] = function(type, layer) { - return type === ol.renderer.Type.CANVAS && layer.getType() === ol.LayerType.IMAGE; + return type === ol.renderer.Type.CANVAS && (layer.getType() === ol.LayerType.IMAGE || + layer.getType() === ol.LayerType.VECTOR && + /** @type {ol.layer.Vector} */ (layer).getRenderMode() === ol.layer.VectorRenderType.IMAGE); }; @@ -53,7 +65,17 @@ ol.renderer.canvas.ImageLayer['handles'] = function(type, layer) { * @return {ol.renderer.canvas.ImageLayer} The layer renderer. */ ol.renderer.canvas.ImageLayer['create'] = function(mapRenderer, layer) { - return new ol.renderer.canvas.ImageLayer(/** @type {ol.layer.Image} */ (layer)); + var renderer = new ol.renderer.canvas.ImageLayer(/** @type {ol.layer.Image} */ (layer)); + if (layer.getType() === ol.LayerType.VECTOR) { + var candidates = ol.plugins.getLayerRendererPlugins(); + for (var i = 0, ii = candidates.length; i < ii; ++i) { + var candidate = /** @type {Object.} */ (candidates[i]); + if (candidate !== ol.renderer.canvas.ImageLayer && candidate['handles'](ol.renderer.Type.CANVAS, layer)) { + renderer.setVectorRenderer(candidate['create'](mapRenderer, layer)); + } + } + } + return renderer; }; @@ -105,12 +127,31 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye projection = sourceProjection; } } - image = imageSource.getImage( - renderedExtent, viewResolution, pixelRatio, projection); - if (image) { - var loaded = this.loadImage(image); - if (loaded) { - this.image_ = image; + if (this.vectorRenderer_) { + var context = this.vectorRenderer_.context; + var imageFrameState = /** @type {olx.FrameState} */ (ol.obj.assign({}, frameState, { + size: [ + ol.extent.getWidth(renderedExtent) / viewResolution, + ol.extent.getHeight(renderedExtent) / viewResolution + ], + viewState: /** @type {olx.ViewState} */ (ol.obj.assign({}, frameState.viewState, { + rotation: 0 + })) + })); + if (this.vectorRenderer_.prepareFrame(imageFrameState, layerState)) { + context.canvas.width = imageFrameState.size[0] * pixelRatio; + context.canvas.height = imageFrameState.size[1] * pixelRatio; + this.vectorRenderer_.composeFrame(imageFrameState, layerState, context); + } + this.image_ = new ol.ImageCanvas(renderedExtent, viewResolution, pixelRatio, context.canvas); + } else { + image = imageSource.getImage( + renderedExtent, viewResolution, pixelRatio, projection); + if (image) { + var loaded = this.loadImage(image); + if (loaded) { + this.image_ = image; + } } } } @@ -140,3 +181,23 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye return !!this.image_; }; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { + if (this.vectorRenderer_) { + return this.vectorRenderer_.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); + } else { + return ol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate.call(this, coordinate, frameState, hitTolerance, callback, thisArg); + } +}; + + +/** + * @param {ol.renderer.canvas.VectorLayer} renderer Vector renderer. + */ +ol.renderer.canvas.ImageLayer.prototype.setVectorRenderer = function(renderer) { + this.vectorRenderer_ = renderer; +}; diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index ca702e15cd..d1fca8eb10 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -70,10 +70,9 @@ ol.renderer.canvas.VectorLayer = function(vectorLayer) { this.replayGroup_ = null; /** - * @private * @type {CanvasRenderingContext2D} */ - this.context_ = ol.dom.createCanvasContext2D(); + this.context = ol.dom.createCanvasContext2D(); ol.events.listen(ol.render.canvas.labelCache, ol.events.EventType.CLEAR, this.handleFontsChanged_, this); @@ -139,7 +138,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay } var replayGroup = this.replayGroup_; if (replayGroup && !replayGroup.isEmpty()) { - var layer = this.getLayer(); + var layer = /** @type {ol.layer.Vector} */ (this.getLayer()); var drawOffsetX = 0; var drawOffsetY = 0; var replayContext; @@ -155,9 +154,9 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay drawWidth = drawHeight = drawSize; } // resize and clear - this.context_.canvas.width = drawWidth; - this.context_.canvas.height = drawHeight; - replayContext = this.context_; + this.context.canvas.width = drawWidth; + this.context.canvas.height = drawHeight; + replayContext = this.context; } else { replayContext = context; } diff --git a/src/ol/renderer/webgl/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js index 485b442eb7..a6304d7d63 100644 --- a/src/ol/renderer/webgl/imagelayer.js +++ b/src/ol/renderer/webgl/imagelayer.js @@ -8,7 +8,6 @@ goog.require('ol.extent'); goog.require('ol.functions'); goog.require('ol.renderer.Type'); goog.require('ol.renderer.webgl.Layer'); -goog.require('ol.source.ImageVector'); goog.require('ol.transform'); goog.require('ol.webgl'); goog.require('ol.webgl.Context'); @@ -248,7 +247,7 @@ ol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, fra return undefined; } - if (this.getLayer().getSource() instanceof ol.source.ImageVector) { + if (this.getLayer().getSource().forEachFeatureAtCoordinate !== ol.nullFunction) { // for ImageVector sources use the original hit-detection logic, // so that for example also transparent polygons are detected var coordinate = ol.transform.apply( diff --git a/src/ol/source/imagevector.js b/src/ol/source/imagevector.js index 66a15f2fe6..78352056d7 100644 --- a/src/ol/source/imagevector.js +++ b/src/ol/source/imagevector.js @@ -14,7 +14,11 @@ goog.require('ol.transform'); /** + * @deprecated * @classdesc + * **Deprecated**. Use an `ol.layer.Vector` with `renderMode: 'image'` and an + * `ol.source.Vector` instead. + * * An image source whose images are canvas elements into which vector features * read from a vector source (`ol.source.Vector`) are drawn. An * `ol.source.ImageVector` object is to be used as the `source` of an image diff --git a/test/rendering/ol/layer/vector.test.js b/test/rendering/ol/layer/vector.test.js index bca13ea6ad..0f33f50977 100644 --- a/test/rendering/ol/layer/vector.test.js +++ b/test/rendering/ol/layer/vector.test.js @@ -101,6 +101,33 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('renders opacity correctly with renderMode: \'image\'', function(done) { + createMap('canvas'); + var smallLine = new ol.Feature(new ol.geom.LineString([ + [center[0], center[1] - 1], + [center[0], center[1] + 1] + ])); + smallLine.setStyle(new ol.style.Style({ + zIndex: -99, + stroke: new ol.style.Stroke({width: 75, color: 'red'}) + })); + source.addFeature(smallLine); + addPolygon(100); + addCircle(200); + addPolygon(250); + addCircle(500); + addPolygon(600); + addPolygon(720); + map.addLayer(new ol.layer.Vector({ + renerMode: 'image', + source: source + })); + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas.png', + 17, done); + }); + }); + it('renders transparent layers correctly with the canvas renderer', function(done) { createMap('canvas'); var smallLine = new ol.Feature(new ol.geom.LineString([ @@ -140,6 +167,46 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('renders transparent layers correctly with renderMode: \'image\'', function(done) { + createMap('canvas'); + var smallLine = new ol.Feature(new ol.geom.LineString([ + [center[0], center[1] - 1], + [center[0], center[1] + 1] + ])); + smallLine.setStyle([ + new ol.style.Style({ + stroke: new ol.style.Stroke({width: 75, color: 'red'}) + }), + new ol.style.Style({ + stroke: new ol.style.Stroke({width: 45, color: 'white'}) + }) + ]); + source.addFeature(smallLine); + var smallLine2 = new ol.Feature(new ol.geom.LineString([ + [center[0], center[1] - 1000], + [center[0], center[1] + 1000] + ])); + smallLine2.setStyle([ + new ol.style.Style({ + stroke: new ol.style.Stroke({width: 35, color: 'blue'}) + }), + new ol.style.Style({ + stroke: new ol.style.Stroke({width: 15, color: 'green'}) + }) + ]); + source.addFeature(smallLine2); + + map.addLayer(new ol.layer.Vector({ + renderMode: 'image', + source: source, + opacity: 0.5 + })); + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-transparent.png', + 7, done); + }); + }); + it('renders rotation correctly with the canvas renderer', function(done) { createMap('canvas'); map.getView().setRotation(Math.PI + Math.PI / 4); @@ -160,6 +227,27 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('renders rotation correctly with renderMode: \'image\'', function(done) { + createMap('canvas'); + map.getView().setRotation(Math.PI + Math.PI / 4); + addPolygon(300); + addCircle(500); + map.addLayer(new ol.layer.Vector({ + renderMode: 'image', + source: source, + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + width: 2, + color: 'black' + }) + }) + })); + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-rotated.png', + 2.9, done); + }); + }); + it('renders fill/stroke batches correctly with the canvas renderer', function(done) { createMap('canvas'); source = new ol.source.Vector({ @@ -510,6 +598,47 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('declutters text with renderMode: \'image\'', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + renderMode: 'image', + source: source + }); + map.addLayer(layer); + + var centerFeature = new ol.Feature({ + geometry: new ol.geom.Point(center), + text: 'center' + }); + source.addFeature(centerFeature); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]), + text: 'west' + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]), + text: 'east' + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + text: new ol.style.Text({ + text: feature.get('text'), + font: '12px sans-serif' + }) + }); + }); + + map.once('postrender', function() { + var hitDetected = map.getFeaturesAtPixel([42, 42]); + expect(hitDetected).to.have.length(1); + expect(hitDetected[0]).to.equal(centerFeature); + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter.png', + 2.2, done); + }); + }); + it('declutters text and respects z-index', function(done) { createMap('canvas'); var layer = new ol.layer.Vector({ @@ -589,6 +718,46 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('declutters images with renderMode: \'image\'', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + renderMode: 'image', + source: source + }); + map.addLayer(layer); + + var centerFeature = new ol.Feature({ + geometry: new ol.geom.Point(center) + }); + source.addFeature(centerFeature); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]) + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]) + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + image: new ol.style.Circle({ + radius: 15, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + }); + }); + + map.once('postrender', function() { + var hitDetected = map.getFeaturesAtPixel([40, 40]); + expect(hitDetected).to.have.length(1); + expect(hitDetected[0]).to.equal(centerFeature); + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-image.png', + IMAGE_TOLERANCE, done); + }); + }); + it('declutters images and respects z-index', function(done) { createMap('canvas'); var layer = new ol.layer.Vector({ @@ -715,6 +884,49 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('declutters text along lines and images with renderMode: \'image\'', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + var point = new ol.Feature(new ol.geom.Point(center)); + point.setStyle(new ol.style.Style({ + image: new ol.style.Circle({ + radius: 8, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + })); + var line = new ol.Feature(new ol.geom.LineString([ + [center[0] - 650, center[1] - 200], + [center[0] + 650, center[1] - 200] + ])); + line.setStyle(new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#CCC', + width: 12 + }), + text: new ol.style.Text({ + placement: 'line', + text: 'east-west', + font: '12px sans-serif' + }) + })); + + source.addFeature(point); + source.addFeature(line); + + layer.setDeclutter(true); + + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-line.png', + IMAGE_TOLERANCE, done); + }); + }); + it('declutters text along lines and images with z-index', function(done) { createMap('canvas'); var layer = new ol.layer.Vector({