diff --git a/src/ol/featureoverlay.js b/src/ol/featureoverlay.js index 3aa6b1e045..b9db01bb52 100644 --- a/src/ol/featureoverlay.js +++ b/src/ol/featureoverlay.js @@ -10,7 +10,7 @@ goog.require('ol.CollectionEventType'); goog.require('ol.Feature'); goog.require('ol.feature'); goog.require('ol.render.EventType'); -goog.require('ol.style.ImageState'); +goog.require('ol.renderer.vector'); @@ -71,8 +71,11 @@ ol.FeatureOverlay = function(opt_options) { * @private * @type {ol.feature.StyleFunction|undefined} */ - this.styleFunction_ = goog.isDef(options.style) ? - ol.feature.createStyleFunction(options.style) : undefined; + this.styleFunction_ = undefined; + + if (goog.isDef(options.style)) { + this.setStyle(options.style); + } if (goog.isDef(options.features)) { if (goog.isArray(options.features)) { @@ -101,37 +104,6 @@ ol.FeatureOverlay.prototype.addFeature = function(feature) { }; -/** - * @param {ol.render.IVectorContext|undefined} vectorContext Vector context. - * @param {ol.Feature} feature Feature. - * @param {ol.style.Style} style Style. - * @private - */ -ol.FeatureOverlay.prototype.drawFeature_ = function(vectorContext, feature, - style) { - var imageStyle = style.getImage(); - if (!goog.isNull(imageStyle)) { - var imageState = imageStyle.getImageState(); - if (imageState == ol.style.ImageState.LOADED || - imageState == ol.style.ImageState.ERROR) { - imageStyle.unlistenImageChange(this.handleImageChange_, this); - if (imageState == ol.style.ImageState.LOADED) { - vectorContext.drawFeature(feature, style); - } - } else { - if (imageState == ol.style.ImageState.IDLE) { - imageStyle.load(); - } - imageState = imageStyle.getImageState(); - goog.asserts.assert(imageState == ol.style.ImageState.LOADING); - imageStyle.listenImageChange(this.handleImageChange_, this); - } - } else { - vectorContext.drawFeature(feature, style); - } -}; - - /** * @return {ol.Collection} Features collection. * @todo api @@ -199,8 +171,12 @@ ol.FeatureOverlay.prototype.handleMapPostCompose_ = function(event) { if (!goog.isDef(styleFunction)) { styleFunction = ol.feature.defaultStyleFunction; } - var resolution = event.frameState.view2DState.resolution; - var vectorContext = event.vectorContext; + var replayGroup = /** @type {ol.render.IReplayGroup} */ + (event.replayGroup); + goog.asserts.assert(goog.isDef(replayGroup)); + var frameState = event.frameState; + var pixelRatio = frameState.pixelRatio; + var resolution = frameState.view2DState.resolution; var i, ii, styles; this.features_.forEach(function(feature) { styles = styleFunction(feature, resolution); @@ -209,7 +185,9 @@ ol.FeatureOverlay.prototype.handleMapPostCompose_ = function(event) { } ii = styles.length; for (i = 0; i < ii; ++i) { - this.drawFeature_(vectorContext, feature, styles[i]); + ol.renderer.vector.renderFeature(replayGroup, feature, styles[i], + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), + feature, this.handleImageChange_, this); } }, this); }; diff --git a/src/ol/geom/flat/simplifyflatgeom.js b/src/ol/geom/flat/simplifyflatgeom.js index b5f2032040..7b747c6280 100644 --- a/src/ol/geom/flat/simplifyflatgeom.js +++ b/src/ol/geom/flat/simplifyflatgeom.js @@ -238,7 +238,7 @@ ol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end, /** * @param {number} value Value. - * @param {number} tolerance Squared tolerance. + * @param {number} tolerance Tolerance. * @return {number} Rounded value. */ ol.geom.flat.simplify.snap = function(value, tolerance) { @@ -259,7 +259,7 @@ ol.geom.flat.simplify.snap = function(value, tolerance) { * @param {number} offset Offset. * @param {number} end End. * @param {number} stride Stride. - * @param {number} tolerance Squared tolerance. + * @param {number} tolerance Tolerance. * @param {Array.} simplifiedFlatCoordinates Simplified flat * coordinates. * @param {number} simplifiedOffset Simplified offset. @@ -344,7 +344,7 @@ ol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride, * @param {number} offset Offset. * @param {Array.} ends Ends. * @param {number} stride Stride. - * @param {number} tolerance Squared tolerance. + * @param {number} tolerance Tolerance. * @param {Array.} simplifiedFlatCoordinates Simplified flat * coordinates. * @param {number} simplifiedOffset Simplified offset. @@ -374,7 +374,7 @@ ol.geom.flat.simplify.quantizes = function( * @param {number} offset Offset. * @param {Array.>} endss Endss. * @param {number} stride Stride. - * @param {number} tolerance Squared tolerance. + * @param {number} tolerance Tolerance. * @param {Array.} simplifiedFlatCoordinates Simplified flat * coordinates. * @param {number} simplifiedOffset Simplified offset. diff --git a/src/ol/interaction/selectinteraction.js b/src/ol/interaction/selectinteraction.js index a926065dee..8d3f727ce1 100644 --- a/src/ol/interaction/selectinteraction.js +++ b/src/ol/interaction/selectinteraction.js @@ -150,6 +150,8 @@ ol.interaction.Select.prototype.handleMapBrowserEvent = } } else { // Modify the currently selected feature(s). + var /** @type {Array.} */ deselected = []; + var /** @type {Array.} */ selected = []; map.forEachFeatureAtPixel(mapBrowserEvent.pixel, /** * @param {ol.Feature} feature Feature. @@ -159,14 +161,19 @@ ol.interaction.Select.prototype.handleMapBrowserEvent = var index = goog.array.indexOf(features.getArray(), feature); if (index == -1) { if (add || toggle) { - features.push(feature); + selected.push(feature); } } else { if (remove || toggle) { - features.removeAt(index); + deselected.push(index); } } }, undefined, this.layerFilter_); + var i; + for (i = deselected.length - 1; i >= 0; --i) { + features.removeAt(deselected[i]); + } + features.extend(selected); } return false; }; diff --git a/src/ol/map.js b/src/ol/map.js index ce082836d0..07aedc3fd4 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -535,16 +535,24 @@ ol.Map.prototype.disposeInternal = function() { /** + * Detect features that intersect a pixel on the viewport, and execute a + * callback with each intersecting feature. Layers included in the detection can + * be configured through `opt_layerFilter`. Feature overlays will always be + * included in the detection. * @param {ol.Pixel} pixel Pixel. * @param {function(this: S, ol.Feature, ol.layer.Layer): T} callback Feature - * callback. + * callback. If the detected feature is not on a layer, but on a + * {@link ol.FeatureOverlay}, then the 2nd argument to this function will + * be `null`. To stop detection, callback functions can return a truthy + * value. * @param {S=} opt_this Value to use as `this` when executing `callback`. * @param {function(this: U, ol.layer.Layer): boolean=} opt_layerFilter Layer * filter function, only layers which are visible and for which this - * function returns `true` will be tested for features. By default, all - * visible layers will be tested. + * function returns `true` will be tested for features. By default, all + * visible layers will be tested. Feature overlays will always be tested. * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`. - * @return {T|undefined} Callback result. + * @return {T|undefined} Callback result, i.e. the return value of last + * callback execution, or the first truthy callback return value. * @template S,T,U * @todo api stable */ diff --git a/src/ol/ol.js b/src/ol/ol.js index 56c0874dc6..c2057ed4c4 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -173,6 +173,12 @@ ol.MOUSEWHEELZOOM_TIMEOUT_DURATION = 80; ol.ROTATE_ANIMATION_DURATION = 250; +/** + * @define {number} Tolerance for geometry simplification in device pixels. + */ +ol.SIMPLIFY_TOLERANCE = 0.5; + + /** * @define {number} Texture cache high water mark. */ diff --git a/src/ol/render/renderevent.js b/src/ol/render/renderevent.js index dcb2597ef5..df6fe50664 100644 --- a/src/ol/render/renderevent.js +++ b/src/ol/render/renderevent.js @@ -35,13 +35,14 @@ ol.render.EventType = { * @param {ol.render.EventType} type Type. * @param {Object=} opt_target Target. * @param {ol.render.IVectorContext=} opt_vectorContext Vector context. + * @param {ol.render.IReplayGroup=} opt_replayGroup Replay group. * @param {olx.FrameState=} opt_frameState Frame state. * @param {?CanvasRenderingContext2D=} opt_context Context. * @param {?ol.webgl.Context=} opt_glContext WebGL Context. */ ol.render.Event = function( - type, opt_target, opt_vectorContext, opt_frameState, opt_context, - opt_glContext) { + type, opt_target, opt_vectorContext, opt_replayGroup, opt_frameState, + opt_context, opt_glContext) { goog.base(this, type, opt_target); @@ -52,6 +53,11 @@ ol.render.Event = function( */ this.vectorContext = opt_vectorContext; + /** + * @type {ol.render.IReplayGroup|undefined} + */ + this.replayGroup = opt_replayGroup; + /** * @type {olx.FrameState|undefined} * @todo api diff --git a/src/ol/render/vector.js b/src/ol/render/vector.js index 93271dd843..39b259670a 100644 --- a/src/ol/render/vector.js +++ b/src/ol/render/vector.js @@ -24,6 +24,27 @@ ol.renderer.vector.defaultOrder = function(feature1, feature2) { }; +/** + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @return {number} Squared pixel tolerance. + */ +ol.renderer.vector.getSquaredTolerance = function(resolution, pixelRatio) { + var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio); + return tolerance * tolerance; +}; + + +/** + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @return {number} Pixel tolerance. + */ +ol.renderer.vector.getTolerance = function(resolution, pixelRatio) { + return ol.SIMPLIFY_TOLERANCE * resolution / pixelRatio; +}; + + /** * @param {ol.render.IReplayGroup} replayGroup Replay group. * @param {ol.geom.Geometry} geometry Geometry. diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 54375fc932..2732f5a1a0 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -90,8 +90,8 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = var render = new ol.render.canvas.Immediate( context, frameState.pixelRatio, frameState.extent, transform, frameState.view2DState.rotation); - var composeEvent = new ol.render.Event(type, layer, render, frameState, - context, null); + var composeEvent = new ol.render.Event(type, layer, render, null, + frameState, context, null); layer.dispatchEvent(composeEvent); render.flush(); } diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index c262d394e9..7177e5c824 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -17,11 +17,13 @@ goog.require('ol.layer.Vector'); goog.require('ol.render.Event'); goog.require('ol.render.EventType'); goog.require('ol.render.canvas.Immediate'); +goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.Map'); goog.require('ol.renderer.canvas.ImageLayer'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.TileLayer'); goog.require('ol.renderer.canvas.VectorLayer'); +goog.require('ol.renderer.vector'); goog.require('ol.source.State'); goog.require('ol.vec.Mat4'); @@ -97,8 +99,11 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = var map = this.getMap(); var context = this.context_; if (map.hasListener(type)) { - var view2DState = frameState.view2DState; + var extent = frameState.extent; var pixelRatio = frameState.pixelRatio; + var view2DState = frameState.view2DState; + var resolution = view2DState.resolution; + var rotation = view2DState.rotation; ol.vec.Mat4.makeTransform2D(this.transform_, this.canvas_.width / 2, this.canvas_.height / 2, @@ -106,12 +111,21 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = -pixelRatio / view2DState.resolution, -view2DState.rotation, -view2DState.center[0], -view2DState.center[1]); - var render = new ol.render.canvas.Immediate(context, pixelRatio, - frameState.extent, this.transform_, view2DState.rotation); - var composeEvent = new ol.render.Event(type, map, render, frameState, - context, null); + var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio, + extent, this.transform_, rotation); + var replayGroup = new ol.render.canvas.ReplayGroup( + ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, + resolution); + var composeEvent = new ol.render.Event(type, map, vectorContext, + replayGroup, frameState, context, null); map.dispatchEvent(composeEvent); - render.flush(); + replayGroup.finish(); + if (!replayGroup.isEmpty()) { + replayGroup.replay(context, extent, pixelRatio, this.transform_, + rotation, {}); + } + vectorContext.flush(); + this.replayGroup = replayGroup; } }; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index aa148b4d77..19970bdc7f 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -206,9 +206,10 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = if (!goog.isDef(styleFunction)) { styleFunction = ol.feature.defaultStyleFunction; } - var tolerance = resolution / (2 * pixelRatio); var replayGroup = - new ol.render.canvas.ReplayGroup(tolerance, extent, resolution); + new ol.render.canvas.ReplayGroup( + ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, + resolution); vectorSource.loadFeatures(extent, resolution, projection); var renderFeature = /** @@ -261,14 +262,12 @@ ol.renderer.canvas.VectorLayer.prototype.renderFeature = if (!goog.isDefAndNotNull(styles)) { return false; } - // simplify to a tolerance of half a device pixel - var squaredTolerance = - resolution * resolution / (4 * pixelRatio * pixelRatio); var i, ii, loading = false; for (i = 0, ii = styles.length; i < ii; ++i) { loading = ol.renderer.vector.renderFeature( - replayGroup, feature, styles[i], squaredTolerance, feature, - this.handleImageChange_, this) || loading; + replayGroup, feature, styles[i], + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), + feature, this.handleImageChange_, this) || loading; } return loading; }; diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 9308533ff1..d8cd7cf3a8 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -43,6 +43,12 @@ ol.renderer.Map = function(container, map) { */ this.map_ = map; + /** + * @protected + * @type {ol.render.IReplayGroup} + */ + this.replayGroup = null; + /** * @private * @type {Object.} @@ -110,9 +116,36 @@ ol.renderer.Map.prototype.disposeInternal = function() { ol.renderer.Map.prototype.forEachFeatureAtPixel = function(coordinate, frameState, callback, thisArg, layerFilter, thisArg2) { + var result; + var extent = frameState.extent; + var view2DState = frameState.view2DState; + var viewResolution = view2DState.resolution; + var viewRotation = view2DState.rotation; + if (!goog.isNull(this.replayGroup)) { + /** @type {Object.} */ + var features = {}; + result = this.replayGroup.forEachGeometryAtPixel(extent, viewResolution, + viewRotation, coordinate, {}, + /** + * @param {ol.geom.Geometry} geometry Geometry. + * @param {Object} data Data. + * @return {?} Callback result. + */ + function(geometry, data) { + var feature = /** @type {ol.Feature} */ (data); + goog.asserts.assert(goog.isDef(feature)); + var key = goog.getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, null); + } + }); + if (result) { + return result; + } + } var layerStates = this.map_.getLayerGroup().getLayerStatesArray(); var numLayers = layerStates.length; - var viewResolution = frameState.view2DState.resolution; var i; for (i = numLayers - 1; i >= 0; --i) { var layerState = layerStates[i]; @@ -120,7 +153,7 @@ ol.renderer.Map.prototype.forEachFeatureAtPixel = if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { var layerRenderer = this.getLayerRenderer(layer); - var result = layerRenderer.forEachFeatureAtPixel( + result = layerRenderer.forEachFeatureAtPixel( coordinate, frameState, callback, thisArg); if (result) { return result; diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 707a661abd..823ffcc184 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -239,7 +239,7 @@ ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ = if (layer.hasListener(type)) { var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio); var composeEvent = new ol.render.Event( - type, layer, render, frameState, null, context); + type, layer, render, null, frameState, null, context); layer.dispatchEvent(composeEvent); } }; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 2b8074ee29..ecb2c222fe 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -269,7 +269,7 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = var context = this.getContext(); var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio); var composeEvent = new ol.render.Event( - type, map, render, frameState, null, context); + type, map, render, null, frameState, null, context); map.dispatchEvent(composeEvent); } }; diff --git a/src/ol/source/imagevectorsource.js b/src/ol/source/imagevectorsource.js index c26b1a9d87..992ac56c7c 100644 --- a/src/ol/source/imagevectorsource.js +++ b/src/ol/source/imagevectorsource.js @@ -102,8 +102,8 @@ goog.inherits(ol.source.ImageVector, ol.source.ImageCanvas); ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resolution, pixelRatio, size, projection) { - var tolerance = resolution / (2 * pixelRatio); - var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, extent, + var replayGroup = new ol.render.canvas.ReplayGroup( + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), extent, resolution); var loading = false; @@ -226,14 +226,12 @@ ol.source.ImageVector.prototype.renderFeature_ = if (!goog.isDefAndNotNull(styles)) { return false; } - // simplify to a tolerance of half a device pixel - var squaredTolerance = - resolution * resolution / (4 * pixelRatio * pixelRatio); var i, ii, loading = false; for (i = 0, ii = styles.length; i < ii; ++i) { loading = ol.renderer.vector.renderFeature( - replayGroup, feature, styles[i], squaredTolerance, feature, - this.handleImageChange_, this) || loading; + replayGroup, feature, styles[i], + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), + feature, this.handleImageChange_, this) || loading; } return loading; }; diff --git a/test/spec/ol/featureoverlay.test.js b/test/spec/ol/featureoverlay.test.js index fe46d74f4f..47aa06189a 100644 --- a/test/spec/ol/featureoverlay.test.js +++ b/test/spec/ol/featureoverlay.test.js @@ -1,70 +1,34 @@ goog.provide('ol.test.FeatureOverlay'); -describe('ol.Feature', function() { - var featureOverlay; +describe('ol.FeatureOverlay', function() { - beforeEach(function() { - featureOverlay = new ol.FeatureOverlay(); - }); + describe('constructor', function() { - afterEach(function() { - ol.style.IconImageCache.getInstance().clear(); - }); - - describe('#drawFeature_ style with no image', function() { - it('calls vectorContext.drawFeature', function() { - var vectorContext = new ol.render.canvas.Immediate( - null, // context - 1, // pixelRatio - [], // extent - goog.vec.Mat4.createNumberIdentity(), // transform - 0 // viewRotation - ); - var feature = new ol.Feature(); - var style = new ol.style.Style({ - fill: new ol.style.Fill({ - color: '#ffffff' - }) - }); - - var spy = sinon.spy(vectorContext, 'drawFeature'); - featureOverlay.drawFeature_(vectorContext, feature, style); - expect(spy.calledOnce).to.be.ok(); + it('creates an new feature overlay', function() { + var featureOverlay = new ol.FeatureOverlay(); + expect(featureOverlay).to.be.a(ol.FeatureOverlay); }); - }); - describe('#drawFeature_ style with unloaded image', function() { - it('calls image.load', function() { - var vectorContext = new ol.render.canvas.Immediate( - null, // context - 1, // pixelRatio - [], // extent - goog.vec.Mat4.createNumberIdentity(), // transform - 0 // viewRotation - ); - var feature = new ol.Feature(); - var style = new ol.style.Style({ - image: new ol.style.Icon({ - src: 'http://example.com/icon.png' - }) + it('takes features', function() { + var featureOverlay = new ol.FeatureOverlay({ + features: [new ol.Feature(new ol.geom.Point([0, 0]))] }); - - var stub = sinon.stub(style.getImage(), 'load', function() { - style.getImage().iconImage_.imageState_ = - ol.style.ImageState.LOADING; - }); - featureOverlay.drawFeature_(vectorContext, feature, style); - expect(stub.calledOnce).to.be.ok(); + expect(featureOverlay.getFeatures().getLength()).to.be(1); }); + + it('takes a style', function() { + var style = [new ol.style.Style()]; + var featureOverlay = new ol.FeatureOverlay({ + style: [new ol.style.Style()] + }); + expect(featureOverlay.getStyle()).to.eql(style); + expect(featureOverlay.getStyleFunction()()).to.eql(style); + }); + }); }); -goog.require('goog.vec.Mat4'); goog.require('ol.Feature'); goog.require('ol.FeatureOverlay'); -goog.require('ol.style.ImageState'); -goog.require('ol.render.canvas.Immediate'); -goog.require('ol.style.Fill'); -goog.require('ol.style.Icon'); -goog.require('ol.style.IconImageCache'); +goog.require('ol.geom.Point'); goog.require('ol.style.Style');