From 3ef61fa1c5f89f22d99914488474ec7d4c1c8c7b Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 18 Dec 2014 13:21:20 +0100 Subject: [PATCH] Add hit-detection support for webgl --- src/ol/render/canvas/canvasreplay.js | 2 + src/ol/render/webgl/webglreplay.js | 190 ++++++++++++++++-- src/ol/renderer/webgl/webglmaprenderer.js | 85 +++++++- .../webgl/webglvectorlayerrenderer.js | 37 ++++ 4 files changed, 293 insertions(+), 21 deletions(-) diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index c1459e8a44..8b8eed5689 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -1951,6 +1951,8 @@ ol.render.canvas.ReplayGroup.prototype.replay = function( var zs = goog.array.map(goog.object.getKeys(this.replaysByZIndex_), Number); goog.array.sort(zs); + // setup clipping so that the parts of over-simplified geometries are not + // visible outside the current extent when panning var maxExtent = this.maxExtent_; var minX = maxExtent[0]; var minY = maxExtent[1]; diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 723e5c8164..d017868ef3 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -180,6 +180,13 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.verticesBuffer_ = null; + /** + * Start indices per feature. + * @type {Array.>} + * @private + */ + this.startIndexForFeature_ = []; + /** * @type {number|undefined} * @private @@ -377,6 +384,7 @@ ol.render.webgl.ImageReplay.prototype.drawMultiLineStringGeometry = */ ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = function(multiPointGeometry, feature) { + this.startIndexForFeature_.push([this.indices_.length, feature]); var flatCoordinates = multiPointGeometry.getFlatCoordinates(); var stride = multiPointGeometry.getStride(); this.drawCoordinates_( @@ -396,6 +404,7 @@ ol.render.webgl.ImageReplay.prototype.drawMultiPolygonGeometry = */ ol.render.webgl.ImageReplay.prototype.drawPointGeometry = function(pointGeometry, feature) { + this.startIndexForFeature_.push([this.indices_.length, feature]); var flatCoordinates = pointGeometry.getFlatCoordinates(); var stride = pointGeometry.getStride(); this.drawCoordinates_( @@ -503,12 +512,14 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { * @param {number} hue Global hue. * @param {number} saturation Global saturation. * @param {Object} skippedFeaturesHash Ids of features to skip. + * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. * @return {T|undefined} Callback result. * @template T */ ol.render.webgl.ImageReplay.prototype.replay = function(context, center, resolution, rotation, size, pixelRatio, - opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) { + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + featureCallback) { var gl = context.getGL(); // bind the vertices buffer @@ -609,17 +620,12 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, } // draw! - goog.asserts.assert(this.textures_.length == this.groupIndices_.length); - var i, ii, start; - for (i = 0, ii = this.textures_.length, start = 0; i < ii; ++i) { - gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]); - var end = this.groupIndices_[i]; - var numItems = end - start; - var offsetInBytes = start * (context.hasOESElementIndexUint ? 4 : 2); - var elementType = context.hasOESElementIndexUint ? - goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT; - gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); - start = end; + var result; + if (!goog.isDef(featureCallback)) { + this.drawReplay_(gl, context); + } else { + // draw feature by feature for the hit-detection + result = this.drawHitDetectionReplay_(gl, context, featureCallback); } // disable the vertex attrib arrays @@ -628,6 +634,79 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.disableVertexAttribArray(locations.a_texCoord); gl.disableVertexAttribArray(locations.a_opacity); gl.disableVertexAttribArray(locations.a_rotateWithView); + + return result; +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ImageReplay.prototype.drawReplay_ = + function(gl, context) { + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + var elementType = context.hasOESElementIndexUint ? + goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT; + var elementSize = context.hasOESElementIndexUint ? 4 : 2; + + var i, ii, start; + for (i = 0, ii = this.textures_.length, start = 0; i < ii; ++i) { + gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]); + var end = this.groupIndices_[i]; + var numItems = end - start; + var offsetInBytes = start * elementSize; + gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); + start = end; + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplay_ = + function(gl, context, featureCallback) { + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + var elementType = context.hasOESElementIndexUint ? + goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT; + var elementSize = context.hasOESElementIndexUint ? 4 : 2; + + var i, groupStart, groupEnd, numItems, featureInfo, start, end; + var featureIndex = this.startIndexForFeature_.length - 1; + for (i = this.textures_.length - 1; i >= 0; --i) { + gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]); + groupStart = (i > 0) ? this.groupIndices_[i - 1] : 0; + end = this.groupIndices_[i]; + + // draw all features for this texture group + while (featureIndex >= 0 && + this.startIndexForFeature_[featureIndex][0] >= groupStart) { + featureInfo = this.startIndexForFeature_[featureIndex]; + start = featureInfo[0]; + numItems = end - start; + var offsetInBytes = start * elementSize; + + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.drawElements( + goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); + + var result = featureCallback(/** @type {ol.Feature} */ (featureInfo[1])); + if (result) { + return result; + } + + end = start; + featureIndex--; + } + } + return undefined; }; @@ -788,19 +867,53 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { * @param {number} hue Global hue. * @param {number} saturation Global saturation. * @param {Object} skippedFeaturesHash Ids of features to skip. - * @return {T|undefined} Callback result. - * @template T */ ol.render.webgl.ReplayGroup.prototype.replay = function(context, center, resolution, rotation, size, pixelRatio, opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) { var i, ii, replay, result; for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) { + replay = this.replays_[ol.render.REPLAY_ORDER[i]]; + if (goog.isDef(replay)) { + replay.replay(context, + center, resolution, rotation, size, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + undefined); + } + } +}; + + +/** + * @private + * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {number} brightness Global brightness. + * @param {number} contrast Global contrast. + * @param {number} hue Global hue. + * @param {number} saturation Global saturation. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context, + center, resolution, rotation, size, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + featureCallback) { + var i, replay, result; + for (i = ol.render.REPLAY_ORDER.length - 1; i >= 0; --i) { replay = this.replays_[ol.render.REPLAY_ORDER[i]]; if (goog.isDef(replay)) { result = replay.replay(context, center, resolution, rotation, size, pixelRatio, - opacity, brightness, contrast, hue, saturation, skippedFeaturesHash); + opacity, brightness, contrast, hue, saturation, + skippedFeaturesHash, featureCallback); if (result) { return result; } @@ -810,6 +923,53 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, }; +/** + * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {number} brightness Global brightness. + * @param {number} contrast Global contrast. + * @param {number} hue Global hue. + * @param {number} saturation Global saturation. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @param {ol.Coordinate} coordinate Coordinate. + * @param {function(ol.Feature): T|undefined} callback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ReplayGroup.prototype.forEachGeometryAtPixel = function( + context, center, resolution, rotation, size, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + coordinate, callback) { + var gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + return this.replayHitDetection_(context, + coordinate, resolution, rotation, [1, 1], pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + /** + * @param {ol.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + var imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + + if (imageData[3] > 0) { + var result = callback(feature); + if (result) { + return result; + } + } + }); +}; + + /** * @const * @private diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index b5979505dc..1c06f8dda7 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -290,13 +290,10 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = replayGroup.finish(context); if (!replayGroup.isEmpty()) { // use default color values - var opacity = 1; - var brightness = 0; - var contrast = 1; - var hue = 0; - var saturation = 1; + var d = ol.renderer.webgl.Map.DEFAULT_COLOR_VALUES_; replayGroup.replay(context, center, resolution, rotation, size, - pixelRatio, opacity, brightness, contrast, hue, saturation, {}); + pixelRatio, d.opacity, d.brightness, d.contrast, + d.hue, d.saturation, {}); } replayGroup.getDeleteResourcesFunction(context)(); @@ -538,3 +535,79 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { this.scheduleExpireIconCache(frameState); }; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.Map.prototype.forEachFeatureAtPixel = + function(coordinate, frameState, callback, thisArg, + layerFilter, thisArg2) { + var result; + + if (this.getGL().isContextLost()) { + return false; + } + + var context = this.getContext(); + var viewState = frameState.viewState; + + // do the hit-detection for the overlays first + if (!goog.isNull(this.replayGroup)) { + /** @type {Object.} */ + var features = {}; + + // use default color values + var d = ol.renderer.webgl.Map.DEFAULT_COLOR_VALUES_; + + result = this.replayGroup.forEachGeometryAtPixel(context, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, + d.opacity, d.brightness, d.contrast, d.hue, d.saturation, {}, + coordinate, + /** + * @param {ol.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + 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.getMap().getLayerGroup().getLayerStatesArray(); + var numLayers = layerStates.length; + var i; + for (i = numLayers - 1; i >= 0; --i) { + var layerState = layerStates[i]; + var layer = layerState.layer; + if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) && + layerFilter.call(thisArg2, layer)) { + var layerRenderer = this.getLayerRenderer(layer); + result = layerRenderer.forEachFeatureAtPixel( + coordinate, frameState, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; +}; + + +/** + * @private + */ +ol.renderer.webgl.Map.DEFAULT_COLOR_VALUES_ = { + opacity: 1, + brightness: 0, + contrast: 1, + hue: 0, + saturation: 1 +}; diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index e2a685a633..5c914ef16a 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -58,6 +58,13 @@ ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) { */ this.replayGroup_ = null; + /** + * The last layer state. + * @private + * @type {?ol.layer.LayerState} + */ + this.layerState_ = null; + }; goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); @@ -67,6 +74,7 @@ goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); */ ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) { + this.layerState_ = layerState; var viewState = frameState.viewState; var replayGroup = this.replayGroup_; if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { @@ -100,6 +108,35 @@ ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() { */ ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtPixel = function(coordinate, frameState, callback, thisArg) { + if (goog.isNull(this.replayGroup_) || goog.isNull(this.layerState_)) { + return undefined; + } else { + var mapRenderer = this.getWebGLMapRenderer(); + var context = mapRenderer.getContext(); + var viewState = frameState.viewState; + var layer = this.getLayer(); + var layerState = this.layerState_; + /** @type {Object.} */ + var features = {}; + return this.replayGroup_.forEachGeometryAtPixel(context, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, + layerState.opacity, layerState.brightness, layerState.contrast, + layerState.hue, layerState.saturation, frameState.skippedFeatureUids, + coordinate, + /** + * @param {ol.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + goog.asserts.assert(goog.isDef(feature)); + var key = goog.getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, layer); + } + }); + } };