From 33adcd390378128820b3e85da15a83e2cf74b592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 23 Oct 2014 10:50:05 +0200 Subject: [PATCH 01/98] Add very basic webgl vector renderer --- examples/icon.js | 2 +- src/ol/render/canvas/canvasreplay.js | 2 +- src/ol/render/ireplay.js | 7 - src/ol/render/webgl/webglreplay.js | 367 ++++++++++++++++++ src/ol/renderer/canvas/canvaslayerrenderer.js | 8 + src/ol/renderer/dom/domlayerrenderer.js | 8 + src/ol/renderer/layerrenderer.js | 9 - .../renderer/webgl/webglimagelayerrenderer.js | 2 +- src/ol/renderer/webgl/webgllayerrenderer.js | 9 + src/ol/renderer/webgl/webglmaprenderer.js | 6 +- .../renderer/webgl/webgltilelayerrenderer.js | 5 +- src/ol/renderer/webgl/webglvectorlayer.glsl | 23 ++ .../webgl/webglvectorlayerrenderer.js | 266 +++++++++++++ .../renderer/webgl/webglvectorlayershader.js | 99 +++++ 14 files changed, 790 insertions(+), 23 deletions(-) create mode 100644 src/ol/render/webgl/webglreplay.js create mode 100644 src/ol/renderer/webgl/webglvectorlayer.glsl create mode 100644 src/ol/renderer/webgl/webglvectorlayerrenderer.js create mode 100644 src/ol/renderer/webgl/webglvectorlayershader.js diff --git a/examples/icon.js b/examples/icon.js index d510cf16fa..ab9cb41deb 100644 --- a/examples/icon.js +++ b/examples/icon.js @@ -5,7 +5,7 @@ goog.require('ol.View'); goog.require('ol.geom.Point'); goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); -goog.require('ol.source.TileJSON'); +goog.require('ol.source.OSM'); goog.require('ol.source.Vector'); goog.require('ol.style.Icon'); goog.require('ol.style.Style'); diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index 5d7b35aef6..e2ab0f38e0 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -2011,7 +2011,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachGeometryAtPixel = function( /** - * @inheritDoc + * FIXME empty description for jsdoc */ ol.render.canvas.ReplayGroup.prototype.finish = function() { var zKey; diff --git a/src/ol/render/ireplay.js b/src/ol/render/ireplay.js index 3f4d7c122a..78e2faf67a 100644 --- a/src/ol/render/ireplay.js +++ b/src/ol/render/ireplay.js @@ -35,13 +35,6 @@ ol.render.IReplayGroup = function() { }; -/** - * FIXME empty description for jsdoc - */ -ol.render.IReplayGroup.prototype.finish = function() { -}; - - /** * @param {number|undefined} zIndex Z index. * @param {ol.render.ReplayType} replayType Replay type. diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js new file mode 100644 index 0000000000..849a87e848 --- /dev/null +++ b/src/ol/render/webgl/webglreplay.js @@ -0,0 +1,367 @@ +goog.provide('ol.render.webgl.ReplayGroup'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.object'); +goog.require('ol.extent'); +goog.require('ol.render.IReplayGroup'); + + + +/** + * @constructor + * @implements {ol.render.IVectorContext} + * @param {number} tolerance Tolerance. + * @protected + * @struct + */ +ol.render.webgl.Replay = function(tolerance) { + + /** + * @protected + * @type {Array.} + */ + this.coordinates = []; + + /** + * @protected + * @type {WebGLBuffer} + */ + this.buffer = null; + + /** + * @private + * @type {ol.Extent} + */ + this.extent_ = ol.extent.createEmpty(); + +}; + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @param {boolean} close Close. + * @protected + * @return {number} My end. + */ +ol.render.webgl.Replay.prototype.appendFlatCoordinates = + function(flatCoordinates, offset, end, stride, close) { + var myEnd = this.coordinates.length; + var i; + for (i = offset; i < end; i += stride) { + this.coordinates[myEnd++] = flatCoordinates[i]; + this.coordinates[myEnd++] = flatCoordinates[i + 1]; + } + if (close) { + this.coordinates[myEnd++] = flatCoordinates[offset]; + this.coordinates[myEnd++] = flatCoordinates[offset + 1]; + } + return myEnd; +}; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.Replay.prototype.finish = goog.nullFunction; + + +/** + * @param {ol.webgl.Context} context Context. + * @param {number} attribLocation Attribute location. + * @param {WebGLUniformLocation} projectionMatrixLocation Projection + * matrix location. + * @param {number} pixelRatio Pixel ratio. + * @param {goog.vec.Mat4.Number} transform Transform. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.Replay.prototype.replay = + function(context, attribLocation, projectionMatrixLocation, + pixelRatio, transform, skippedFeaturesHash) { + var gl = context.getGL(); + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.buffer); + gl.uniformMatrix4fv(projectionMatrixLocation, false, + transform); + gl.enableVertexAttribArray(attribLocation); + gl.vertexAttribPointer(attribLocation, 2, goog.webgl.FLOAT, + false, 0, 0); + gl.drawArrays(goog.webgl.POINTS, 0, this.coordinates.length / 2); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawAsync = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawCircleGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawFeature = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawGeometryCollectionGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawLineStringGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawMultiLineStringGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawPointGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawMultiPointGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawPolygonGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawMultiPolygonGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.drawText = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.setFillStrokeStyle = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.setImageStyle = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.Replay.prototype.setTextStyle = goog.abstractMethod; + + + +/** + * @constructor + * @extends {ol.render.webgl.Replay} + * @param {number} tolerance Tolerance. + * @protected + * @struct + */ +ol.render.webgl.ImageReplay = function(tolerance) { + + goog.base(this, tolerance); + +}; +goog.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @private + * @return {number} My end. + */ +ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = + function(flatCoordinates, offset, end, stride) { + return this.appendFlatCoordinates( + flatCoordinates, offset, end, stride, false); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawPointGeometry = + function(pointGeometry, data) { + ol.extent.extend(this.extent_, pointGeometry.getExtent()); + var flatCoordinates = pointGeometry.getFlatCoordinates(); + var stride = pointGeometry.getStride(); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = + function(multiPointGeometry, data) { + ol.extent.extend(this.extent_, multiPointGeometry.getExtent()); + var flatCoordinates = multiPointGeometry.getFlatCoordinates(); + var stride = multiPointGeometry.getStride(); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); +}; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ImageReplay.prototype.finish = function(context) { + var gl = context.getGL(); + this.buffer = gl.createBuffer(); + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.buffer); + gl.bufferData(goog.webgl.ARRAY_BUFFER, + new Float32Array(this.coordinates), goog.webgl.STATIC_DRAW); +}; + + +/** + * @return {ol.Extent} Extent. + */ +ol.render.webgl.Replay.prototype.getExtent = function() { + return this.extent_; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { +}; + + + +/** + * @constructor + * @implements {ol.render.IReplayGroup} + * @param {number} tolerance Tolerance. + * @struct + */ +ol.render.webgl.ReplayGroup = function(tolerance) { + + /** + * @private + * @type {number} + */ + this.tolerance_ = tolerance; + + /** + * @private + * @type {Object.} + */ + this.replays_ = {}; + +}; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ReplayGroup.prototype.finish = function(context) { + var replayKey; + for (replayKey in this.replays_) { + this.replays_[replayKey].finish(context); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.getReplay = + function(zIndex, replayType) { + var replay = this.replays_[replayType]; + if (!goog.isDef(replay)) { + var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType]; + goog.asserts.assert(goog.isDef(constructor)); + replay = new constructor(this.tolerance_); + this.replays_[replayType] = replay; + } + return replay; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { + return goog.object.isEmpty(this.replays_); +}; + + +/** + * @param {ol.webgl.Context} context Context. + * @param {number} attribLocation Attribute location. + * @param {WebGLUniformLocation} projectionMatrixLocation Projection + * matrix location. + * @param {ol.Extent} extent Extent. + * @param {number} pixelRatio Pixel ratio. + * @param {goog.vec.Mat4.Number} transform Transform. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ReplayGroup.prototype.replay = function( + context, attribLocation, projectionMatrixLocation, extent, + pixelRatio, transform, 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) && + ol.extent.intersects(extent, replay.getExtent())) { + result = replay.replay( + context, attribLocation, projectionMatrixLocation, + pixelRatio, transform, skippedFeaturesHash); + if (result) { + return result; + } + } + } + return undefined; +}; + + +/** + * @const + * @private + * @type {Object.} + */ +ol.render.webgl.BATCH_CONSTRUCTORS_ = { + 'Image': ol.render.webgl.ImageReplay +}; diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 65fa90866e..f48f64eb7a 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -172,6 +172,14 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState) { }; +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.canvas.Layer.prototype.prepareFrame = goog.abstractMethod; + + /** * @param {ol.Size} size Size. * @return {boolean} True when the canvas with the current size does not exceed diff --git a/src/ol/renderer/dom/domlayerrenderer.js b/src/ol/renderer/dom/domlayerrenderer.js index df94a74b2e..072e32435e 100644 --- a/src/ol/renderer/dom/domlayerrenderer.js +++ b/src/ol/renderer/dom/domlayerrenderer.js @@ -46,3 +46,11 @@ ol.renderer.dom.Layer.prototype.composeFrame = goog.nullFunction; ol.renderer.dom.Layer.prototype.getTarget = function() { return this.target; }; + + +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.dom.Layer.prototype.prepareFrame = goog.abstractMethod; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index ef9ee9029d..356561bb0f 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -6,7 +6,6 @@ goog.require('ol.ImageState'); goog.require('ol.TileRange'); goog.require('ol.TileState'); goog.require('ol.layer.Layer'); -goog.require('ol.layer.LayerState'); goog.require('ol.source.Source'); goog.require('ol.source.State'); goog.require('ol.source.Tile'); @@ -95,14 +94,6 @@ ol.renderer.Layer.prototype.handleImageChange = function(event) { }; -/** - * @param {olx.FrameState} frameState Frame state. - * @param {ol.layer.LayerState} layerState Layer state. - * @return {boolean} whether composeFrame should be called. - */ -ol.renderer.Layer.prototype.prepareFrame = goog.abstractMethod; - - /** * @protected */ diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js index 7022b626e9..9b093e7e44 100644 --- a/src/ol/renderer/webgl/webglimagelayerrenderer.js +++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js @@ -100,7 +100,7 @@ ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtPixel = * @inheritDoc */ ol.renderer.webgl.ImageLayer.prototype.prepareFrame = - function(frameState, layerState) { + function(frameState, layerState, context) { var gl = this.getWebGLMapRenderer().getGL(); diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 823ffcc184..6c51fc0394 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -286,3 +286,12 @@ ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() { this.framebuffer = null; this.framebufferDimension = undefined; }; + + +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @param {ol.webgl.Context} context Context. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.webgl.Layer.prototype.prepareFrame = goog.abstractMethod; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 12d5982bee..f093ba274f 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -20,6 +20,7 @@ goog.require('ol.dom'); goog.require('ol.layer.Image'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Tile'); +goog.require('ol.layer.Vector'); goog.require('ol.render.Event'); goog.require('ol.render.EventType'); goog.require('ol.render.webgl.Immediate'); @@ -27,6 +28,7 @@ goog.require('ol.renderer.Map'); goog.require('ol.renderer.webgl.ImageLayer'); goog.require('ol.renderer.webgl.Layer'); goog.require('ol.renderer.webgl.TileLayer'); +goog.require('ol.renderer.webgl.VectorLayer'); goog.require('ol.source.State'); goog.require('ol.structs.LRUCache'); goog.require('ol.structs.PriorityQueue'); @@ -250,6 +252,8 @@ ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) { return new ol.renderer.webgl.ImageLayer(this, layer); } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) { return new ol.renderer.webgl.TileLayer(this, layer); + } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) { + return new ol.renderer.webgl.VectorLayer(this, layer); } else { goog.asserts.fail(); return null; @@ -455,7 +459,7 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { layerState.sourceState == ol.source.State.READY) { layerRenderer = this.getLayerRenderer(layerState.layer); goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer); - if (layerRenderer.prepareFrame(frameState, layerState)) { + if (layerRenderer.prepareFrame(frameState, layerState, context)) { layerStatesToDraw.push(layerState); } } diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index b9cfb26b24..426f0fbc4e 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -107,11 +107,10 @@ ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() { * @inheritDoc */ ol.renderer.webgl.TileLayer.prototype.prepareFrame = - function(frameState, layerState) { + function(frameState, layerState, context) { var mapRenderer = this.getWebGLMapRenderer(); - var context = mapRenderer.getContext(); - var gl = mapRenderer.getGL(); + var gl = context.getGL(); var viewState = frameState.viewState; var projection = viewState.projection; diff --git a/src/ol/renderer/webgl/webglvectorlayer.glsl b/src/ol/renderer/webgl/webglvectorlayer.glsl new file mode 100644 index 0000000000..cc8aaf7dc2 --- /dev/null +++ b/src/ol/renderer/webgl/webglvectorlayer.glsl @@ -0,0 +1,23 @@ +//! NAMESPACE=ol.renderer.webgl.vectorlayer.shader +//! CLASS=ol.renderer.webgl.vectorlayer.shader. + + +//! COMMON + + +//! VERTEX +attribute vec2 a_position; + +uniform mat4 u_projectionMatrix; + +void main(void) { + gl_PointSize = 10.0; + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.); +} + + +//! FRAGMENT + +void main(void) { + gl_FragColor = vec4(1.0, 1.0, 0.0, 0.7); +} diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js new file mode 100644 index 0000000000..ef285cfde0 --- /dev/null +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -0,0 +1,266 @@ +goog.provide('ol.renderer.webgl.VectorLayer'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('ol.ViewHint'); +goog.require('ol.extent'); +goog.require('ol.layer.Vector'); +goog.require('ol.render.webgl.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.renderer.webgl.Layer'); +goog.require('ol.renderer.webgl.vectorlayer.shader'); +goog.require('ol.vec.Mat4'); + + + +/** + * @constructor + * @extends {ol.renderer.webgl.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.Vector} vectorLayer Vector layer. + */ +ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) { + + goog.base(this, mapRenderer, vectorLayer); + + /** + * @private + * @type {ol.webgl.shader.Fragment} + */ + this.fragmentShader_ = + ol.renderer.webgl.vectorlayer.shader.Fragment.getInstance(); + + /** + * @private + * @type {ol.webgl.shader.Vertex} + */ + this.vertexShader_ = + ol.renderer.webgl.vectorlayer.shader.Vertex.getInstance(); + + /** + * @private + * @type {ol.renderer.webgl.vectorlayer.shader.Locations} + */ + this.locations_ = null; + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = -1; + + /** + * @private + * @type {number} + */ + this.renderedResolution_ = NaN; + + /** + * @private + * @type {ol.Extent} + */ + this.renderedExtent_ = ol.extent.createEmpty(); + + /** + * @private + * @type {function(ol.Feature, ol.Feature): number|null} + */ + this.renderedRenderOrder_ = null; + + /** + * @private + * @type {ol.render.webgl.ReplayGroup} + */ + this.replayGroup_ = null; + +}; +goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.composeFrame = + function(frameState, layerState, context) { + + var gl = context.getGL(); + + var program = context.getProgram( + this.fragmentShader_, this.vertexShader_); + context.useProgram(program); + + if (goog.isNull(this.locations_)) { + this.locations_ = + new ol.renderer.webgl.vectorlayer.shader.Locations( + gl, program); + } + + var viewState = frameState.viewState; + ol.vec.Mat4.makeTransform2D(this.projectionMatrix, + 0.0, 0.0, + 2 / (viewState.resolution * frameState.size[0]), + 2 / (viewState.resolution * frameState.size[1]), + -viewState.rotation, + -viewState.center[0], -viewState.center[1]); + + var replayGroup = this.replayGroup_; + if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { + replayGroup.replay(context, + this.locations_.a_position, + this.locations_.u_projectionMatrix, + frameState.extent, frameState.pixelRatio, + this.projectionMatrix, + frameState.skippedFeatureUids); + } + +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtPixel = + function(coordinate, frameState, callback, thisArg) { +}; + + +/** + * Handle changes in image style state. + * @param {goog.events.Event} event Image style change event. + * @private + */ +ol.renderer.webgl.VectorLayer.prototype.handleImageChange_ = + function(event) { + this.renderIfReadyAndVisible(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.prepareFrame = + function(frameState, layerState, context) { + + var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer()); + goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector); + var vectorSource = vectorLayer.getSource(); + + this.updateAttributions( + frameState.attributions, vectorSource.getAttributions()); + this.updateLogos(frameState, vectorSource); + + if (!this.dirty_ && (frameState.viewHints[ol.ViewHint.ANIMATING] || + frameState.viewHints[ol.ViewHint.INTERACTING])) { + return true; + } + + var frameStateExtent = frameState.extent; + var viewState = frameState.viewState; + var projection = viewState.projection; + var resolution = viewState.resolution; + var pixelRatio = frameState.pixelRatio; + var vectorLayerRevision = vectorLayer.getRevision(); + var vectorLayerRenderOrder = vectorLayer.getRenderOrder(); + if (!goog.isDef(vectorLayerRenderOrder)) { + vectorLayerRenderOrder = ol.renderer.vector.defaultOrder; + } + + if (!this.dirty_ && + this.renderedResolution_ == resolution && + this.renderedRevision_ == vectorLayerRevision && + this.renderedRenderOrder_ == vectorLayerRenderOrder && + ol.extent.containsExtent(this.renderedExtent_, frameStateExtent)) { + return true; + } + + var extent = this.renderedExtent_; + var xBuffer = ol.extent.getWidth(frameStateExtent) / 4; + var yBuffer = ol.extent.getHeight(frameStateExtent) / 4; + extent[0] = frameStateExtent[0] - xBuffer; + extent[1] = frameStateExtent[1] - yBuffer; + extent[2] = frameStateExtent[2] + xBuffer; + extent[3] = frameStateExtent[3] + yBuffer; + + // FIXME dispose of old replayGroup in post render + goog.dispose(this.replayGroup_); + this.replayGroup_ = null; + + this.dirty_ = false; + + var replayGroup = new ol.render.webgl.ReplayGroup( + ol.renderer.vector.getTolerance(resolution, pixelRatio)); + vectorSource.loadFeatures(extent, resolution, projection); + var renderFeature = + /** + * @param {ol.Feature} feature Feature. + * @this {ol.renderer.webgl.VectorLayer} + */ + function(feature) { + var styles; + if (goog.isDef(feature.getStyleFunction())) { + styles = feature.getStyleFunction().call(feature, resolution); + } else if (goog.isDef(vectorLayer.getStyleFunction())) { + styles = vectorLayer.getStyleFunction()(feature, resolution); + } + if (goog.isDefAndNotNull(styles)) { + var dirty = this.renderFeature( + feature, resolution, pixelRatio, styles, replayGroup); + this.dirty_ = this.dirty_ || dirty; + } + }; + if (!goog.isNull(vectorLayerRenderOrder)) { + /** @type {Array.} */ + var features = []; + vectorSource.forEachFeatureInExtentAtResolution(extent, resolution, + /** + * @param {ol.Feature} feature Feature. + */ + function(feature) { + features.push(feature); + }, this); + goog.array.sort(features, vectorLayerRenderOrder); + goog.array.forEach(features, renderFeature, this); + } else { + vectorSource.forEachFeatureInExtentAtResolution( + extent, resolution, renderFeature, this); + } + replayGroup.finish(context); + + this.renderedResolution_ = resolution; + this.renderedRevision_ = vectorLayerRevision; + this.renderedRenderOrder_ = vectorLayerRenderOrder; + this.replayGroup_ = replayGroup; + + return true; +}; + + +/** + * @param {ol.Feature} feature Feature. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {Array.} styles Array of styles + * @param {ol.render.webgl.ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. + */ +ol.renderer.webgl.VectorLayer.prototype.renderFeature = + function(feature, resolution, pixelRatio, styles, replayGroup) { + if (!goog.isDefAndNotNull(styles)) { + return false; + } + var i, ii, loading = false; + for (i = 0, ii = styles.length; i < ii; ++i) { + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), + feature, this.handleImageChange_, this) || loading; + } + return loading; +}; diff --git a/src/ol/renderer/webgl/webglvectorlayershader.js b/src/ol/renderer/webgl/webglvectorlayershader.js new file mode 100644 index 0000000000..c5befe3852 --- /dev/null +++ b/src/ol/renderer/webgl/webglvectorlayershader.js @@ -0,0 +1,99 @@ +// This file is automatically generated, do not edit +goog.provide('ol.renderer.webgl.vectorlayer.shader'); + +goog.require('ol.webgl.shader'); + + + +/** + * @constructor + * @extends {ol.webgl.shader.Fragment} + * @struct + */ +ol.renderer.webgl.vectorlayer.shader.Fragment = function() { + goog.base(this, ol.renderer.webgl.vectorlayer.shader.Fragment.SOURCE); +}; +goog.inherits(ol.renderer.webgl.vectorlayer.shader.Fragment, ol.webgl.shader.Fragment); +goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Fragment); + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\n\nvoid main(void) {\n gl_FragColor = vec4(1.0, 1.0, 0.0, 0.7);\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;void main(void){gl_FragColor=vec4(1.0,1.0,0.0,0.7);}'; + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Fragment.SOURCE = goog.DEBUG ? + ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE : + ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @extends {ol.webgl.shader.Vertex} + * @struct + */ +ol.renderer.webgl.vectorlayer.shader.Vertex = function() { + goog.base(this, ol.renderer.webgl.vectorlayer.shader.Vertex.SOURCE); +}; +goog.inherits(ol.renderer.webgl.vectorlayer.shader.Vertex, ol.webgl.shader.Vertex); +goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Vertex); + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = '\n\nattribute vec2 a_position;\n\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n gl_PointSize = 10.0;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;uniform mat4 b;void main(void){gl_PointSize=10.0;gl_Position=b*vec4(a,0.,1.);}'; + + +/** + * @const + * @type {string} + */ +ol.renderer.webgl.vectorlayer.shader.Vertex.SOURCE = goog.DEBUG ? + ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE : + ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.renderer.webgl.vectorlayer.shader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_projectionMatrix' : 'b'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, goog.DEBUG ? 'a_position' : 'a'); +}; From 9f108391aefb253c13b6a2e9f70b94153ca6c55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 23 Oct 2014 15:24:54 +0200 Subject: [PATCH 02/98] Get renderer from query string in vector point examples --- examples/icon.js | 5 ++--- examples/synthetic-points.js | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/icon.js b/examples/icon.js index ab9cb41deb..5ffb2e1ad7 100644 --- a/examples/icon.js +++ b/examples/icon.js @@ -39,12 +39,11 @@ var vectorLayer = new ol.layer.Vector({ }); var rasterLayer = new ol.layer.Tile({ - source: new ol.source.TileJSON({ - url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.jsonp' - }) + source: new ol.source.OSM() }); var map = new ol.Map({ + renderer: exampleNS.getRendererFromQueryString(), layers: [rasterLayer, vectorLayer], target: document.getElementById('map'), view: new ol.View({ diff --git a/examples/synthetic-points.js b/examples/synthetic-points.js index 4381fd355c..d85a8f4a9c 100644 --- a/examples/synthetic-points.js +++ b/examples/synthetic-points.js @@ -51,6 +51,7 @@ var vector = new ol.layer.Vector({ }); var map = new ol.Map({ + renderer: exampleNS.getRendererFromQueryString(), layers: [vector], target: document.getElementById('map'), view: new ol.View({ From c8225e49b137653f49e1b8789ee14214122b031b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 23 Oct 2014 15:29:28 +0200 Subject: [PATCH 03/98] Use triangles to draw points with WebGL --- src/ol/render/webgl/webglreplay.js | 118 +++++++++++++----- src/ol/renderer/webgl/webglvectorlayer.glsl | 7 +- .../webgl/webglvectorlayerrenderer.js | 1 + .../renderer/webgl/webglvectorlayershader.js | 16 ++- 4 files changed, 105 insertions(+), 37 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 849a87e848..c86225faf7 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -21,13 +21,25 @@ ol.render.webgl.Replay = function(tolerance) { * @protected * @type {Array.} */ - this.coordinates = []; + this.vertices = []; + + /** + * @protected + * @type {Array.} + */ + this.indices = []; /** * @protected * @type {WebGLBuffer} */ - this.buffer = null; + this.verticesBuffer = null; + + /** + * @protected + * @type {WebGLBuffer} + */ + this.indicesBuffer = null; /** * @private @@ -44,22 +56,57 @@ ol.render.webgl.Replay = function(tolerance) { * @param {number} end End. * @param {number} stride Stride. * @param {boolean} close Close. - * @protected * @return {number} My end. + * @protected */ ol.render.webgl.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, close) { - var myEnd = this.coordinates.length; - var i; + var numIndices = this.indices.length; + var numVertices = this.vertices.length; + var i, x, y, n; + var oy = 0.05; + var ox = 0.01; for (i = offset; i < end; i += stride) { - this.coordinates[myEnd++] = flatCoordinates[i]; - this.coordinates[myEnd++] = flatCoordinates[i + 1]; + x = flatCoordinates[i]; + y = flatCoordinates[i + 1]; + + n = numVertices / 4; + + // create 4 vertices per coordinate + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = -ox; + this.vertices[numVertices++] = -oy; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = ox; + this.vertices[numVertices++] = -oy; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = ox; + this.vertices[numVertices++] = oy; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = -ox; + this.vertices[numVertices++] = oy; + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 3; } + if (close) { - this.coordinates[myEnd++] = flatCoordinates[offset]; - this.coordinates[myEnd++] = flatCoordinates[offset + 1]; + // FIXME + goog.asserts.fail(); } - return myEnd; + return numVertices; }; @@ -71,7 +118,8 @@ ol.render.webgl.Replay.prototype.finish = goog.nullFunction; /** * @param {ol.webgl.Context} context Context. - * @param {number} attribLocation Attribute location. + * @param {number} positionAttribLocation Attribute location for positions. + * @param {number} offsetsAttribLocation Attribute location for offsets. * @param {WebGLUniformLocation} projectionMatrixLocation Projection * matrix location. * @param {number} pixelRatio Pixel ratio. @@ -81,16 +129,23 @@ ol.render.webgl.Replay.prototype.finish = goog.nullFunction; * @template T */ ol.render.webgl.Replay.prototype.replay = - function(context, attribLocation, projectionMatrixLocation, - pixelRatio, transform, skippedFeaturesHash) { + function(context, positionAttribLocation, offsetsAttribLocation, + projectionMatrixLocation, pixelRatio, transform, + skippedFeaturesHash) { var gl = context.getGL(); - gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.buffer); - gl.uniformMatrix4fv(projectionMatrixLocation, false, - transform); - gl.enableVertexAttribArray(attribLocation); - gl.vertexAttribPointer(attribLocation, 2, goog.webgl.FLOAT, - false, 0, 0); - gl.drawArrays(goog.webgl.POINTS, 0, this.coordinates.length / 2); + + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); + gl.enableVertexAttribArray(positionAttribLocation); + gl.vertexAttribPointer(positionAttribLocation, 2, goog.webgl.FLOAT, + false, 16, 0); + gl.enableVertexAttribArray(offsetsAttribLocation); + gl.vertexAttribPointer(offsetsAttribLocation, 2, goog.webgl.FLOAT, + false, 16, 8); + + gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); + gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); + gl.drawElements(goog.webgl.TRIANGLES, this.indices.length, + goog.webgl.UNSIGNED_SHORT, 0); }; @@ -243,10 +298,14 @@ ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = */ ol.render.webgl.ImageReplay.prototype.finish = function(context) { var gl = context.getGL(); - this.buffer = gl.createBuffer(); - gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.buffer); + this.verticesBuffer = gl.createBuffer(); + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); gl.bufferData(goog.webgl.ARRAY_BUFFER, - new Float32Array(this.coordinates), goog.webgl.STATIC_DRAW); + new Float32Array(this.vertices), goog.webgl.STATIC_DRAW); + this.indicesBuffer = gl.createBuffer(); + gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); + gl.bufferData(goog.webgl.ELEMENT_ARRAY_BUFFER, + new Uint16Array(this.indices), goog.webgl.STATIC_DRAW); }; @@ -326,7 +385,8 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { /** * @param {ol.webgl.Context} context Context. - * @param {number} attribLocation Attribute location. + * @param {number} positionAttribLocation Attribute location for positions. + * @param {number} offsetsAttribLocation Attribute location for offsets. * @param {WebGLUniformLocation} projectionMatrixLocation Projection * matrix location. * @param {ol.Extent} extent Extent. @@ -337,16 +397,18 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { * @template T */ ol.render.webgl.ReplayGroup.prototype.replay = function( - context, attribLocation, projectionMatrixLocation, extent, - pixelRatio, transform, skippedFeaturesHash) { + context, positionAttribLocation, offsetsAttribLocation, + projectionMatrixLocation, extent, pixelRatio, transform, + 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) && ol.extent.intersects(extent, replay.getExtent())) { result = replay.replay( - context, attribLocation, projectionMatrixLocation, - pixelRatio, transform, skippedFeaturesHash); + context, positionAttribLocation, offsetsAttribLocation, + projectionMatrixLocation, pixelRatio, transform, + skippedFeaturesHash); if (result) { return result; } diff --git a/src/ol/renderer/webgl/webglvectorlayer.glsl b/src/ol/renderer/webgl/webglvectorlayer.glsl index cc8aaf7dc2..1154bf2fb3 100644 --- a/src/ol/renderer/webgl/webglvectorlayer.glsl +++ b/src/ol/renderer/webgl/webglvectorlayer.glsl @@ -4,20 +4,19 @@ //! COMMON - //! VERTEX attribute vec2 a_position; +attribute vec2 a_offsets; uniform mat4 u_projectionMatrix; void main(void) { - gl_PointSize = 10.0; - gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.); + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(a_offsets, 0., 0.); } //! FRAGMENT void main(void) { - gl_FragColor = vec4(1.0, 1.0, 0.0, 0.7); + gl_FragColor = vec4(1.0, 1.0, 0.0, 1); } diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index ef285cfde0..a85b6d1efc 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -114,6 +114,7 @@ ol.renderer.webgl.VectorLayer.prototype.composeFrame = if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { replayGroup.replay(context, this.locations_.a_position, + this.locations_.a_offsets, this.locations_.u_projectionMatrix, frameState.extent, frameState.pixelRatio, this.projectionMatrix, diff --git a/src/ol/renderer/webgl/webglvectorlayershader.js b/src/ol/renderer/webgl/webglvectorlayershader.js index c5befe3852..4559a8a4fe 100644 --- a/src/ol/renderer/webgl/webglvectorlayershader.js +++ b/src/ol/renderer/webgl/webglvectorlayershader.js @@ -21,14 +21,14 @@ goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Fragment); * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\n\nvoid main(void) {\n gl_FragColor = vec4(1.0, 1.0, 0.0, 0.7);\n}\n'; +ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\nvoid main(void) {\n gl_FragColor = vec4(1.0, 1.0, 0.0, 1);\n}\n'; /** * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;void main(void){gl_FragColor=vec4(1.0,1.0,0.0,0.7);}'; +ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;void main(void){gl_FragColor=vec4(1.0,1.0,0.0,1);}'; /** @@ -57,14 +57,14 @@ goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Vertex); * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = '\n\nattribute vec2 a_position;\n\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n gl_PointSize = 10.0;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n}\n\n\n'; +ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = '\nattribute vec2 a_position;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(a_offsets, 0., 0.);\n}\n\n\n'; /** * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;uniform mat4 b;void main(void){gl_PointSize=10.0;gl_Position=b*vec4(a,0.,1.);}'; +ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;attribute vec2 b;uniform mat4 c;void main(void){gl_Position=c*vec4(a,0.,1.)+vec4(b,0.,0.);}'; /** @@ -89,7 +89,13 @@ ol.renderer.webgl.vectorlayer.shader.Locations = function(gl, program) { * @type {WebGLUniformLocation} */ this.u_projectionMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_projectionMatrix' : 'b'); + program, goog.DEBUG ? 'u_projectionMatrix' : 'c'); + + /** + * @type {number} + */ + this.a_offsets = gl.getAttribLocation( + program, goog.DEBUG ? 'a_offsets' : 'b'); /** * @type {number} From 2ecd2eadf70364f2b2b70f505cfa2bc147a03fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 23 Oct 2014 16:39:33 +0200 Subject: [PATCH 04/98] Remove snap code from synthetic-points example --- examples/synthetic-points.js | 58 ------------------------------------ 1 file changed, 58 deletions(-) diff --git a/examples/synthetic-points.js b/examples/synthetic-points.js index d85a8f4a9c..447cc7d930 100644 --- a/examples/synthetic-points.js +++ b/examples/synthetic-points.js @@ -1,7 +1,6 @@ goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.View'); -goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); @@ -60,63 +59,6 @@ var map = new ol.Map({ }) }); -var point = null; -var line = null; -var displaySnap = function(coordinate) { - var closestFeature = vectorSource.getClosestFeatureToCoordinate(coordinate); - if (closestFeature === null) { - point = null; - line = null; - } else { - var geometry = closestFeature.getGeometry(); - var closestPoint = geometry.getClosestPoint(coordinate); - if (point === null) { - point = new ol.geom.Point(closestPoint); - } else { - point.setCoordinates(closestPoint); - } - if (line === null) { - line = new ol.geom.LineString([coordinate, closestPoint]); - } else { - line.setCoordinates([coordinate, closestPoint]); - } - } - map.render(); -}; - -$(map.getViewport()).on('mousemove', function(evt) { - var coordinate = map.getEventCoordinate(evt.originalEvent); - displaySnap(coordinate); -}); - -map.on('click', function(evt) { - displaySnap(evt.coordinate); -}); - -var imageStyle = new ol.style.Circle({ - radius: 10, - fill: null, - stroke: new ol.style.Stroke({ - color: 'rgba(255,255,0,0.9)', - width: 3 - }) -}); -var strokeStyle = new ol.style.Stroke({ - color: 'rgba(255,255,0,0.9)', - width: 3 -}); -map.on('postcompose', function(evt) { - var vectorContext = evt.vectorContext; - if (point !== null) { - vectorContext.setImageStyle(imageStyle); - vectorContext.drawPointGeometry(point); - } - if (line !== null) { - vectorContext.setFillStrokeStyle(null, strokeStyle); - vectorContext.drawLineStringGeometry(line); - } -}); - $(map.getViewport()).on('mousemove', function(e) { var pixel = map.getEventPixel(e.originalEvent); From bbea205a9ce8f76f4b415f6ca19d5f8e05448398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 24 Oct 2014 16:57:21 +0200 Subject: [PATCH 05/98] Use texture to draw points with WebGL --- examples/synthetic-points.js | 2 +- src/ol/render/webgl/webglreplay.js | 219 +++++++++++------- src/ol/renderer/webgl/webglvectorlayer.glsl | 10 +- .../webgl/webglvectorlayerrenderer.js | 4 +- .../renderer/webgl/webglvectorlayershader.js | 32 ++- 5 files changed, 174 insertions(+), 93 deletions(-) diff --git a/examples/synthetic-points.js b/examples/synthetic-points.js index 447cc7d930..c26a5615b8 100644 --- a/examples/synthetic-points.js +++ b/examples/synthetic-points.js @@ -18,7 +18,7 @@ for (var i = 0; i < count; ++i) { 'geometry': new ol.geom.Point( [2 * e * Math.random() - e, 2 * e * Math.random() - e]), 'i': i, - 'size': i % 2 ? 10 : 20 + 'size': 20 }); } diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index c86225faf7..b977824bad 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -41,6 +41,12 @@ ol.render.webgl.Replay = function(tolerance) { */ this.indicesBuffer = null; + /** + * @protected + * @type {WebGLTexture} + */ + this.texture = null; + /** * @private * @type {ol.Extent} @@ -50,66 +56,6 @@ ol.render.webgl.Replay = function(tolerance) { }; -/** - * @param {Array.} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @param {boolean} close Close. - * @return {number} My end. - * @protected - */ -ol.render.webgl.Replay.prototype.appendFlatCoordinates = - function(flatCoordinates, offset, end, stride, close) { - var numIndices = this.indices.length; - var numVertices = this.vertices.length; - var i, x, y, n; - var oy = 0.05; - var ox = 0.01; - for (i = offset; i < end; i += stride) { - x = flatCoordinates[i]; - y = flatCoordinates[i + 1]; - - n = numVertices / 4; - - // create 4 vertices per coordinate - - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -ox; - this.vertices[numVertices++] = -oy; - - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = ox; - this.vertices[numVertices++] = -oy; - - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = ox; - this.vertices[numVertices++] = oy; - - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -ox; - this.vertices[numVertices++] = oy; - - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n + 3; - } - - if (close) { - // FIXME - goog.asserts.fail(); - } - return numVertices; -}; - - /** * @param {ol.webgl.Context} context Context. */ @@ -120,30 +66,43 @@ ol.render.webgl.Replay.prototype.finish = goog.nullFunction; * @param {ol.webgl.Context} context Context. * @param {number} positionAttribLocation Attribute location for positions. * @param {number} offsetsAttribLocation Attribute location for offsets. - * @param {WebGLUniformLocation} projectionMatrixLocation Projection - * matrix location. + * @param {number} texCoordAttribLocation Attribute location for texCoord. + * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. + * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. * @param {number} pixelRatio Pixel ratio. + * @param {Array.} size Size. * @param {goog.vec.Mat4.Number} transform Transform. * @param {Object} skippedFeaturesHash Ids of features to skip. * @return {T|undefined} Callback result. * @template T */ -ol.render.webgl.Replay.prototype.replay = - function(context, positionAttribLocation, offsetsAttribLocation, - projectionMatrixLocation, pixelRatio, transform, - skippedFeaturesHash) { +ol.render.webgl.Replay.prototype.replay = function(context, + positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, + projectionMatrixLocation, sizeMatrixLocation, + pixelRatio, size, transform, skippedFeaturesHash) { var gl = context.getGL(); gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); + gl.enableVertexAttribArray(positionAttribLocation); gl.vertexAttribPointer(positionAttribLocation, 2, goog.webgl.FLOAT, - false, 16, 0); + false, 24, 0); + gl.enableVertexAttribArray(offsetsAttribLocation); gl.vertexAttribPointer(offsetsAttribLocation, 2, goog.webgl.FLOAT, - false, 16, 8); + false, 24, 8); + + gl.enableVertexAttribArray(texCoordAttribLocation); + gl.vertexAttribPointer(texCoordAttribLocation, 2, goog.webgl.FLOAT, + false, 24, 16); + + gl.bindTexture(goog.webgl.TEXTURE_2D, this.texture); + + gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); + gl.uniformMatrix2fv(sizeMatrixLocation, false, + new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); - gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); gl.drawElements(goog.webgl.TRIANGLES, this.indices.length, goog.webgl.UNSIGNED_SHORT, 0); }; @@ -248,6 +207,24 @@ ol.render.webgl.ImageReplay = function(tolerance) { goog.base(this, tolerance); + /** + * @private + * @type {number|undefined} + */ + this.height_ = undefined; + + /** + * @private + * @type {HTMLCanvasElement|HTMLVideoElement|Image} + */ + this.image_ = null; + + /** + * @private + * @type {number|undefined} + */ + this.width_ = undefined; + }; goog.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); @@ -257,13 +234,63 @@ goog.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); * @param {number} offset Offset. * @param {number} end End. * @param {number} stride Stride. - * @private * @return {number} My end. + * @private */ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { - return this.appendFlatCoordinates( - flatCoordinates, offset, end, stride, false); + goog.asserts.assert(goog.isDef(this.width_)); + goog.asserts.assert(goog.isDef(this.height_)); + var numIndices = this.indices.length; + var numVertices = this.vertices.length; + var i, x, y, n; + var ox = this.width_; + var oy = this.height_; + for (i = offset; i < end; i += stride) { + x = flatCoordinates[i]; + y = flatCoordinates[i + 1]; + + n = numVertices / 6; + + // create 4 vertices per coordinate + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = -ox; + this.vertices[numVertices++] = -oy; + this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = 1; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = ox; + this.vertices[numVertices++] = -oy; + this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = 1; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = ox; + this.vertices[numVertices++] = oy; + this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = 0; + + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + this.vertices[numVertices++] = -ox; + this.vertices[numVertices++] = oy; + this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = 0; + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 3; + } + + return numVertices; }; @@ -306,6 +333,23 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); gl.bufferData(goog.webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), goog.webgl.STATIC_DRAW); + + this.texture = gl.createTexture(); + gl.bindTexture(goog.webgl.TEXTURE_2D, this.texture); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_S, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.NEAREST); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, + goog.webgl.UNSIGNED_BYTE, this.image_); + + this.image_ = null; + this.width_ = undefined; + this.height_ = undefined; }; @@ -321,6 +365,15 @@ ol.render.webgl.Replay.prototype.getExtent = function() { * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { + if (goog.isNull(this.image_)) { + var image = imageStyle.getImage(1); + goog.asserts.assert(!goog.isNull(image)); + var size = imageStyle.getSize(); + goog.asserts.assert(!goog.isNull(size)); + this.image_ = image; + this.width_ = size[0]; + this.height_ = size[1]; + } }; @@ -387,28 +440,30 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { * @param {ol.webgl.Context} context Context. * @param {number} positionAttribLocation Attribute location for positions. * @param {number} offsetsAttribLocation Attribute location for offsets. - * @param {WebGLUniformLocation} projectionMatrixLocation Projection - * matrix location. + * @param {number} texCoordAttribLocation Attribute location for texCoord. + * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. + * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. * @param {ol.Extent} extent Extent. * @param {number} pixelRatio Pixel ratio. + * @param {Array.} size Size. * @param {goog.vec.Mat4.Number} transform Transform. * @param {Object} skippedFeaturesHash Ids of features to skip. * @return {T|undefined} Callback result. * @template T */ -ol.render.webgl.ReplayGroup.prototype.replay = function( - context, positionAttribLocation, offsetsAttribLocation, - projectionMatrixLocation, extent, pixelRatio, transform, - skippedFeaturesHash) { +ol.render.webgl.ReplayGroup.prototype.replay = function(context, + positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, + projectionMatrixLocation, sizeMatrixLocation, + extent, pixelRatio, size, transform, 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) && ol.extent.intersects(extent, replay.getExtent())) { - result = replay.replay( - context, positionAttribLocation, offsetsAttribLocation, - projectionMatrixLocation, pixelRatio, transform, - skippedFeaturesHash); + result = replay.replay(context, + positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, + projectionMatrixLocation, sizeMatrixLocation, + pixelRatio, size, transform, skippedFeaturesHash); if (result) { return result; } diff --git a/src/ol/renderer/webgl/webglvectorlayer.glsl b/src/ol/renderer/webgl/webglvectorlayer.glsl index 1154bf2fb3..53e57efbda 100644 --- a/src/ol/renderer/webgl/webglvectorlayer.glsl +++ b/src/ol/renderer/webgl/webglvectorlayer.glsl @@ -3,20 +3,26 @@ //! COMMON +varying vec2 v_texCoord; //! VERTEX attribute vec2 a_position; +attribute vec2 a_texCoord; attribute vec2 a_offsets; uniform mat4 u_projectionMatrix; +uniform mat2 u_sizeMatrix; void main(void) { - gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(a_offsets, 0., 0.); + vec2 offsets = u_sizeMatrix * a_offsets; + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.); + v_texCoord = a_texCoord; } //! FRAGMENT +uniform sampler2D u_image; void main(void) { - gl_FragColor = vec4(1.0, 1.0, 0.0, 1); + gl_FragColor = texture2D(u_image, v_texCoord); } diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index a85b6d1efc..cb67b0174b 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -115,8 +115,10 @@ ol.renderer.webgl.VectorLayer.prototype.composeFrame = replayGroup.replay(context, this.locations_.a_position, this.locations_.a_offsets, + this.locations_.a_texCoord, this.locations_.u_projectionMatrix, - frameState.extent, frameState.pixelRatio, + this.locations_.u_sizeMatrix, + frameState.extent, frameState.pixelRatio, frameState.size, this.projectionMatrix, frameState.skippedFeatureUids); } diff --git a/src/ol/renderer/webgl/webglvectorlayershader.js b/src/ol/renderer/webgl/webglvectorlayershader.js index 4559a8a4fe..f3226c8963 100644 --- a/src/ol/renderer/webgl/webglvectorlayershader.js +++ b/src/ol/renderer/webgl/webglvectorlayershader.js @@ -21,14 +21,14 @@ goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Fragment); * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\nvoid main(void) {\n gl_FragColor = vec4(1.0, 1.0, 0.0, 1);\n}\n'; +ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\nuniform sampler2D u_image;\n\nvoid main(void) {\n gl_FragColor = texture2D(u_image, v_texCoord);\n}\n'; /** * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;void main(void){gl_FragColor=vec4(1.0,1.0,0.0,1);}'; +ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D g;void main(void){gl_FragColor=texture2D(g,a);}'; /** @@ -57,14 +57,14 @@ goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Vertex); * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = '\nattribute vec2 a_position;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(a_offsets, 0., 0.);\n}\n\n\n'; +ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n}\n\n\n'; /** * @const * @type {string} */ -ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;attribute vec2 b;uniform mat4 c;void main(void){gl_Position=c*vec4(a,0.,1.)+vec4(b,0.,0.);}'; +ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;attribute vec2 d;uniform mat4 e;uniform mat2 f;void main(void){vec2 offsets=f*d;gl_Position=e*vec4(b,0.,1.)+vec4(offsets,0.,0.);a=c;}'; /** @@ -85,21 +85,39 @@ ol.renderer.webgl.vectorlayer.shader.Vertex.SOURCE = goog.DEBUG ? */ ol.renderer.webgl.vectorlayer.shader.Locations = function(gl, program) { + /** + * @type {WebGLUniformLocation} + */ + this.u_image = gl.getUniformLocation( + program, goog.DEBUG ? 'u_image' : 'g'); + /** * @type {WebGLUniformLocation} */ this.u_projectionMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_projectionMatrix' : 'c'); + program, goog.DEBUG ? 'u_projectionMatrix' : 'e'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_sizeMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_sizeMatrix' : 'f'); /** * @type {number} */ this.a_offsets = gl.getAttribLocation( - program, goog.DEBUG ? 'a_offsets' : 'b'); + program, goog.DEBUG ? 'a_offsets' : 'd'); /** * @type {number} */ this.a_position = gl.getAttribLocation( - program, goog.DEBUG ? 'a_position' : 'a'); + program, goog.DEBUG ? 'a_position' : 'b'); + + /** + * @type {number} + */ + this.a_texCoord = gl.getAttribLocation( + program, goog.DEBUG ? 'a_texCoord' : 'c'); }; From 920131273798bb9fc54ed3b82ac0a3a5272206de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 31 Oct 2014 11:16:51 +0100 Subject: [PATCH 06/98] Add support for icon sprites --- src/ol/render/webgl/webglreplay.js | 187 +++++++++++++++++++++-------- 1 file changed, 135 insertions(+), 52 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index b977824bad..a1b4ad9e2d 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -41,18 +41,18 @@ ol.render.webgl.Replay = function(tolerance) { */ this.indicesBuffer = null; - /** - * @protected - * @type {WebGLTexture} - */ - this.texture = null; - /** * @private * @type {ol.Extent} */ this.extent_ = ol.extent.createEmpty(); + /** + * @protected + * @type {Array.>} + */ + this.textures = []; + }; @@ -96,15 +96,23 @@ ol.render.webgl.Replay.prototype.replay = function(context, gl.vertexAttribPointer(texCoordAttribLocation, 2, goog.webgl.FLOAT, false, 24, 16); - gl.bindTexture(goog.webgl.TEXTURE_2D, this.texture); gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); gl.uniformMatrix2fv(sizeMatrixLocation, false, new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); - gl.drawElements(goog.webgl.TRIANGLES, this.indices.length, - goog.webgl.UNSIGNED_SHORT, 0); + + var i; + var ii = this.textures.length; + var texture; + for (i = 0; i < ii; ++i) { + texture = this.textures[i]; + gl.bindTexture(goog.webgl.TEXTURE_2D, + /** @type {WebGLTexture} */ (texture[0])); + gl.drawElements(goog.webgl.TRIANGLES, /** @type {number} */ (texture[1]), + goog.webgl.UNSIGNED_SHORT, 0); + } }; @@ -215,9 +223,33 @@ ol.render.webgl.ImageReplay = function(tolerance) { /** * @private - * @type {HTMLCanvasElement|HTMLVideoElement|Image} + * @type {Array.>} */ - this.image_ = null; + this.images_ = []; + + /** + * @private + * @type {number|undefined} + */ + this.imageHeight_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.imageWidth_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.originX_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.originY_ = undefined; /** * @private @@ -239,13 +271,21 @@ goog.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); */ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { - goog.asserts.assert(goog.isDef(this.width_)); goog.asserts.assert(goog.isDef(this.height_)); + goog.asserts.assert(goog.isDef(this.imageHeight_)); + goog.asserts.assert(goog.isDef(this.imageWidth_)); + goog.asserts.assert(goog.isDef(this.originX_)); + goog.asserts.assert(goog.isDef(this.originY_)); + goog.asserts.assert(goog.isDef(this.width_)); + var height = this.height_; + var imageHeight = this.imageHeight_; + var imageWidth = this.imageWidth_; + var originX = this.originX_; + var originY = this.originY_; + var width = this.width_; var numIndices = this.indices.length; var numVertices = this.vertices.length; var i, x, y, n; - var ox = this.width_; - var oy = this.height_; for (i = offset; i < end; i += stride) { x = flatCoordinates[i]; y = flatCoordinates[i + 1]; @@ -256,31 +296,31 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices[numVertices++] = x; this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -ox; - this.vertices[numVertices++] = -oy; - this.vertices[numVertices++] = 1; - this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = -width; + this.vertices[numVertices++] = -height; + this.vertices[numVertices++] = (originX + width) / imageWidth; + this.vertices[numVertices++] = (originY + height) / imageHeight; this.vertices[numVertices++] = x; this.vertices[numVertices++] = y; - this.vertices[numVertices++] = ox; - this.vertices[numVertices++] = -oy; - this.vertices[numVertices++] = 0; - this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = width; + this.vertices[numVertices++] = -height; + this.vertices[numVertices++] = originX / imageWidth; + this.vertices[numVertices++] = (originY + height) / imageHeight; this.vertices[numVertices++] = x; this.vertices[numVertices++] = y; - this.vertices[numVertices++] = ox; - this.vertices[numVertices++] = oy; - this.vertices[numVertices++] = 0; - this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = width; + this.vertices[numVertices++] = height; + this.vertices[numVertices++] = originX / imageWidth; + this.vertices[numVertices++] = originY / imageHeight; this.vertices[numVertices++] = x; this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -ox; - this.vertices[numVertices++] = oy; - this.vertices[numVertices++] = 1; - this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = -width; + this.vertices[numVertices++] = height; + this.vertices[numVertices++] = (originX + width) / imageWidth; + this.vertices[numVertices++] = originY / imageHeight; this.indices[numIndices++] = n; this.indices[numIndices++] = n + 1; @@ -325,6 +365,12 @@ ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = */ ol.render.webgl.ImageReplay.prototype.finish = function(context) { var gl = context.getGL(); + + goog.asserts.assert(this.images_.length > 0); + var current = this.images_[this.images_.length - 1]; + goog.asserts.assert(!goog.isDef(current[1])); + current[1] = this.indices.length; + this.verticesBuffer = gl.createBuffer(); gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); gl.bufferData(goog.webgl.ARRAY_BUFFER, @@ -334,22 +380,38 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { gl.bufferData(goog.webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), goog.webgl.STATIC_DRAW); - this.texture = gl.createTexture(); - gl.bindTexture(goog.webgl.TEXTURE_2D, this.texture); - gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_WRAP_S, goog.webgl.CLAMP_TO_EDGE); - gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); - gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.NEAREST); - gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); - gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, - goog.webgl.UNSIGNED_BYTE, this.image_); + goog.asserts.assert(this.textures.length === 0); + + var i; + var ii = this.images_.length; + var texture; + for (i = 0; i < ii; ++i) { + var image = + /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ + (this.images_[i][0]); + current = this.images_[i]; + texture = gl.createTexture(); + gl.bindTexture(goog.webgl.TEXTURE_2D, texture); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_S, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.NEAREST); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, + goog.webgl.UNSIGNED_BYTE, image); + this.textures[i] = [texture, this.images_[i][1]]; + } - this.image_ = null; - this.width_ = undefined; this.height_ = undefined; + this.images_.length = 0; + this.imageHeight_ = undefined; + this.imageWidth_ = undefined; + this.originX_ = undefined; + this.originY_ = undefined; + this.width_ = undefined; }; @@ -365,15 +427,36 @@ ol.render.webgl.Replay.prototype.getExtent = function() { * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { - if (goog.isNull(this.image_)) { - var image = imageStyle.getImage(1); - goog.asserts.assert(!goog.isNull(image)); - var size = imageStyle.getSize(); - goog.asserts.assert(!goog.isNull(size)); - this.image_ = image; - this.width_ = size[0]; - this.height_ = size[1]; + var image = imageStyle.getImage(1); + goog.asserts.assert(!goog.isNull(image)); + // FIXME getImageSize does not exist for circles + var imageSize = imageStyle.getImageSize(); + goog.asserts.assert(!goog.isNull(imageSize)); + var origin = imageStyle.getOrigin(); + goog.asserts.assert(!goog.isNull(origin)); + var size = imageStyle.getSize(); + goog.asserts.assert(!goog.isNull(size)); + + if (this.images_.length === 0) { + this.images_.push([image, undefined]); + } else { + var current = this.images_[this.images_.length - 1]; + var currentImage = + /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ + (current[0]); + goog.asserts.assert(!goog.isDef(current[1])); + if (goog.getUid(currentImage) != goog.getUid(image)) { + current[1] = this.indices.length; + this.images_.push([image, undefined]); + } } + + this.height_ = size[1]; + this.imageHeight_ = imageSize[1]; + this.imageWidth_ = imageSize[0]; + this.originX_ = origin[0]; + this.originY_ = origin[1]; + this.width_ = size[0]; }; From 46ec0785012c23bd1129bf4664358d613e5ce344 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Fri, 31 Oct 2014 11:17:03 +0100 Subject: [PATCH 07/98] Add Hashable interface --- src/ol/structs/hashable.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/ol/structs/hashable.js diff --git a/src/ol/structs/hashable.js b/src/ol/structs/hashable.js new file mode 100644 index 0000000000..371beafa6c --- /dev/null +++ b/src/ol/structs/hashable.js @@ -0,0 +1,16 @@ +goog.provide('ol.structs.IHashable'); + + + +/** + * @interface + */ +ol.structs.IHashable = function() { +}; + + +/** + * @return {number} The hash code. + */ +ol.structs.IHashable.prototype.hashCode = function() { +}; From f332cdacf145f20f99f23872cdcd5f781313801b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 31 Oct 2014 11:17:24 +0100 Subject: [PATCH 08/98] Add an icon sprite webgl example --- examples/data/Butterfly.png | Bin 0 -> 64098 bytes examples/icon-sprite-webgl.html | 52 +++++++++++++++++++++++++++ examples/icon-sprite-webgl.js | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 examples/data/Butterfly.png create mode 100644 examples/icon-sprite-webgl.html create mode 100644 examples/icon-sprite-webgl.js diff --git a/examples/data/Butterfly.png b/examples/data/Butterfly.png new file mode 100644 index 0000000000000000000000000000000000000000..ef37aaad4d3f1637bbbebcfa583e750be6a68e63 GIT binary patch literal 64098 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�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 + + + + + + + + + + Icon sprites with WebGL example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Icon sprite with WebGL example

+

Icon sprite with WebGL.

+
+

See the icon-sprite-webgl.js source to see how this is done.

+
+
webgl, icon, sprite, vector, point
+
+ +
+ +
+ + + + + + + + diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js new file mode 100644 index 0000000000..6b8d24bbf0 --- /dev/null +++ b/examples/icon-sprite-webgl.js @@ -0,0 +1,60 @@ +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.geom.Point'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Icon'); +goog.require('ol.style.Style'); + + +var iconInfo = [ + {size: [55, 55], offset: [0, 0]}, + {size: [55, 55], offset: [110, 86]}, + {size: [55, 86], offset: [55, 0]} +]; + +var i; + +var iconCount = iconInfo.length; +var icons = new Array(iconCount); +for (i = 0; i < iconCount; ++i) { + icons[i] = new ol.style.Icon({ + src: 'data/Butterfly.png', + size: iconInfo[i].size, + offset: iconInfo[i].offset + }); +} + +var featureCount = 10000; +var features = new Array(featureCount); +var feature, geometry; +var e = 25000000; +for (i = 0; i < featureCount; ++i) { + geometry = new ol.geom.Point( + [2 * e * Math.random() - e, 2 * e * Math.random() - e]); + feature = new ol.Feature(geometry); + feature.setStyle( + new ol.style.Style({ + image: icons[i % iconCount] + }) + ); + features[i] = feature; +} + +var vectorSource = new ol.source.Vector({ + features: features +}); +var vector = new ol.layer.Vector({ + source: vectorSource +}); + +var map = new ol.Map({ + renderer: 'webgl', + layers: [vector], + target: document.getElementById('map'), + view: new ol.View({ + center: [0, 0], + zoom: 5 + }) +}); From 8bff6a1abe117cfbab0344dd273d8db1eb67d406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 31 Oct 2014 15:23:50 +0100 Subject: [PATCH 09/98] =?UTF-8?q?Flatten=20the=20WebGL=C2=A0replay=20class?= =?UTF-8?q?=20hierarchy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ol/render/webgl/webglreplay.js | 470 +++++++++++++---------------- 1 file changed, 216 insertions(+), 254 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index a1b4ad9e2d..75ae117f53 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -15,250 +15,93 @@ goog.require('ol.render.IReplayGroup'); * @protected * @struct */ -ol.render.webgl.Replay = function(tolerance) { +ol.render.webgl.ImageReplay = function(tolerance) { /** - * @protected - * @type {Array.} - */ - this.vertices = []; - - /** - * @protected - * @type {Array.} - */ - this.indices = []; - - /** - * @protected - * @type {WebGLBuffer} - */ - this.verticesBuffer = null; - - /** - * @protected - * @type {WebGLBuffer} - */ - this.indicesBuffer = null; - - /** - * @private * @type {ol.Extent} + * @private */ this.extent_ = ol.extent.createEmpty(); /** - * @protected - * @type {Array.>} - */ - this.textures = []; - -}; - - -/** - * @param {ol.webgl.Context} context Context. - */ -ol.render.webgl.Replay.prototype.finish = goog.nullFunction; - - -/** - * @param {ol.webgl.Context} context Context. - * @param {number} positionAttribLocation Attribute location for positions. - * @param {number} offsetsAttribLocation Attribute location for offsets. - * @param {number} texCoordAttribLocation Attribute location for texCoord. - * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. - * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. - * @param {number} pixelRatio Pixel ratio. - * @param {Array.} size Size. - * @param {goog.vec.Mat4.Number} transform Transform. - * @param {Object} skippedFeaturesHash Ids of features to skip. - * @return {T|undefined} Callback result. - * @template T - */ -ol.render.webgl.Replay.prototype.replay = function(context, - positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, - projectionMatrixLocation, sizeMatrixLocation, - pixelRatio, size, transform, skippedFeaturesHash) { - var gl = context.getGL(); - - gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); - - gl.enableVertexAttribArray(positionAttribLocation); - gl.vertexAttribPointer(positionAttribLocation, 2, goog.webgl.FLOAT, - false, 24, 0); - - gl.enableVertexAttribArray(offsetsAttribLocation); - gl.vertexAttribPointer(offsetsAttribLocation, 2, goog.webgl.FLOAT, - false, 24, 8); - - gl.enableVertexAttribArray(texCoordAttribLocation); - gl.vertexAttribPointer(texCoordAttribLocation, 2, goog.webgl.FLOAT, - false, 24, 16); - - - gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); - gl.uniformMatrix2fv(sizeMatrixLocation, false, - new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); - - gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); - - var i; - var ii = this.textures.length; - var texture; - for (i = 0; i < ii; ++i) { - texture = this.textures[i]; - gl.bindTexture(goog.webgl.TEXTURE_2D, - /** @type {WebGLTexture} */ (texture[0])); - gl.drawElements(goog.webgl.TRIANGLES, /** @type {number} */ (texture[1]), - goog.webgl.UNSIGNED_SHORT, 0); - } -}; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawAsync = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawCircleGeometry = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawFeature = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawGeometryCollectionGeometry = - goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawLineStringGeometry = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawMultiLineStringGeometry = - goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawPointGeometry = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawMultiPointGeometry = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawPolygonGeometry = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawMultiPolygonGeometry = - goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.drawText = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.setFillStrokeStyle = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.setImageStyle = goog.abstractMethod; - - -/** - * @inheritDoc - */ -ol.render.webgl.Replay.prototype.setTextStyle = goog.abstractMethod; - - - -/** - * @constructor - * @extends {ol.render.webgl.Replay} - * @param {number} tolerance Tolerance. - * @protected - * @struct - */ -ol.render.webgl.ImageReplay = function(tolerance) { - - goog.base(this, tolerance); - - /** - * @private * @type {number|undefined} + * @private */ this.height_ = undefined; /** - * @private * @type {Array.>} + * @private */ this.images_ = []; /** - * @private * @type {number|undefined} + * @private */ this.imageHeight_ = undefined; /** - * @private * @type {number|undefined} + * @private */ this.imageWidth_ = undefined; /** + * @type {Array.} * @private + */ + this.indices_ = []; + + /** + * @type {WebGLBuffer} + * @private + */ + this.indicesBuffer_ = null; + + /** * @type {number|undefined} + * @private */ this.originX_ = undefined; /** - * @private * @type {number|undefined} + * @private */ this.originY_ = undefined; /** + * @type {Array.} * @private + */ + this.vertices_ = []; + + /** + * @type {WebGLBuffer} + * @private + */ + this.verticesBuffer_ = null; + + /** * @type {number|undefined} + * @private */ this.width_ = undefined; + /** + * @type {Array.>} + * @private + */ + this.textures_ = []; + }; -goog.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawAsync = goog.abstractMethod; /** @@ -283,8 +126,8 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var originX = this.originX_; var originY = this.originY_; var width = this.width_; - var numIndices = this.indices.length; - var numVertices = this.vertices.length; + var numIndices = this.indices_.length; + var numVertices = this.vertices_.length; var i, x, y, n; for (i = offset; i < end; i += stride) { x = flatCoordinates[i]; @@ -294,46 +137,99 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = // create 4 vertices per coordinate - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -width; - this.vertices[numVertices++] = -height; - this.vertices[numVertices++] = (originX + width) / imageWidth; - this.vertices[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = -width; + this.vertices_[numVertices++] = -height; + this.vertices_[numVertices++] = (originX + width) / imageWidth; + this.vertices_[numVertices++] = (originY + height) / imageHeight; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = width; - this.vertices[numVertices++] = -height; - this.vertices[numVertices++] = originX / imageWidth; - this.vertices[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = width; + this.vertices_[numVertices++] = -height; + this.vertices_[numVertices++] = originX / imageWidth; + this.vertices_[numVertices++] = (originY + height) / imageHeight; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = width; - this.vertices[numVertices++] = height; - this.vertices[numVertices++] = originX / imageWidth; - this.vertices[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = width; + this.vertices_[numVertices++] = height; + this.vertices_[numVertices++] = originX / imageWidth; + this.vertices_[numVertices++] = originY / imageHeight; - this.vertices[numVertices++] = x; - this.vertices[numVertices++] = y; - this.vertices[numVertices++] = -width; - this.vertices[numVertices++] = height; - this.vertices[numVertices++] = (originX + width) / imageWidth; - this.vertices[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = -width; + this.vertices_[numVertices++] = height; + this.vertices_[numVertices++] = (originX + width) / imageWidth; + this.vertices_[numVertices++] = originY / imageHeight; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 1; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n; - this.indices[numIndices++] = n + 2; - this.indices[numIndices++] = n + 3; + this.indices_[numIndices++] = n; + this.indices_[numIndices++] = n + 1; + this.indices_[numIndices++] = n + 2; + this.indices_[numIndices++] = n; + this.indices_[numIndices++] = n + 2; + this.indices_[numIndices++] = n + 3; } return numVertices; }; +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawCircleGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawFeature = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawGeometryCollectionGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawLineStringGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiLineStringGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = + function(multiPointGeometry, data) { + ol.extent.extend(this.extent_, multiPointGeometry.getExtent()); + var flatCoordinates = multiPointGeometry.getFlatCoordinates(); + var stride = multiPointGeometry.getStride(); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiPolygonGeometry = + goog.abstractMethod; + + /** * @inheritDoc */ @@ -350,14 +246,13 @@ ol.render.webgl.ImageReplay.prototype.drawPointGeometry = /** * @inheritDoc */ -ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = - function(multiPointGeometry, data) { - ol.extent.extend(this.extent_, multiPointGeometry.getExtent()); - var flatCoordinates = multiPointGeometry.getFlatCoordinates(); - var stride = multiPointGeometry.getStride(); - this.drawCoordinates_( - flatCoordinates, 0, flatCoordinates.length, stride); -}; +ol.render.webgl.ImageReplay.prototype.drawPolygonGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawText = goog.abstractMethod; /** @@ -369,18 +264,18 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { goog.asserts.assert(this.images_.length > 0); var current = this.images_[this.images_.length - 1]; goog.asserts.assert(!goog.isDef(current[1])); - current[1] = this.indices.length; + current[1] = this.indices_.length; - this.verticesBuffer = gl.createBuffer(); - gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer); + this.verticesBuffer_ = gl.createBuffer(); + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); gl.bufferData(goog.webgl.ARRAY_BUFFER, - new Float32Array(this.vertices), goog.webgl.STATIC_DRAW); - this.indicesBuffer = gl.createBuffer(); - gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); + new Float32Array(this.vertices_), goog.webgl.STATIC_DRAW); + this.indicesBuffer_ = gl.createBuffer(); + gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); gl.bufferData(goog.webgl.ELEMENT_ARRAY_BUFFER, - new Uint16Array(this.indices), goog.webgl.STATIC_DRAW); + new Uint16Array(this.indices_), goog.webgl.STATIC_DRAW); - goog.asserts.assert(this.textures.length === 0); + goog.asserts.assert(this.textures_.length === 0); var i; var ii = this.images_.length; @@ -402,7 +297,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, goog.webgl.UNSIGNED_BYTE, image); - this.textures[i] = [texture, this.images_[i][1]]; + this.textures_[i] = [texture, this.images_[i][1]]; } this.height_ = undefined; @@ -418,11 +313,71 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { /** * @return {ol.Extent} Extent. */ -ol.render.webgl.Replay.prototype.getExtent = function() { +ol.render.webgl.ImageReplay.prototype.getExtent = function() { return this.extent_; }; +/** + * @param {ol.webgl.Context} context Context. + * @param {number} positionAttribLocation Attribute location for positions. + * @param {number} offsetsAttribLocation Attribute location for offsets. + * @param {number} texCoordAttribLocation Attribute location for texCoord. + * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. + * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. + * @param {number} pixelRatio Pixel ratio. + * @param {Array.} size Size. + * @param {goog.vec.Mat4.Number} transform Transform. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ImageReplay.prototype.replay = function(context, + positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, + projectionMatrixLocation, sizeMatrixLocation, + pixelRatio, size, transform, skippedFeaturesHash) { + var gl = context.getGL(); + + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); + + gl.enableVertexAttribArray(positionAttribLocation); + gl.vertexAttribPointer(positionAttribLocation, 2, goog.webgl.FLOAT, + false, 24, 0); + + gl.enableVertexAttribArray(offsetsAttribLocation); + gl.vertexAttribPointer(offsetsAttribLocation, 2, goog.webgl.FLOAT, + false, 24, 8); + + gl.enableVertexAttribArray(texCoordAttribLocation); + gl.vertexAttribPointer(texCoordAttribLocation, 2, goog.webgl.FLOAT, + false, 24, 16); + + + gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); + gl.uniformMatrix2fv(sizeMatrixLocation, false, + new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); + + gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + + var i; + var ii = this.textures_.length; + var texture; + for (i = 0; i < ii; ++i) { + texture = this.textures_[i]; + gl.bindTexture(goog.webgl.TEXTURE_2D, + /** @type {WebGLTexture} */ (texture[0])); + gl.drawElements(goog.webgl.TRIANGLES, /** @type {number} */ (texture[1]), + goog.webgl.UNSIGNED_SHORT, 0); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod; + + /** * @inheritDoc */ @@ -446,7 +401,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { (current[0]); goog.asserts.assert(!goog.isDef(current[1])); if (goog.getUid(currentImage) != goog.getUid(image)) { - current[1] = this.indices.length; + current[1] = this.indices_.length; this.images_.push([image, undefined]); } } @@ -460,6 +415,12 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { }; +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setTextStyle = goog.abstractMethod; + + /** * @constructor @@ -470,14 +431,15 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { ol.render.webgl.ReplayGroup = function(tolerance) { /** - * @private * @type {number} + * @private */ this.tolerance_ = tolerance; /** + * ImageReplay only is supported at this point. + * @type {Object.} * @private - * @type {Object.} */ this.replays_ = {}; @@ -560,7 +522,7 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, * @const * @private * @type {Object.} + * function(new: ol.render.webgl.ImageReplay, number)>} */ ol.render.webgl.BATCH_CONSTRUCTORS_ = { 'Image': ol.render.webgl.ImageReplay From 64bc8f74be41e76ec6a83ddc391b159d9e2dd29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 31 Oct 2014 15:41:25 +0100 Subject: [PATCH 10/98] Better typing, fewer arrays --- src/ol/render/webgl/webglreplay.js | 61 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 75ae117f53..b4999fe027 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -23,6 +23,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.extent_ = ol.extent.createEmpty(); + /** + * @type {Array.} + * @private + */ + this.groupIndices_ = []; + /** * @type {number|undefined} * @private @@ -30,7 +36,7 @@ ol.render.webgl.ImageReplay = function(tolerance) { this.height_ = undefined; /** - * @type {Array.>} + * @type {Array.} * @private */ this.images_ = []; @@ -71,6 +77,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.originY_ = undefined; + /** + * @type {Array.} + * @private + */ + this.textures_ = []; + /** * @type {Array.} * @private @@ -89,12 +101,6 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.width_ = undefined; - /** - * @type {Array.>} - * @private - */ - this.textures_ = []; - }; @@ -261,10 +267,8 @@ ol.render.webgl.ImageReplay.prototype.drawText = goog.abstractMethod; ol.render.webgl.ImageReplay.prototype.finish = function(context) { var gl = context.getGL(); - goog.asserts.assert(this.images_.length > 0); - var current = this.images_[this.images_.length - 1]; - goog.asserts.assert(!goog.isDef(current[1])); - current[1] = this.indices_.length; + this.groupIndices_.push(this.indices_.length); + goog.asserts.assert(this.images_.length == this.groupIndices_.length); this.verticesBuffer_ = gl.createBuffer(); gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); @@ -281,10 +285,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { var ii = this.images_.length; var texture; for (i = 0; i < ii; ++i) { - var image = - /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ - (this.images_[i][0]); - current = this.images_[i]; + var image = this.images_[i]; texture = gl.createTexture(); gl.bindTexture(goog.webgl.TEXTURE_2D, texture); gl.texParameteri(goog.webgl.TEXTURE_2D, @@ -297,15 +298,19 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, goog.webgl.UNSIGNED_BYTE, image); - this.textures_[i] = [texture, this.images_[i][1]]; + this.textures_[i] = texture; } + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + this.height_ = undefined; - this.images_.length = 0; + this.images_ = null; this.imageHeight_ = undefined; this.imageWidth_ = undefined; + this.indices_ = null; this.originX_ = undefined; this.originY_ = undefined; + this.vertices_ = null; this.width_ = undefined; }; @@ -359,14 +364,13 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + var i; var ii = this.textures_.length; - var texture; for (i = 0; i < ii; ++i) { - texture = this.textures_[i]; - gl.bindTexture(goog.webgl.TEXTURE_2D, - /** @type {WebGLTexture} */ (texture[0])); - gl.drawElements(goog.webgl.TRIANGLES, /** @type {number} */ (texture[1]), + gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]); + gl.drawElements(goog.webgl.TRIANGLES, this.groupIndices_[i], goog.webgl.UNSIGNED_SHORT, 0); } }; @@ -393,16 +397,13 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { goog.asserts.assert(!goog.isNull(size)); if (this.images_.length === 0) { - this.images_.push([image, undefined]); + this.images_.push(image); } else { - var current = this.images_[this.images_.length - 1]; - var currentImage = - /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ - (current[0]); - goog.asserts.assert(!goog.isDef(current[1])); + var currentImage = this.images_[this.images_.length - 1]; if (goog.getUid(currentImage) != goog.getUid(image)) { - current[1] = this.indices_.length; - this.images_.push([image, undefined]); + this.groupIndices_.push(this.indices_.length); + goog.asserts.assert(this.groupIndices_.length == this.images_.length); + this.images_.push(image); } } From 495a7c95a1d1054dabc5cdfe1d83698daa9c43be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 31 Oct 2014 15:42:52 +0100 Subject: [PATCH 11/98] Clearer comment --- src/ol/render/webgl/webglreplay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index b4999fe027..5e38defb51 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -141,7 +141,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = n = numVertices / 6; - // create 4 vertices per coordinate + // 4 vertices per coordinate, with 6 values per vertex this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; From 22e1159736bc9d32a879e20292bbda98428c1d7d Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Fri, 31 Oct 2014 11:18:17 +0100 Subject: [PATCH 12/98] Implement hashCode for ol.style.Circle --- src/ol/style/circlestyle.js | 20 +++++ src/ol/style/fillstyle.js | 18 ++++ src/ol/style/strokestyle.js | 28 ++++++ test/spec/ol/style/circlestyle.test.js | 117 +++++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 test/spec/ol/style/circlestyle.test.js diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 0d209ba876..235d066290 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -2,8 +2,10 @@ goog.provide('ol.style.Circle'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); +goog.require('goog.string'); goog.require('ol.color'); goog.require('ol.render.canvas'); +goog.require('ol.structs.IHashable'); goog.require('ol.style.Fill'); goog.require('ol.style.Image'); goog.require('ol.style.ImageState'); @@ -18,6 +20,7 @@ goog.require('ol.style.Stroke'); * @constructor * @param {olx.style.CircleOptions=} opt_options Options. * @extends {ol.style.Image} + * @implements {ol.structs.IHashable} * @api */ ol.style.Circle = function(opt_options) { @@ -260,3 +263,20 @@ ol.style.Circle.prototype.render_ = function() { return size; }; + + +/** + * @inheritDoc + */ +ol.style.Circle.prototype.hashCode = function() { + var hash = 17; + + hash = hash * 23 + (!goog.isNull(this.stroke_) ? + this.stroke_.hashCode() : 0); + hash = hash * 23 + (!goog.isNull(this.fill_) ? + this.fill_.hashCode() : 0); + hash = hash * 23 + (goog.isDef(this.radius_) ? + goog.string.hashCode(this.radius_.toString()) : 0); + + return hash; +}; diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js index 6714445868..65091029a7 100644 --- a/src/ol/style/fillstyle.js +++ b/src/ol/style/fillstyle.js @@ -1,5 +1,9 @@ goog.provide('ol.style.Fill'); +goog.require('goog.string'); +goog.require('ol.color'); +goog.require('ol.structs.IHashable'); + /** @@ -8,6 +12,7 @@ goog.provide('ol.style.Fill'); * * @constructor * @param {olx.style.FillOptions=} opt_options Options. + * @implements {ol.structs.IHashable} * @api */ ol.style.Fill = function(opt_options) { @@ -40,3 +45,16 @@ ol.style.Fill.prototype.getColor = function() { ol.style.Fill.prototype.setColor = function(color) { this.color_ = color; }; + + +/** + * @inheritDoc + */ +ol.style.Fill.prototype.hashCode = function() { + var hash = 17; + + hash = hash * 23 + (!goog.isNull(this.color_) ? + goog.string.hashCode(ol.color.asString(this.color_)) : 0); + + return hash; +}; diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js index 15e99bf260..934490f3b1 100644 --- a/src/ol/style/strokestyle.js +++ b/src/ol/style/strokestyle.js @@ -1,5 +1,9 @@ goog.provide('ol.style.Stroke'); +goog.require('goog.string'); +goog.require('ol.color'); +goog.require('ol.structs.IHashable'); + /** @@ -11,6 +15,7 @@ goog.provide('ol.style.Stroke'); * * @constructor * @param {olx.style.StrokeOptions=} opt_options Options. + * @implements {ol.structs.IHashable} * @api */ ol.style.Stroke = function(opt_options) { @@ -173,3 +178,26 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { ol.style.Stroke.prototype.setWidth = function(width) { this.width_ = width; }; + + +/** + * @inheritDoc + */ +ol.style.Stroke.prototype.hashCode = function() { + var hash = 17; + + hash = hash * 23 + (!goog.isNull(this.color_) ? + goog.string.hashCode(ol.color.asString(this.color_)) : 0); + hash = hash * 23 + (goog.isDef(this.lineCap_) ? + goog.string.hashCode(this.lineCap_.toString()) : 0); + hash = hash * 23 + (!goog.isNull(this.lineDash_) ? + goog.string.hashCode(this.lineDash_.toString()) : 0); + hash = hash * 23 + (goog.isDef(this.lineJoin_) ? + goog.string.hashCode(this.lineJoin_) : 0); + hash = hash * 23 + (goog.isDef(this.miterLimit_) ? + goog.string.hashCode(this.miterLimit_.toString()) : 0); + hash = hash * 23 + (goog.isDef(this.width_) ? + goog.string.hashCode(this.width_.toString()) : 0); + + return hash; +}; diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js new file mode 100644 index 0000000000..566164e199 --- /dev/null +++ b/test/spec/ol/style/circlestyle.test.js @@ -0,0 +1,117 @@ +goog.provide('ol.test.style.Circle'); + + +describe('ol.style.Circle', function() { + + describe('#hashCode', function() { + + it('calculates the same hash code for default options', function() { + var style1 = new ol.style.Circle(); + var style2 = new ol.style.Circle(); + expect(style1.hashCode()).to.eql(style2.hashCode()); + }); + + it('calculates not the same hash code (radius)', function() { + var style1 = new ol.style.Circle(); + var style2 = new ol.style.Circle({ + radius: 5 + }); + expect(style1.hashCode()).to.not.eql(style2.hashCode()); + }); + + it('calculates the same hash code (radius)', function() { + var style1 = new ol.style.Circle({ + radius: 5 + }); + var style2 = new ol.style.Circle({ + radius: 5 + }); + expect(style1.hashCode()).to.eql(style2.hashCode()); + }); + + it('calculates not the same hash code (color)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.hashCode()).to.not.eql(style2.hashCode()); + }); + + it('calculates the same hash code (everything set)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + expect(style1.hashCode()).to.eql(style2.hashCode()); + }); + + it('calculates not the same hash code (stroke width differs)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 3 + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + expect(style1.hashCode()).to.not.eql(style2.hashCode()); + }); + + }); +}); + +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); From 4534bb8861e47d0c108ba529d71626a4fa7350c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 12:43:22 +0100 Subject: [PATCH 13/98] Make shaders closer to WebGL image replay --- .../webgl/webglimage.glsl} | 4 +- src/ol/render/webgl/webglimageshader.js | 123 ++++++++++++++++++ src/ol/render/webgl/webglreplay.js | 66 ++++++---- .../webgl/webglvectorlayerrenderer.js | 38 ------ .../renderer/webgl/webglvectorlayershader.js | 123 ------------------ 5 files changed, 166 insertions(+), 188 deletions(-) rename src/ol/{renderer/webgl/webglvectorlayer.glsl => render/webgl/webglimage.glsl} (82%) create mode 100644 src/ol/render/webgl/webglimageshader.js delete mode 100644 src/ol/renderer/webgl/webglvectorlayershader.js diff --git a/src/ol/renderer/webgl/webglvectorlayer.glsl b/src/ol/render/webgl/webglimage.glsl similarity index 82% rename from src/ol/renderer/webgl/webglvectorlayer.glsl rename to src/ol/render/webgl/webglimage.glsl index 53e57efbda..ade5cb7446 100644 --- a/src/ol/renderer/webgl/webglvectorlayer.glsl +++ b/src/ol/render/webgl/webglimage.glsl @@ -1,5 +1,5 @@ -//! NAMESPACE=ol.renderer.webgl.vectorlayer.shader -//! CLASS=ol.renderer.webgl.vectorlayer.shader. +//! NAMESPACE=ol.render.webgl.imagereplay.shader +//! CLASS=ol.render.webgl.imagereplay.shader. //! COMMON diff --git a/src/ol/render/webgl/webglimageshader.js b/src/ol/render/webgl/webglimageshader.js new file mode 100644 index 0000000000..1d792306b6 --- /dev/null +++ b/src/ol/render/webgl/webglimageshader.js @@ -0,0 +1,123 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.imagereplay.shader'); + +goog.require('ol.webgl.shader'); + + + +/** + * @constructor + * @extends {ol.webgl.shader.Fragment} + * @struct + */ +ol.render.webgl.imagereplay.shader.Fragment = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.Fragment.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.Fragment, ol.webgl.shader.Fragment); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.Fragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\nuniform sampler2D u_image;\n\nvoid main(void) {\n gl_FragColor = texture2D(u_image, v_texCoord);\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D g;void main(void){gl_FragColor=texture2D(g,a);}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Fragment.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.Fragment.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @extends {ol.webgl.shader.Vertex} + * @struct + */ +ol.render.webgl.imagereplay.shader.Vertex = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.Vertex.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.Vertex, ol.webgl.shader.Vertex); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.Vertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;attribute vec2 d;uniform mat4 e;uniform mat2 f;void main(void){vec2 offsets=f*d;gl_Position=e*vec4(b,0.,1.)+vec4(offsets,0.,0.);a=c;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.Vertex.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.imagereplay.shader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_image = gl.getUniformLocation( + program, goog.DEBUG ? 'u_image' : 'g'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_projectionMatrix' : 'e'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_sizeMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_sizeMatrix' : 'f'); + + /** + * @type {number} + */ + this.a_offsets = gl.getAttribLocation( + program, goog.DEBUG ? 'a_offsets' : 'd'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, goog.DEBUG ? 'a_position' : 'b'); + + /** + * @type {number} + */ + this.a_texCoord = gl.getAttribLocation( + program, goog.DEBUG ? 'a_texCoord' : 'c'); +}; diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 5e38defb51..034a5a2842 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -5,6 +5,7 @@ goog.require('goog.asserts'); goog.require('goog.object'); goog.require('ol.extent'); goog.require('ol.render.IReplayGroup'); +goog.require('ol.render.webgl.imagereplay.shader'); @@ -23,6 +24,13 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.extent_ = ol.extent.createEmpty(); + /** + * @private + * @type {ol.webgl.shader.Fragment} + */ + this.fragmentShader_ = + ol.render.webgl.imagereplay.shader.Fragment.getInstance(); + /** * @type {Array.} * @private @@ -65,6 +73,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.indicesBuffer_ = null; + /** + * @private + * @type {ol.render.webgl.imagereplay.shader.Locations} + */ + this.locations_ = null; + /** * @type {number|undefined} * @private @@ -83,6 +97,13 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.textures_ = []; + /** + * @private + * @type {ol.webgl.shader.Vertex} + */ + this.vertexShader_ = + ol.render.webgl.imagereplay.shader.Vertex.getInstance(); + /** * @type {Array.} * @private @@ -325,11 +346,6 @@ ol.render.webgl.ImageReplay.prototype.getExtent = function() { /** * @param {ol.webgl.Context} context Context. - * @param {number} positionAttribLocation Attribute location for positions. - * @param {number} offsetsAttribLocation Attribute location for offsets. - * @param {number} texCoordAttribLocation Attribute location for texCoord. - * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. - * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. * @param {number} pixelRatio Pixel ratio. * @param {Array.} size Size. * @param {goog.vec.Mat4.Number} transform Transform. @@ -338,28 +354,37 @@ ol.render.webgl.ImageReplay.prototype.getExtent = function() { * @template T */ ol.render.webgl.ImageReplay.prototype.replay = function(context, - positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, - projectionMatrixLocation, sizeMatrixLocation, pixelRatio, size, transform, skippedFeaturesHash) { var gl = context.getGL(); + var program = context.getProgram( + this.fragmentShader_, this.vertexShader_); + context.useProgram(program); + + if (goog.isNull(this.locations_)) { + this.locations_ = + new ol.render.webgl.imagereplay.shader.Locations( + gl, program); + } + + var locations = this.locations_; + gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); - gl.enableVertexAttribArray(positionAttribLocation); - gl.vertexAttribPointer(positionAttribLocation, 2, goog.webgl.FLOAT, + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT, false, 24, 0); - gl.enableVertexAttribArray(offsetsAttribLocation); - gl.vertexAttribPointer(offsetsAttribLocation, 2, goog.webgl.FLOAT, + gl.enableVertexAttribArray(locations.a_offsets); + gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT, false, 24, 8); - gl.enableVertexAttribArray(texCoordAttribLocation); - gl.vertexAttribPointer(texCoordAttribLocation, 2, goog.webgl.FLOAT, + gl.enableVertexAttribArray(locations.a_texCoord); + gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT, false, 24, 16); - - gl.uniformMatrix4fv(projectionMatrixLocation, false, transform); - gl.uniformMatrix2fv(sizeMatrixLocation, false, + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, transform); + gl.uniformMatrix2fv(locations.u_sizeMatrix, false, new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); @@ -484,11 +509,6 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { /** * @param {ol.webgl.Context} context Context. - * @param {number} positionAttribLocation Attribute location for positions. - * @param {number} offsetsAttribLocation Attribute location for offsets. - * @param {number} texCoordAttribLocation Attribute location for texCoord. - * @param {WebGLUniformLocation} projectionMatrixLocation Proj matrix location. - * @param {WebGLUniformLocation} sizeMatrixLocation Size matrix location. * @param {ol.Extent} extent Extent. * @param {number} pixelRatio Pixel ratio. * @param {Array.} size Size. @@ -498,8 +518,6 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { * @template T */ ol.render.webgl.ReplayGroup.prototype.replay = function(context, - positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, - projectionMatrixLocation, sizeMatrixLocation, extent, pixelRatio, size, transform, skippedFeaturesHash) { var i, ii, replay, result; for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) { @@ -507,8 +525,6 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, if (goog.isDef(replay) && ol.extent.intersects(extent, replay.getExtent())) { result = replay.replay(context, - positionAttribLocation, offsetsAttribLocation, texCoordAttribLocation, - projectionMatrixLocation, sizeMatrixLocation, pixelRatio, size, transform, skippedFeaturesHash); if (result) { return result; diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index cb67b0174b..440581bfbf 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -9,7 +9,6 @@ goog.require('ol.layer.Vector'); goog.require('ol.render.webgl.ReplayGroup'); goog.require('ol.renderer.vector'); goog.require('ol.renderer.webgl.Layer'); -goog.require('ol.renderer.webgl.vectorlayer.shader'); goog.require('ol.vec.Mat4'); @@ -24,26 +23,6 @@ ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) { goog.base(this, mapRenderer, vectorLayer); - /** - * @private - * @type {ol.webgl.shader.Fragment} - */ - this.fragmentShader_ = - ol.renderer.webgl.vectorlayer.shader.Fragment.getInstance(); - - /** - * @private - * @type {ol.webgl.shader.Vertex} - */ - this.vertexShader_ = - ol.renderer.webgl.vectorlayer.shader.Vertex.getInstance(); - - /** - * @private - * @type {ol.renderer.webgl.vectorlayer.shader.Locations} - */ - this.locations_ = null; - /** * @private * @type {boolean} @@ -90,18 +69,6 @@ goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) { - var gl = context.getGL(); - - var program = context.getProgram( - this.fragmentShader_, this.vertexShader_); - context.useProgram(program); - - if (goog.isNull(this.locations_)) { - this.locations_ = - new ol.renderer.webgl.vectorlayer.shader.Locations( - gl, program); - } - var viewState = frameState.viewState; ol.vec.Mat4.makeTransform2D(this.projectionMatrix, 0.0, 0.0, @@ -113,11 +80,6 @@ ol.renderer.webgl.VectorLayer.prototype.composeFrame = var replayGroup = this.replayGroup_; if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { replayGroup.replay(context, - this.locations_.a_position, - this.locations_.a_offsets, - this.locations_.a_texCoord, - this.locations_.u_projectionMatrix, - this.locations_.u_sizeMatrix, frameState.extent, frameState.pixelRatio, frameState.size, this.projectionMatrix, frameState.skippedFeatureUids); diff --git a/src/ol/renderer/webgl/webglvectorlayershader.js b/src/ol/renderer/webgl/webglvectorlayershader.js deleted file mode 100644 index f3226c8963..0000000000 --- a/src/ol/renderer/webgl/webglvectorlayershader.js +++ /dev/null @@ -1,123 +0,0 @@ -// This file is automatically generated, do not edit -goog.provide('ol.renderer.webgl.vectorlayer.shader'); - -goog.require('ol.webgl.shader'); - - - -/** - * @constructor - * @extends {ol.webgl.shader.Fragment} - * @struct - */ -ol.renderer.webgl.vectorlayer.shader.Fragment = function() { - goog.base(this, ol.renderer.webgl.vectorlayer.shader.Fragment.SOURCE); -}; -goog.inherits(ol.renderer.webgl.vectorlayer.shader.Fragment, ol.webgl.shader.Fragment); -goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Fragment); - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\nuniform sampler2D u_image;\n\nvoid main(void) {\n gl_FragColor = texture2D(u_image, v_texCoord);\n}\n'; - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D g;void main(void){gl_FragColor=texture2D(g,a);}'; - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Fragment.SOURCE = goog.DEBUG ? - ol.renderer.webgl.vectorlayer.shader.Fragment.DEBUG_SOURCE : - ol.renderer.webgl.vectorlayer.shader.Fragment.OPTIMIZED_SOURCE; - - - -/** - * @constructor - * @extends {ol.webgl.shader.Vertex} - * @struct - */ -ol.renderer.webgl.vectorlayer.shader.Vertex = function() { - goog.base(this, ol.renderer.webgl.vectorlayer.shader.Vertex.SOURCE); -}; -goog.inherits(ol.renderer.webgl.vectorlayer.shader.Vertex, ol.webgl.shader.Vertex); -goog.addSingletonGetter(ol.renderer.webgl.vectorlayer.shader.Vertex); - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n}\n\n\n'; - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;attribute vec2 d;uniform mat4 e;uniform mat2 f;void main(void){vec2 offsets=f*d;gl_Position=e*vec4(b,0.,1.)+vec4(offsets,0.,0.);a=c;}'; - - -/** - * @const - * @type {string} - */ -ol.renderer.webgl.vectorlayer.shader.Vertex.SOURCE = goog.DEBUG ? - ol.renderer.webgl.vectorlayer.shader.Vertex.DEBUG_SOURCE : - ol.renderer.webgl.vectorlayer.shader.Vertex.OPTIMIZED_SOURCE; - - - -/** - * @constructor - * @param {WebGLRenderingContext} gl GL. - * @param {WebGLProgram} program Program. - * @struct - */ -ol.renderer.webgl.vectorlayer.shader.Locations = function(gl, program) { - - /** - * @type {WebGLUniformLocation} - */ - this.u_image = gl.getUniformLocation( - program, goog.DEBUG ? 'u_image' : 'g'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_projectionMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_projectionMatrix' : 'e'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_sizeMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_sizeMatrix' : 'f'); - - /** - * @type {number} - */ - this.a_offsets = gl.getAttribLocation( - program, goog.DEBUG ? 'a_offsets' : 'd'); - - /** - * @type {number} - */ - this.a_position = gl.getAttribLocation( - program, goog.DEBUG ? 'a_position' : 'b'); - - /** - * @type {number} - */ - this.a_texCoord = gl.getAttribLocation( - program, goog.DEBUG ? 'a_texCoord' : 'c'); -}; From 14d7f2a797ab80e5afdc22a4af2e9ce795f850cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 14:16:32 +0100 Subject: [PATCH 14/98] Delete ImageReplay textures --- src/ol/render/webgl/webglreplay.js | 37 +++++++++++++++++++ .../webgl/webglvectorlayerrenderer.js | 6 +-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 034a5a2842..21706333ca 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -125,6 +125,30 @@ ol.render.webgl.ImageReplay = function(tolerance) { }; +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ImageReplay.prototype.deleteTextures = + function(frameState, context) { + frameState.postRenderFunctions.push( + goog.partial( + /** + * @param {WebGLRenderingContext} gl GL. + * @param {Array.} textures Textures. + */ + function(gl, textures) { + if (!gl.isContextLost()) { + var i, ii; + for (i = 0, ii = textures.length; i < ii; ++i) { + gl.deleteTexture(textures[i]); + } + } + }, context.getGL(), this.textures_)); + +}; + + /** * @inheritDoc */ @@ -472,6 +496,19 @@ ol.render.webgl.ReplayGroup = function(tolerance) { }; +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ReplayGroup.prototype.deleteTextures = + function(frameState, context) { + var replayKey; + for (replayKey in this.replays_) { + this.replays_[replayKey].deleteTextures(frameState, context); + } +}; + + /** * @param {ol.webgl.Context} context Context. */ diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index 440581bfbf..78355ec7ac 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -153,9 +153,9 @@ ol.renderer.webgl.VectorLayer.prototype.prepareFrame = extent[2] = frameStateExtent[2] + xBuffer; extent[3] = frameStateExtent[3] + yBuffer; - // FIXME dispose of old replayGroup in post render - goog.dispose(this.replayGroup_); - this.replayGroup_ = null; + if (!goog.isNull(this.replayGroup_)) { + this.replayGroup_.deleteTextures(frameState, context); + } this.dirty_ = false; From 2c92d9a709a613917cb2ec32b349d871ae6d125e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 14:54:41 +0100 Subject: [PATCH 15/98] Fix drawElement call --- src/ol/render/webgl/webglreplay.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 21706333ca..26a5bcddd6 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -415,12 +415,13 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, goog.asserts.assert(this.textures_.length == this.groupIndices_.length); - var i; - var ii = this.textures_.length; - for (i = 0; i < ii; ++i) { + 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]); - gl.drawElements(goog.webgl.TRIANGLES, this.groupIndices_[i], - goog.webgl.UNSIGNED_SHORT, 0); + var end = this.groupIndices_[i]; + gl.drawElements(goog.webgl.TRIANGLES, end - start, + goog.webgl.UNSIGNED_SHORT, start); + start = end; } }; From 05bbfd58a8928c33f40b440cb230683f2896af29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 15:40:00 +0100 Subject: [PATCH 16/98] Support image anchor --- src/ol/render/webgl/webglreplay.js | 38 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 26a5bcddd6..c314d2585e 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -18,6 +18,18 @@ goog.require('ol.render.webgl.imagereplay.shader'); */ ol.render.webgl.ImageReplay = function(tolerance) { + /** + * @type {number|undefined} + * @private + */ + this.anchorX_ = undefined; + + /** + * @type {number|undefined} + * @private + */ + this.anchorY_ = undefined; + /** * @type {ol.Extent} * @private @@ -165,12 +177,16 @@ ol.render.webgl.ImageReplay.prototype.drawAsync = goog.abstractMethod; */ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { + goog.asserts.assert(goog.isDef(this.anchorX_)); + goog.asserts.assert(goog.isDef(this.anchorY_)); goog.asserts.assert(goog.isDef(this.height_)); goog.asserts.assert(goog.isDef(this.imageHeight_)); goog.asserts.assert(goog.isDef(this.imageWidth_)); goog.asserts.assert(goog.isDef(this.originX_)); goog.asserts.assert(goog.isDef(this.originY_)); goog.asserts.assert(goog.isDef(this.width_)); + var anchorX = this.anchorX_; + var anchorY = this.anchorY_; var height = this.height_; var imageHeight = this.imageHeight_; var imageWidth = this.imageWidth_; @@ -190,29 +206,29 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -width; - this.vertices_[numVertices++] = -height; + this.vertices_[numVertices++] = -2 * anchorX; + this.vertices_[numVertices++] = -2 * (height - anchorY); this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = width; - this.vertices_[numVertices++] = -height; + this.vertices_[numVertices++] = 2 * (width - anchorX); + this.vertices_[numVertices++] = -2 * (height - anchorY); this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = width; - this.vertices_[numVertices++] = height; + this.vertices_[numVertices++] = 2 * (width - anchorX); + this.vertices_[numVertices++] = 2 * anchorY; this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -width; - this.vertices_[numVertices++] = height; + this.vertices_[numVertices++] = -2 * anchorX; + this.vertices_[numVertices++] = 2 * anchorY; this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; @@ -348,6 +364,8 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + this.anchorX_ = undefined; + this.anchorY_ = undefined; this.height_ = undefined; this.images_ = null; this.imageHeight_ = undefined; @@ -436,6 +454,8 @@ ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod; * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { + var anchor = imageStyle.getAnchor(); + goog.asserts.assert(!goog.isNull(anchor)); var image = imageStyle.getImage(1); goog.asserts.assert(!goog.isNull(image)); // FIXME getImageSize does not exist for circles @@ -457,6 +477,8 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { } } + this.anchorX_ = anchor[0]; + this.anchorY_ = anchor[1]; this.height_ = size[1]; this.imageHeight_ = imageSize[1]; this.imageWidth_ = imageSize[0]; From 1bd388188b769f125d62eaa1a0b40bf2951c84bf Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Mon, 3 Nov 2014 15:29:21 +0100 Subject: [PATCH 17/98] Add atlas manager --- src/ol/renderer/webgl/webglatlasmanager.js | 306 ++++++++++++++++++ .../renderer/webgl/webglatlasmanager.test.js | 216 +++++++++++++ 2 files changed, 522 insertions(+) create mode 100644 src/ol/renderer/webgl/webglatlasmanager.js create mode 100644 test/spec/ol/renderer/webgl/webglatlasmanager.test.js diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js new file mode 100644 index 0000000000..cf8163cd44 --- /dev/null +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -0,0 +1,306 @@ +goog.provide('ol.renderer.webgl.Atlas'); +goog.provide('ol.renderer.webgl.AtlasManager'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.object'); + + + +/** + * @constructor + * @struct + * @param {number} offsetX + * @param {number} offsetY + * @param {HTMLCanvasElement} image + */ +ol.renderer.webgl.AtlasInfo = function(offsetX, offsetY, image) { + + /** + * @type {number} + */ + this.offsetX = offsetX; + + /** + * @type {number} + */ + this.offsetY = offsetY; + + /** + * @type {HTMLCanvasElement} + */ + this.image = image; +}; + + + +/** + * @constructor + * @struct + * @param {number=} opt_size The size in pixels of the sprite images + * (default: 256). + * @param {number=} opt_space The space in pixels between images + * (default: 1). + */ +ol.renderer.webgl.AtlasManager = function(opt_size, opt_space) { + + /** + * The size in pixels of the sprite images. + * @private + * @type {number} + */ + this.size_ = goog.isDef(opt_size) ? opt_size : 256; + + /** + * The size in pixels between images. + * @private + * @type {number} + */ + this.space_ = goog.isDef(opt_space) ? opt_space : 1; + + /** + * @private + * @type {Array.} + */ + this.atlases_ = [new ol.renderer.webgl.Atlas(this.size_, this.space_)]; +}; + + +/** + * @param {number} hash The hash of the entry to check. + * @return {ol.renderer.webgl.AtlasInfo} + */ +ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { + var atlas, info; + for (var i = 0, l = this.atlases_.length; i < l; i++) { + atlas = this.atlases_[i]; + info = atlas.get(hash); + if (info !== null) { + return info; + } + } + return null; +}; + + +/** + * Add an image to the atlas manager. + * + * If an entry for the given hash already exists, the entry will + * be overridden (but the space on the atlas graphic will not be freed). + * + * @param {number} hash The hash of the entry to add. + * @param {number} width The width. + * @param {number} height The height. + * @param {function(*)} renderCallback Called to render the new sprite entry + * onto the sprite image. + * @param {object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {ol.renderer.webgl.AtlasInfo} + */ +ol.renderer.webgl.AtlasManager.prototype.add = + function(hash, width, height, renderCallback, opt_this) { + goog.asserts.assert(width <= this.size_ && height <= this.size_, + 'the entry is too big for the current atlas size'); + + var atlas, info; + for (var i = 0, l = this.atlases_.length; i < l; i++) { + atlas = this.atlases_[i]; + info = atlas.add(hash, width, height, renderCallback, opt_this); + if (info !== null) { + return info; + } + } + + // the entry could not be added to one of the existing atlases, + // create a new atlas and add to this one. + // TODO double the size and check for max. size? + atlas = new ol.renderer.webgl.Atlas(this.size_, this.space_); + this.atlases_.push(atlas); + return atlas.add(hash, width, height, renderCallback, opt_this); +}; + + + +/** + * This class facilitates the creation of texture atlases. + * + * Images added to an atlas will be rendered onto a single + * atlas canvas. The distribution of images on the canvas are + * managed with the bin packing algorithm described in: + * http://www.blackpawn.com/texts/lightmaps/ + * + * @constructor + * @struct + * @param {number} size The size in pixels of the sprite images. + * @param {number} space The space in pixels between images. + */ +ol.renderer.webgl.Atlas = function(size, space) { + + /** + * @private + * @type {number} The space in pixels between images. + * Because texture coordinates are float values, the edges of + * texture might not be completely correct (in a way that the + * edges overlap when being rendered). To avoid this we add a + * padding around each image. + */ + this.space_ = space; + + /** + * @private + * @type {Array.} + */ + this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}]; + + /** + * @private + * @type {Object.} + */ + this.entries_ = {}; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_.width = size; + this.canvas_.height = size; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); +}; + + +/** + * @param {number} hash The hash of the entry to check. + * @return {ol.renderer.webgl.AtlasInfo} + */ +ol.renderer.webgl.Atlas.prototype.get = function(hash) { + return goog.object.get(this.entries_, hash, null); +}; + + +/** + * @param {number} hash The hash of the entry to add. + * @param {number} width The width. + * @param {number} height The height. + * @param {function(*)} renderCallback Called to render the new sprite entry + * onto the sprite image. + * @param {object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {ol.renderer.webgl.AtlasInfo} + */ +ol.renderer.webgl.Atlas.prototype.add = + function(hash, width, height, renderCallback, opt_this) { + var block; + for (var i = 0, l = this.emptyBlocks_.length; i < l; i++) { + block = this.emptyBlocks_[i]; + if (block.width >= width + this.space_ && + block.height >= height + this.space_) { + // we found a block that is big enough for our entry + var entry = new ol.renderer.webgl.AtlasInfo( + block.x + this.space_, block.y + this.space_, this.canvas_); + this.entries_[hash] = entry; + + // render the image on the atlas image + renderCallback.call(opt_this, this.context_, + block.x + this.space_, block.y + this.space_); + + // split the block after the insertion, either horizontally or vertically + this.split_(i, block, width + this.space_, height + this.space_); + + return entry; + } + } + + // there is no space for the new entry in this atlas + return null; +}; + + +/** + * @private + * @param {number} index The index of the block. + * @param {ol.renderer.webgl.Atlas.Block} block The block to split. + * @param {number} width The width of the entry to insert. + * @param {number} height The height of the entry to insert. + */ +ol.renderer.webgl.Atlas.prototype.split_ = + function(index, block, width, height) { + var deltaWidth = block.width - width; + var deltaHeight = block.height - height; + + var newBlock1, newBlock2; + if (deltaWidth > deltaHeight) { + // split vertically + // block right of the inserted entry + newBlock1 = { + x: block.x + width, + y: block.y, + width: block.width - width, + height: block.height + }; + + // block below the inserted entry + newBlock2 = { + x: block.x, + y: block.y + height, + width: width, + height: block.height - height + }; + this.updateBlocks_(index, newBlock1, newBlock2); + } else { + // split horizontally + // block right of the inserted entry + newBlock1 = { + x: block.x + width, + y: block.y, + width: block.width - width, + height: height + }; + + // block below the inserted entry + newBlock2 = { + x: block.x, + y: block.y + height, + width: block.width, + height: block.height - height + }; + this.updateBlocks_(index, newBlock1, newBlock2); + } +}; + + +/** + * Remove the old block and insert new blocks at the same array position. + * The new blocks are inserted at the same position, so that splitted + * blocks (that are potentially smaller) are filled first. + * @private + * @param {number} index The index of the block to remove. + * @param {ol.renderer.webgl.Atlas.Block} newBlock1 The 1st block to add. + * @param {ol.renderer.webgl.Atlas.Block} newBlock2 The 2nd block to add. + */ +ol.renderer.webgl.Atlas.prototype.updateBlocks_ = + function(index, newBlock1, newBlock2) { + var args = [index, 1]; + if (newBlock1.width > 0 && newBlock1.height > 0) { + args.push(newBlock1); + } + if (newBlock2.width > 0 && newBlock2.height > 0) { + args.push(newBlock2); + } + this.emptyBlocks_.splice.apply(this.emptyBlocks_, args); +}; + + +/** + * @typedef{{x: number, y: number, width: number, height: number}} + */ +ol.renderer.webgl.Atlas.Block; diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js new file mode 100644 index 0000000000..0fb05a8d19 --- /dev/null +++ b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js @@ -0,0 +1,216 @@ +goog.provide('ol.test.renderer.webgl.AtlasManager'); + + +describe('ol.renderer.webgl.Atlas', function() { + + var defaultRender = function(context, x, y) { + }; + + describe('#constructor', function() { + + it('inits the atlas', function() { + var atlas = new ol.renderer.webgl.Atlas(256, 1); + expect(atlas.emptyBlocks_).to.eql( + [{x: 0, y: 0, width: 256, height: 256}]); + }); + }); + + describe('#add (squares with same size)', function() { + + it('adds one entry', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + var info = atlas.add(1, 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get(1)).to.eql(info); + }); + + it('adds two entries', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + atlas.add(1, 32, 32, defaultRender); + var info = atlas.add(2, 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 34, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get(2)).to.eql(info); + }); + + it('adds three entries', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + atlas.add(1, 32, 32, defaultRender); + atlas.add(2, 32, 32, defaultRender); + var info = atlas.add(3, 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 67, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get(3)).to.eql(info); + }); + + it('adds four entries (new row)', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + atlas.add(1, 32, 32, defaultRender); + atlas.add(2, 32, 32, defaultRender); + atlas.add(3, 32, 32, defaultRender); + var info = atlas.add(4, 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 34, image: atlas.canvas_}); + + expect(atlas.get(4)).to.eql(info); + }); + + it('returns null when an entry is too big', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + atlas.add(1, 32, 32, defaultRender); + atlas.add(2, 32, 32, defaultRender); + atlas.add(3, 32, 32, defaultRender); + var info = atlas.add(4, 100, 100, defaultRender); + + expect(info).to.eql(null); + }); + + it('fills up the whole atlas', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + for (var i = 1; i <= 16; i++) { + expect(atlas.add(i, 28, 28, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add(17, 28, 28, defaultRender)).to.eql(null); + }); + }); + + describe('#add (rectangles with different sizes)', function() { + + it('adds a bunch of rectangles', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + expect(atlas.add(1, 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.add(2, 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 34, image: atlas.canvas_}); + + expect(atlas.add(3, 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 67, image: atlas.canvas_}); + + // this one can not be added anymore + expect(atlas.add(4, 64, 32, defaultRender)).to.eql(null); + + // but there is still room for smaller ones + expect(atlas.add(5, 40, 32, defaultRender)).to.eql( + {offsetX: 66, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.add(6, 40, 32, defaultRender)).to.eql( + {offsetX: 66, offsetY: 34, image: atlas.canvas_}); + }); + + it('fills up the whole atlas (rectangles in portrait format)', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + for (var i = 1; i <= 32; i++) { + expect(atlas.add(i, 28, 14, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add(33, 28, 14, defaultRender)).to.eql(null); + }); + + it('fills up the whole atlas (rectangles in landscape format)', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + for (var i = 1; i <= 32; i++) { + expect(atlas.add(i, 14, 28, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add(33, 14, 28, defaultRender)).to.eql(null); + }); + }); + + describe('#add (rendering)', function() { + + it('calls the render callback with the right values', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + var rendererCallback = sinon.spy(); + atlas.add(1, 32, 32, rendererCallback); + + expect(rendererCallback.calledOnce).to.be.ok(); + expect(rendererCallback.calledWith(atlas.context_, 1, 1)).to.be.ok(); + + rendererCallback = sinon.spy(); + atlas.add(2, 32, 32, rendererCallback); + + expect(rendererCallback.calledOnce).to.be.ok(); + expect(rendererCallback.calledWith(atlas.context_, 34, 1)).to.be.ok(); + }); + + it('is possible to actually draw on the canvas', function() { + var atlas = new ol.renderer.webgl.Atlas(128, 1); + + var rendererCallback = function(context, x, y) { + context.fillStyle = '#FFA500'; + context.fillRect(x, y, 32, 32); + }; + + expect(atlas.add(1, 32, 32, rendererCallback)).to.be.ok(); + expect(atlas.add(2, 32, 32, rendererCallback)).to.be.ok(); + // no error, ok + }); + }); +}); + + +describe('ol.renderer.webgl.AtlasManager', function() { + + var defaultRender = function(context, x, y) { + }; + + describe('#constructor', function() { + + it('inits the atlas manager', function() { + var manager = new ol.renderer.webgl.AtlasManager(); + expect(manager.atlases_).to.not.be.empty(); + }); + }); + + describe('#add', function() { + + it('adds one entry', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + var info = manager.add(1, 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_}); + + expect(manager.getInfo(1)).to.eql(info); + }); + + it('creates a new atlas if needed', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + expect(manager.add(2, 100, 100, defaultRender)).to.be.ok(); + expect(manager.atlases_).to.have.length(2); + }); + }); + + describe('#getInfo', function() { + + it('returns null if no entry for the given hash', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + expect(manager.getInfo(123456)).to.eql(null); + }); + }); +}); + +goog.require('ol.renderer.webgl.Atlas'); +goog.require('ol.renderer.webgl.AtlasManager'); From 209d39a460629edb5fbc1380d70ac887006ee773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 16:16:57 +0100 Subject: [PATCH 18/98] Support image opacity --- examples/icon-sprite-webgl.js | 9 ++++---- src/ol/render/webgl/webglimage.glsl | 7 +++++- src/ol/render/webgl/webglimageshader.js | 26 ++++++++++++--------- src/ol/render/webgl/webglreplay.js | 30 ++++++++++++++++++++----- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 6b8d24bbf0..f036b398c2 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -9,9 +9,9 @@ goog.require('ol.style.Style'); var iconInfo = [ - {size: [55, 55], offset: [0, 0]}, - {size: [55, 55], offset: [110, 86]}, - {size: [55, 86], offset: [55, 0]} + {size: [55, 55], offset: [0, 0], opacity: 1.0}, + {size: [55, 55], offset: [110, 86], opacity: 0.75}, + {size: [55, 86], offset: [55, 0], opacity: 0.5} ]; var i; @@ -22,7 +22,8 @@ for (i = 0; i < iconCount; ++i) { icons[i] = new ol.style.Icon({ src: 'data/Butterfly.png', size: iconInfo[i].size, - offset: iconInfo[i].offset + offset: iconInfo[i].offset, + opacity: iconInfo[i].opacity }); } diff --git a/src/ol/render/webgl/webglimage.glsl b/src/ol/render/webgl/webglimage.glsl index ade5cb7446..5098ac2a3f 100644 --- a/src/ol/render/webgl/webglimage.glsl +++ b/src/ol/render/webgl/webglimage.glsl @@ -4,11 +4,13 @@ //! COMMON varying vec2 v_texCoord; +varying float v_opacity; //! VERTEX attribute vec2 a_position; attribute vec2 a_texCoord; attribute vec2 a_offsets; +attribute float a_opacity; uniform mat4 u_projectionMatrix; uniform mat2 u_sizeMatrix; @@ -17,6 +19,7 @@ void main(void) { vec2 offsets = u_sizeMatrix * a_offsets; gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.); v_texCoord = a_texCoord; + v_opacity = a_opacity; } @@ -24,5 +27,7 @@ void main(void) { uniform sampler2D u_image; void main(void) { - gl_FragColor = texture2D(u_image, v_texCoord); + vec4 texColor = texture2D(u_image, v_texCoord); + gl_FragColor.rgb = texColor.rgb; + gl_FragColor.a = texColor.a * v_opacity; } diff --git a/src/ol/render/webgl/webglimageshader.js b/src/ol/render/webgl/webglimageshader.js index 1d792306b6..f50e358d84 100644 --- a/src/ol/render/webgl/webglimageshader.js +++ b/src/ol/render/webgl/webglimageshader.js @@ -21,14 +21,14 @@ goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.Fragment); * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\nuniform sampler2D u_image;\n\nvoid main(void) {\n gl_FragColor = texture2D(u_image, v_texCoord);\n}\n'; +ol.render.webgl.imagereplay.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\nuniform sampler2D u_image;\n\nvoid main(void) {\n vec4 texColor = texture2D(u_image, v_texCoord);\n gl_FragColor.rgb = texColor.rgb;\n gl_FragColor.a = texColor.a * v_opacity;\n}\n'; /** * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D g;void main(void){gl_FragColor=texture2D(g,a);}'; +ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform sampler2D i;void main(void){vec4 texColor=texture2D(i,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*b;}'; /** @@ -57,14 +57,14 @@ goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.Vertex); * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n}\n\n\n'; +ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; /** * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;attribute vec2 d;uniform mat4 e;uniform mat2 f;void main(void){vec2 offsets=f*d;gl_Position=e*vec4(b,0.,1.)+vec4(offsets,0.,0.);a=c;}'; +ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;uniform mat4 g;uniform mat2 h;void main(void){vec2 offsets=h*e;gl_Position=g*vec4(c,0.,1.)+vec4(offsets,0.,0.);a=d;b=f;}'; /** @@ -89,35 +89,41 @@ ol.render.webgl.imagereplay.shader.Locations = function(gl, program) { * @type {WebGLUniformLocation} */ this.u_image = gl.getUniformLocation( - program, goog.DEBUG ? 'u_image' : 'g'); + program, goog.DEBUG ? 'u_image' : 'i'); /** * @type {WebGLUniformLocation} */ this.u_projectionMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_projectionMatrix' : 'e'); + program, goog.DEBUG ? 'u_projectionMatrix' : 'g'); /** * @type {WebGLUniformLocation} */ this.u_sizeMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_sizeMatrix' : 'f'); + program, goog.DEBUG ? 'u_sizeMatrix' : 'h'); /** * @type {number} */ this.a_offsets = gl.getAttribLocation( - program, goog.DEBUG ? 'a_offsets' : 'd'); + program, goog.DEBUG ? 'a_offsets' : 'e'); + + /** + * @type {number} + */ + this.a_opacity = gl.getAttribLocation( + program, goog.DEBUG ? 'a_opacity' : 'f'); /** * @type {number} */ this.a_position = gl.getAttribLocation( - program, goog.DEBUG ? 'a_position' : 'b'); + program, goog.DEBUG ? 'a_position' : 'c'); /** * @type {number} */ this.a_texCoord = gl.getAttribLocation( - program, goog.DEBUG ? 'a_texCoord' : 'c'); + program, goog.DEBUG ? 'a_texCoord' : 'd'); }; diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index c314d2585e..c5f8d144f0 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -91,6 +91,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.locations_ = null; + /** + * @private + * @type {number|undefined} + */ + this.opacity_ = undefined; + /** * @type {number|undefined} * @private @@ -182,6 +188,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = goog.asserts.assert(goog.isDef(this.height_)); goog.asserts.assert(goog.isDef(this.imageHeight_)); goog.asserts.assert(goog.isDef(this.imageWidth_)); + goog.asserts.assert(goog.isDef(this.opacity_)); goog.asserts.assert(goog.isDef(this.originX_)); goog.asserts.assert(goog.isDef(this.originY_)); goog.asserts.assert(goog.isDef(this.width_)); @@ -190,6 +197,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var height = this.height_; var imageHeight = this.imageHeight_; var imageWidth = this.imageWidth_; + var opacity = this.opacity_; var originX = this.originX_; var originY = this.originY_; var width = this.width_; @@ -200,9 +208,9 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = x = flatCoordinates[i]; y = flatCoordinates[i + 1]; - n = numVertices / 6; + n = numVertices / 7; - // 4 vertices per coordinate, with 6 values per vertex + // 4 vertices per coordinate, with 7 values per vertex this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; @@ -210,6 +218,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = -2 * (height - anchorY); this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; @@ -217,6 +226,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = -2 * (height - anchorY); this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; @@ -224,6 +234,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = 2 * anchorY; this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; @@ -231,6 +242,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = 2 * anchorY; this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = opacity; this.indices_[numIndices++] = n; this.indices_[numIndices++] = n + 1; @@ -371,6 +383,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { this.imageHeight_ = undefined; this.imageWidth_ = undefined; this.indices_ = null; + this.opacity_ = undefined; this.originX_ = undefined; this.originY_ = undefined; this.vertices_ = null; @@ -415,15 +428,19 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.enableVertexAttribArray(locations.a_position); gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT, - false, 24, 0); + false, 28, 0); gl.enableVertexAttribArray(locations.a_offsets); gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT, - false, 24, 8); + false, 28, 8); gl.enableVertexAttribArray(locations.a_texCoord); gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT, - false, 24, 16); + false, 28, 16); + + gl.enableVertexAttribArray(locations.a_opacity); + gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT, + false, 28, 24); gl.uniformMatrix4fv(locations.u_projectionMatrix, false, transform); gl.uniformMatrix2fv(locations.u_sizeMatrix, false, @@ -461,6 +478,8 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { // FIXME getImageSize does not exist for circles var imageSize = imageStyle.getImageSize(); goog.asserts.assert(!goog.isNull(imageSize)); + var opacity = imageStyle.getOpacity(); + goog.asserts.assert(goog.isDef(opacity)); var origin = imageStyle.getOrigin(); goog.asserts.assert(!goog.isNull(origin)); var size = imageStyle.getSize(); @@ -482,6 +501,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { this.height_ = size[1]; this.imageHeight_ = imageSize[1]; this.imageWidth_ = imageSize[0]; + this.opacity_ = opacity; this.originX_ = origin[0]; this.originY_ = origin[1]; this.width_ = size[0]; From 28e51740c62c1e9ddfe85575c3b2469e1b0574f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Nov 2014 17:44:18 +0100 Subject: [PATCH 19/98] Address precision/jitter problems Address precision/jitter problems by using coordinates relative to the Replay max extent rather that the world. --- src/ol/render/webgl/webglreplay.js | 67 +++++++++++++++---- .../webgl/webglvectorlayerrenderer.js | 16 ++--- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index c5f8d144f0..f3e5f2973a 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -3,9 +3,11 @@ goog.provide('ol.render.webgl.ReplayGroup'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.object'); +goog.require('goog.vec.Mat4'); goog.require('ol.extent'); goog.require('ol.render.IReplayGroup'); goog.require('ol.render.webgl.imagereplay.shader'); +goog.require('ol.vec.Mat4'); @@ -13,10 +15,11 @@ goog.require('ol.render.webgl.imagereplay.shader'); * @constructor * @implements {ol.render.IVectorContext} * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. * @protected * @struct */ -ol.render.webgl.ImageReplay = function(tolerance) { +ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { /** * @type {number|undefined} @@ -30,6 +33,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.anchorY_ = undefined; + /** + * @private + * @type {ol.Extent} + */ + this.origin_ = ol.extent.getBottomLeft(maxExtent); + /** * @type {ol.Extent} * @private @@ -109,6 +118,12 @@ ol.render.webgl.ImageReplay = function(tolerance) { */ this.originY_ = undefined; + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** * @type {Array.} * @private @@ -205,8 +220,8 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var numVertices = this.vertices_.length; var i, x, y, n; for (i = offset; i < end; i += stride) { - x = flatCoordinates[i]; - y = flatCoordinates[i + 1]; + x = flatCoordinates[i] - this.origin_[0]; + y = flatCoordinates[i + 1] - this.origin_[1]; n = numVertices / 7; @@ -401,15 +416,19 @@ ol.render.webgl.ImageReplay.prototype.getExtent = function() { /** * @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 {ol.Extent} extent Extent. * @param {number} pixelRatio Pixel ratio. - * @param {Array.} size Size. - * @param {goog.vec.Mat4.Number} transform Transform. * @param {Object} skippedFeaturesHash Ids of features to skip. * @return {T|undefined} Callback result. * @template T */ ol.render.webgl.ImageReplay.prototype.replay = function(context, - pixelRatio, size, transform, skippedFeaturesHash) { + center, resolution, rotation, size, extent, pixelRatio, + skippedFeaturesHash) { var gl = context.getGL(); var program = context.getProgram( @@ -422,6 +441,14 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl, program); } + var projectionMatrix = this.projectionMatrix_; + ol.vec.Mat4.makeTransform2D(projectionMatrix, + 0.0, 0.0, + 2 / (resolution * size[0]), + 2 / (resolution * size[1]), + -rotation, + -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1])); + var locations = this.locations_; gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); @@ -442,7 +469,7 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT, false, 28, 24); - gl.uniformMatrix4fv(locations.u_projectionMatrix, false, transform); + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix); gl.uniformMatrix2fv(locations.u_sizeMatrix, false, new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); @@ -519,9 +546,16 @@ ol.render.webgl.ImageReplay.prototype.setTextStyle = goog.abstractMethod; * @constructor * @implements {ol.render.IReplayGroup} * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. * @struct */ -ol.render.webgl.ReplayGroup = function(tolerance) { +ol.render.webgl.ReplayGroup = function(tolerance, maxExtent) { + + /** + * @type {ol.Extent} + * @private + */ + this.maxExtent_ = maxExtent; /** * @type {number} @@ -572,7 +606,7 @@ ol.render.webgl.ReplayGroup.prototype.getReplay = if (!goog.isDef(replay)) { var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType]; goog.asserts.assert(goog.isDef(constructor)); - replay = new constructor(this.tolerance_); + replay = new constructor(this.tolerance_, this.maxExtent_); this.replays_[replayType] = replay; } return replay; @@ -589,23 +623,27 @@ ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { /** * @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 {ol.Extent} extent Extent. * @param {number} pixelRatio Pixel ratio. - * @param {Array.} size Size. - * @param {goog.vec.Mat4.Number} transform Transform. * @param {Object} skippedFeaturesHash Ids of features to skip. * @return {T|undefined} Callback result. * @template T */ ol.render.webgl.ReplayGroup.prototype.replay = function(context, - extent, pixelRatio, size, transform, skippedFeaturesHash) { + center, resolution, rotation, size, extent, pixelRatio, + 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) && ol.extent.intersects(extent, replay.getExtent())) { result = replay.replay(context, - pixelRatio, size, transform, skippedFeaturesHash); + center, resolution, rotation, size, extent, pixelRatio, + skippedFeaturesHash); if (result) { return result; } @@ -619,7 +657,8 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, * @const * @private * @type {Object.} + * function(new: ol.render.webgl.ImageReplay, number, + * ol.Extent)>} */ ol.render.webgl.BATCH_CONSTRUCTORS_ = { 'Image': ol.render.webgl.ImageReplay diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index 78355ec7ac..c9a869dd35 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -9,7 +9,6 @@ goog.require('ol.layer.Vector'); goog.require('ol.render.webgl.ReplayGroup'); goog.require('ol.renderer.vector'); goog.require('ol.renderer.webgl.Layer'); -goog.require('ol.vec.Mat4'); @@ -68,20 +67,12 @@ goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); */ ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) { - var viewState = frameState.viewState; - ol.vec.Mat4.makeTransform2D(this.projectionMatrix, - 0.0, 0.0, - 2 / (viewState.resolution * frameState.size[0]), - 2 / (viewState.resolution * frameState.size[1]), - -viewState.rotation, - -viewState.center[0], -viewState.center[1]); - var replayGroup = this.replayGroup_; if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { replayGroup.replay(context, - frameState.extent, frameState.pixelRatio, frameState.size, - this.projectionMatrix, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.extent, frameState.pixelRatio, frameState.skippedFeatureUids); } @@ -160,7 +151,8 @@ ol.renderer.webgl.VectorLayer.prototype.prepareFrame = this.dirty_ = false; var replayGroup = new ol.render.webgl.ReplayGroup( - ol.renderer.vector.getTolerance(resolution, pixelRatio)); + ol.renderer.vector.getTolerance(resolution, pixelRatio), + extent); vectorSource.loadFeatures(extent, resolution, projection); var renderFeature = /** From 7618c96c29fb46b65c476ea771195b6aa76647d0 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Mon, 3 Nov 2014 18:00:57 +0100 Subject: [PATCH 20/98] Double the size when creating new atlases --- src/ol/renderer/webgl/webglatlasmanager.js | 80 +++++++++---------- .../renderer/webgl/webglatlasmanager.test.js | 19 ++++- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js index cf8163cd44..c43ff9dc69 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -7,50 +7,46 @@ goog.require('goog.dom.TagName'); goog.require('goog.object'); - /** - * @constructor - * @struct - * @param {number} offsetX - * @param {number} offsetY - * @param {HTMLCanvasElement} image + * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}} */ -ol.renderer.webgl.AtlasInfo = function(offsetX, offsetY, image) { - - /** - * @type {number} - */ - this.offsetX = offsetX; - - /** - * @type {number} - */ - this.offsetY = offsetY; - - /** - * @type {HTMLCanvasElement} - */ - this.image = image; -}; +ol.renderer.webgl.AtlasInfo; /** + * Manages the creation of texture atlases. + * + * Images added to this manager will be inserted into an atlas, which + * will be used for rendering. + * The `size` given in the constructor is the size for the first + * atlas. After that, when new atlases are created, they will have + * twice the size as the latest atlas (until `maxSize` is reached.) + * * @constructor * @struct - * @param {number=} opt_size The size in pixels of the sprite images + * @param {number=} opt_size The size in pixels of the first atlas image * (default: 256). + * @param {number=} opt_maxSize The maximum size in pixels of atlas images + * (default: 2048). * @param {number=} opt_space The space in pixels between images * (default: 1). */ -ol.renderer.webgl.AtlasManager = function(opt_size, opt_space) { +ol.renderer.webgl.AtlasManager = function(opt_size, opt_maxSize, opt_space) { /** - * The size in pixels of the sprite images. + * The size in pixels of the latest atlas image. * @private * @type {number} */ - this.size_ = goog.isDef(opt_size) ? opt_size : 256; + this.currentSize_ = goog.isDef(opt_size) ? opt_size : 256; + + /** + * The maximum size in pixels of atlas images. + * @private + * @type {number} + */ + this.maxSize_ = goog.isDef(opt_maxSize) ? opt_maxSize : 2048; /** * The size in pixels between images. @@ -63,7 +59,7 @@ ol.renderer.webgl.AtlasManager = function(opt_size, opt_space) { * @private * @type {Array.} */ - this.atlases_ = [new ol.renderer.webgl.Atlas(this.size_, this.space_)]; + this.atlases_ = [new ol.renderer.webgl.Atlas(this.currentSize_, this.space_)]; }; @@ -101,8 +97,9 @@ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { */ ol.renderer.webgl.AtlasManager.prototype.add = function(hash, width, height, renderCallback, opt_this) { - goog.asserts.assert(width <= this.size_ && height <= this.size_, - 'the entry is too big for the current atlas size'); + if (width > this.maxSize_ || height > this.maxSize_) { + return null; + } var atlas, info; for (var i = 0, l = this.atlases_.length; i < l; i++) { @@ -110,15 +107,15 @@ ol.renderer.webgl.AtlasManager.prototype.add = info = atlas.add(hash, width, height, renderCallback, opt_this); if (info !== null) { return info; + } else { + // the entry could not be added to one of the existing atlases, + // create a new atlas that is twice as big and try to add to this one. + this.currentSize_ = Math.min(this.currentSize_ * 2, this.maxSize_); + atlas = new ol.renderer.webgl.Atlas(this.currentSize_, this.space_); + this.atlases_.push(atlas); + l++; } } - - // the entry could not be added to one of the existing atlases, - // create a new atlas and add to this one. - // TODO double the size and check for max. size? - atlas = new ol.renderer.webgl.Atlas(this.size_, this.space_); - this.atlases_.push(atlas); - return atlas.add(hash, width, height, renderCallback, opt_this); }; @@ -205,8 +202,11 @@ ol.renderer.webgl.Atlas.prototype.add = if (block.width >= width + this.space_ && block.height >= height + this.space_) { // we found a block that is big enough for our entry - var entry = new ol.renderer.webgl.AtlasInfo( - block.x + this.space_, block.y + this.space_, this.canvas_); + var entry = { + offsetX: block.x + this.space_, + offsetY: block.y + this.space_, + image: this.canvas_ + }; this.entries_[hash] = entry; // render the image on the atlas image @@ -301,6 +301,6 @@ ol.renderer.webgl.Atlas.prototype.updateBlocks_ = /** - * @typedef{{x: number, y: number, width: number, height: number}} + * @typedef {{x: number, y: number, width: number, height: number}} */ ol.renderer.webgl.Atlas.Block; diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js index 0fb05a8d19..f47520a449 100644 --- a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js +++ b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js @@ -198,9 +198,26 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('creates a new atlas if needed', function() { var manager = new ol.renderer.webgl.AtlasManager(128); expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); - expect(manager.add(2, 100, 100, defaultRender)).to.be.ok(); + var info = manager.add(2, 100, 100, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(256); expect(manager.atlases_).to.have.length(2); }); + + it('creates new atlases until one is large enough', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + var info = manager.add(2, 500, 500, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(512); + expect(manager.atlases_).to.have.length(3); + }); + + it('returns null if the size exceeds the maximum size', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + expect(manager.add(2, 3000, 3000, defaultRender)).to.eql(null); + }); }); describe('#getInfo', function() { From 8415a0c8ba6b92420998389ecc22274ad76fc455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Tue, 4 Nov 2014 08:48:43 +0100 Subject: [PATCH 21/98] Fix type for image replay origin --- src/ol/render/webgl/webglreplay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index f3e5f2973a..66dffe6fde 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -35,7 +35,7 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { /** * @private - * @type {ol.Extent} + * @type {ol.Coordinate} */ this.origin_ = ol.extent.getBottomLeft(maxExtent); From 9029c0fdad062609d8a39e06fa34d9ba032c964c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 6 Nov 2014 09:24:47 +0100 Subject: [PATCH 22/98] Use replay maxExtent center as the coord system origin --- src/ol/render/webgl/webglreplay.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 66dffe6fde..0864f811d2 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -34,10 +34,14 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { this.anchorY_ = undefined; /** + * The origin of the coordinate system for the point coordinates sent to + * the GPU. To eliminate jitter caused by precision problems in the GPU + * we use the "Rendering Relative to Eye" technique described in the "3D + * Engine Design for Virtual Globes" book. * @private * @type {ol.Coordinate} */ - this.origin_ = ol.extent.getBottomLeft(maxExtent); + this.origin_ = ol.extent.getCenter(maxExtent); /** * @type {ol.Extent} From 64a7cdf372958d1a3b78bf1ee846af5697062e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 6 Nov 2014 09:35:47 +0100 Subject: [PATCH 23/98] Implement getImageSize in ol.style.Circle and RegularShape --- src/ol/render/webgl/webglreplay.js | 1 - src/ol/style/circlestyle.js | 8 ++++++++ src/ol/style/imagestyle.js | 6 ++++++ src/ol/style/regularshapestyle.js | 8 ++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 0864f811d2..3bfbe613fa 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -506,7 +506,6 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { goog.asserts.assert(!goog.isNull(anchor)); var image = imageStyle.getImage(1); goog.asserts.assert(!goog.isNull(image)); - // FIXME getImageSize does not exist for circles var imageSize = imageStyle.getImageSize(); goog.asserts.assert(!goog.isNull(imageSize)); var opacity = imageStyle.getOpacity(); diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 0d209ba876..6c3490e17e 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -137,6 +137,14 @@ ol.style.Circle.prototype.getImageState = function() { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getImageSize = function() { + return this.size_; +}; + + /** * @inheritDoc * @api diff --git a/src/ol/style/imagestyle.js b/src/ol/style/imagestyle.js index 35c3a3ac16..bf306ab65c 100644 --- a/src/ol/style/imagestyle.js +++ b/src/ol/style/imagestyle.js @@ -132,6 +132,12 @@ ol.style.Image.prototype.getImage = goog.abstractMethod; ol.style.Image.prototype.getImageState = goog.abstractMethod; +/** + * @return {ol.Size} Image size. + */ +ol.style.Image.prototype.getImageSize = goog.abstractMethod; + + /** * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element. diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js index 74850f1225..526724ee2e 100644 --- a/src/ol/style/regularshapestyle.js +++ b/src/ol/style/regularshapestyle.js @@ -147,6 +147,14 @@ ol.style.RegularShape.prototype.getImage = function(pixelRatio) { }; +/** + * @inheritDoc + */ +ol.style.RegularShape.prototype.getImageSize = function() { + return this.size_; +}; + + /** * @inheritDoc */ From 43756a2d592147a306d9bf5b705c12b4e8b5c3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 6 Nov 2014 09:47:24 +0100 Subject: [PATCH 24/98] Support image scale --- examples/icon-sprite-webgl.js | 14 ++++++++------ src/ol/render/webgl/webglreplay.js | 28 ++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index f036b398c2..03886ffec4 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -9,9 +9,9 @@ goog.require('ol.style.Style'); var iconInfo = [ - {size: [55, 55], offset: [0, 0], opacity: 1.0}, - {size: [55, 55], offset: [110, 86], opacity: 0.75}, - {size: [55, 86], offset: [55, 0], opacity: 0.5} + {size: [55, 55], offset: [0, 0], opacity: 1.0, scale: 1.0}, + {size: [55, 55], offset: [110, 86], opacity: 0.75, scale: 1.25}, + {size: [55, 86], offset: [55, 0], opacity: 0.5, scale: 1.5} ]; var i; @@ -19,11 +19,13 @@ var i; var iconCount = iconInfo.length; var icons = new Array(iconCount); for (i = 0; i < iconCount; ++i) { + var info = iconInfo[i]; icons[i] = new ol.style.Icon({ src: 'data/Butterfly.png', - size: iconInfo[i].size, - offset: iconInfo[i].offset, - opacity: iconInfo[i].opacity + size: info.size, + offset: info.offset, + opacity: info.opacity, + scale: info.scale }); } diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 3bfbe613fa..8725e1cbe5 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -128,6 +128,12 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** + * @private + * @type {number|undefined} + */ + this.scale_ = undefined; + /** * @type {Array.} * @private @@ -210,6 +216,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = goog.asserts.assert(goog.isDef(this.opacity_)); goog.asserts.assert(goog.isDef(this.originX_)); goog.asserts.assert(goog.isDef(this.originY_)); + goog.asserts.assert(goog.isDef(this.scale_)); goog.asserts.assert(goog.isDef(this.width_)); var anchorX = this.anchorX_; var anchorY = this.anchorY_; @@ -219,6 +226,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var opacity = this.opacity_; var originX = this.originX_; var originY = this.originY_; + var scale = this.scale_; var width = this.width_; var numIndices = this.indices_.length; var numVertices = this.vertices_.length; @@ -233,32 +241,32 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -2 * anchorX; - this.vertices_[numVertices++] = -2 * (height - anchorY); + this.vertices_[numVertices++] = -2 * scale * anchorX; + this.vertices_[numVertices++] = -2 * scale * (height - anchorY); this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = 2 * (width - anchorX); - this.vertices_[numVertices++] = -2 * (height - anchorY); + this.vertices_[numVertices++] = 2 * scale * (width - anchorX); + this.vertices_[numVertices++] = -2 * scale * (height - anchorY); this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = 2 * (width - anchorX); - this.vertices_[numVertices++] = 2 * anchorY; + this.vertices_[numVertices++] = 2 * scale * (width - anchorX); + this.vertices_[numVertices++] = 2 * scale * anchorY; this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -2 * anchorX; - this.vertices_[numVertices++] = 2 * anchorY; + this.vertices_[numVertices++] = -2 * scale * anchorX; + this.vertices_[numVertices++] = 2 * scale * anchorY; this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; @@ -405,6 +413,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { this.opacity_ = undefined; this.originX_ = undefined; this.originY_ = undefined; + this.scale_ = undefined; this.vertices_ = null; this.width_ = undefined; }; @@ -514,6 +523,8 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { goog.asserts.assert(!goog.isNull(origin)); var size = imageStyle.getSize(); goog.asserts.assert(!goog.isNull(size)); + var scale = imageStyle.getScale(); + goog.asserts.assert(goog.isDef(scale)); if (this.images_.length === 0) { this.images_.push(image); @@ -534,6 +545,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { this.opacity_ = opacity; this.originX_ = origin[0]; this.originY_ = origin[1]; + this.scale_ = scale; this.width_ = size[0]; }; From 581b372c6a9fd0e5c50fef973046efcc484d2c9e Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 10:24:14 +0100 Subject: [PATCH 25/98] Add constant `ol.has.WEBGL_MAX_TEXTURE_SIZE` --- src/ol/has.js | 43 +++++++++++++++------- src/ol/renderer/webgl/webglatlasmanager.js | 3 ++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/ol/has.js b/src/ol/has.js index 1472c8b53c..63745b48ac 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -113,27 +113,42 @@ ol.has.POINTER = 'PointerEvent' in goog.global; ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled); +/** + * The maximum supported WebGL texture size in pixels. If WebGL is not + * supported, the value is set to `-1`. + * @const + * @type {number} + */ +ol.has.WEBGL_MAX_TEXTURE_SIZE; + + /** * True if browser supports WebGL. * @const * @type {boolean} * @api stable */ -ol.has.WEBGL = ol.ENABLE_WEBGL && ( - /** - * @return {boolean} WebGL supported. - */ - function() { - if (!('WebGLRenderingContext' in goog.global)) { - return false; - } +ol.has.WEBGL; + + +(function() { + if (ol.ENABLE_WEBGL) { + var hasWebGL = false, textureSize = -1; + if ('WebGLRenderingContext' in goog.global) { try { var canvas = /** @type {HTMLCanvasElement} */ (goog.dom.createElement(goog.dom.TagName.CANVAS)); - return !goog.isNull(ol.webgl.getContext(canvas, { + var gl = ol.webgl.getContext(canvas, { failIfMajorPerformanceCaveat: true - })); - } catch (e) { - return false; - } - })(); + }); + if (!goog.isNull(gl)) { + hasWebGL = true; + textureSize = /** @type {number} */ + (gl.getParameter(gl.MAX_TEXTURE_SIZE)); + } + } catch (e) {} + } + ol.has.WEBGL = hasWebGL; + ol.has.WEBGL_MAX_TEXTURE_SIZE = textureSize; + } +})(); diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js index c43ff9dc69..a9cc08689d 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -23,6 +23,9 @@ ol.renderer.webgl.AtlasInfo; * atlas. After that, when new atlases are created, they will have * twice the size as the latest atlas (until `maxSize` is reached.) * + * It is recommended to use `ol.has.WEBGL_MAX_TEXTURE_SIZE` as + * `maxSize` value. + * * @constructor * @struct * @param {number=} opt_size The size in pixels of the first atlas image From 509fbaee1c7cee9dc04dcd576085d7458be38981 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 15:08:34 +0100 Subject: [PATCH 26/98] Replace hashCode with checksum Hash codes are not collision free, so what we actually need is a checksum. --- src/ol/structs/checksum.js | 22 +++++++++ src/ol/structs/hashable.js | 16 ------- src/ol/style/circlestyle.js | 36 ++++++++++----- src/ol/style/fillstyle.js | 24 ++++++---- src/ol/style/strokestyle.js | 54 ++++++++++++++-------- test/spec/ol/style/circlestyle.test.js | 64 +++++++++++++++++++++++--- 6 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 src/ol/structs/checksum.js delete mode 100644 src/ol/structs/hashable.js diff --git a/src/ol/structs/checksum.js b/src/ol/structs/checksum.js new file mode 100644 index 0000000000..423da745cf --- /dev/null +++ b/src/ol/structs/checksum.js @@ -0,0 +1,22 @@ +goog.provide('ol.structs.IHasChecksum'); + + +/** + * @typedef {string} + */ +ol.structs.Checksum; + + + +/** + * @interface + */ +ol.structs.IHasChecksum = function() { +}; + + +/** + * @return {string} The checksum. + */ +ol.structs.IHasChecksum.prototype.getChecksum = function() { +}; diff --git a/src/ol/structs/hashable.js b/src/ol/structs/hashable.js deleted file mode 100644 index 371beafa6c..0000000000 --- a/src/ol/structs/hashable.js +++ /dev/null @@ -1,16 +0,0 @@ -goog.provide('ol.structs.IHashable'); - - - -/** - * @interface - */ -ol.structs.IHashable = function() { -}; - - -/** - * @return {number} The hash code. - */ -ol.structs.IHashable.prototype.hashCode = function() { -}; diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 235d066290..6a52f22ba6 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -2,10 +2,9 @@ goog.provide('ol.style.Circle'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); -goog.require('goog.string'); goog.require('ol.color'); goog.require('ol.render.canvas'); -goog.require('ol.structs.IHashable'); +goog.require('ol.structs.IHasChecksum'); goog.require('ol.style.Fill'); goog.require('ol.style.Image'); goog.require('ol.style.ImageState'); @@ -20,7 +19,7 @@ goog.require('ol.style.Stroke'); * @constructor * @param {olx.style.CircleOptions=} opt_options Options. * @extends {ol.style.Image} - * @implements {ol.structs.IHashable} + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Circle = function(opt_options) { @@ -78,6 +77,12 @@ ol.style.Circle = function(opt_options) { */ this.size_ = [size, size]; + /** + * @private + * @type {Array.|null} + */ + this.checksums_ = null; + /** * @type {boolean} */ @@ -268,15 +273,22 @@ ol.style.Circle.prototype.render_ = function() { /** * @inheritDoc */ -ol.style.Circle.prototype.hashCode = function() { - var hash = 17; +ol.style.Circle.prototype.getChecksum = function() { + var strokeChecksum = !goog.isNull(this.stroke_) ? + this.stroke_.getChecksum() : '-'; + var fillChecksum = !goog.isNull(this.fill_) ? + this.fill_.getChecksum() : '-'; - hash = hash * 23 + (!goog.isNull(this.stroke_) ? - this.stroke_.hashCode() : 0); - hash = hash * 23 + (!goog.isNull(this.fill_) ? - this.fill_.hashCode() : 0); - hash = hash * 23 + (goog.isDef(this.radius_) ? - goog.string.hashCode(this.radius_.toString()) : 0); + var recalculate = goog.isNull(this.checksums_) || + (strokeChecksum != this.checksums_[1] || + fillChecksum != this.checksums_[2] || + this.radius_ != this.checksums_[3]); - return hash; + if (recalculate) { + var checksum = 'c' + strokeChecksum + fillChecksum + + (goog.isDef(this.radius_) ? this.radius_.toString() : '-'); + this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_]; + } + + return this.checksums_[0]; }; diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js index 65091029a7..a54d8d2498 100644 --- a/src/ol/style/fillstyle.js +++ b/src/ol/style/fillstyle.js @@ -1,8 +1,7 @@ goog.provide('ol.style.Fill'); -goog.require('goog.string'); goog.require('ol.color'); -goog.require('ol.structs.IHashable'); +goog.require('ol.structs.IHasChecksum'); @@ -12,7 +11,7 @@ goog.require('ol.structs.IHashable'); * * @constructor * @param {olx.style.FillOptions=} opt_options Options. - * @implements {ol.structs.IHashable} + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Fill = function(opt_options) { @@ -24,6 +23,12 @@ ol.style.Fill = function(opt_options) { * @type {ol.Color|string} */ this.color_ = goog.isDef(options.color) ? options.color : null; + + /** + * @private + * @type {?ol.structs.Checksum} + */ + this.checksum_ = null; }; @@ -44,17 +49,18 @@ ol.style.Fill.prototype.getColor = function() { */ ol.style.Fill.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = null; }; /** * @inheritDoc */ -ol.style.Fill.prototype.hashCode = function() { - var hash = 17; +ol.style.Fill.prototype.getChecksum = function() { + if (goog.isNull(this.checksum_)) { + this.checksum_ = 'f' + (!goog.isNull(this.color_) ? + ol.color.asString(this.color_) : '-'); + } - hash = hash * 23 + (!goog.isNull(this.color_) ? - goog.string.hashCode(ol.color.asString(this.color_)) : 0); - - return hash; + return this.checksum_; }; diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js index 934490f3b1..419e771cd3 100644 --- a/src/ol/style/strokestyle.js +++ b/src/ol/style/strokestyle.js @@ -1,8 +1,9 @@ goog.provide('ol.style.Stroke'); -goog.require('goog.string'); +goog.require('goog.crypt'); +goog.require('goog.crypt.Md5'); goog.require('ol.color'); -goog.require('ol.structs.IHashable'); +goog.require('ol.structs.IHasChecksum'); @@ -15,7 +16,7 @@ goog.require('ol.structs.IHashable'); * * @constructor * @param {olx.style.StrokeOptions=} opt_options Options. - * @implements {ol.structs.IHashable} + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Stroke = function(opt_options) { @@ -57,6 +58,12 @@ ol.style.Stroke = function(opt_options) { * @type {number|undefined} */ this.width_ = options.width; + + /** + * @private + * @type {?ol.structs.Checksum} + */ + this.checksum_ = null; }; @@ -122,6 +129,7 @@ ol.style.Stroke.prototype.getWidth = function() { */ ol.style.Stroke.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = null; }; @@ -133,6 +141,7 @@ ol.style.Stroke.prototype.setColor = function(color) { */ ol.style.Stroke.prototype.setLineCap = function(lineCap) { this.lineCap_ = lineCap; + this.checksum_ = null; }; @@ -144,6 +153,7 @@ ol.style.Stroke.prototype.setLineCap = function(lineCap) { */ ol.style.Stroke.prototype.setLineDash = function(lineDash) { this.lineDash_ = lineDash; + this.checksum_ = null; }; @@ -155,6 +165,7 @@ ol.style.Stroke.prototype.setLineDash = function(lineDash) { */ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { this.lineJoin_ = lineJoin; + this.checksum_ = null; }; @@ -166,6 +177,7 @@ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { */ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { this.miterLimit_ = miterLimit; + this.checksum_ = null; }; @@ -177,27 +189,33 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { */ ol.style.Stroke.prototype.setWidth = function(width) { this.width_ = width; + this.checksum_ = null; }; /** * @inheritDoc */ -ol.style.Stroke.prototype.hashCode = function() { - var hash = 17; +ol.style.Stroke.prototype.getChecksum = function() { + if (goog.isNull(this.checksum_)) { + var raw = 's' + + (!goog.isNull(this.color_) ? + ol.color.asString(this.color_) : '-') + ',' + + (goog.isDef(this.lineCap_) ? + this.lineCap_.toString() : '-') + ',' + + (!goog.isNull(this.lineDash_) ? + this.lineDash_.toString() : '-') + ',' + + (goog.isDef(this.lineJoin_) ? + this.lineJoin_ : '-') + ',' + + (goog.isDef(this.miterLimit_) ? + this.miterLimit_.toString() : '-') + ',' + + (goog.isDef(this.width_) ? + this.width_.toString() : '-'); - hash = hash * 23 + (!goog.isNull(this.color_) ? - goog.string.hashCode(ol.color.asString(this.color_)) : 0); - hash = hash * 23 + (goog.isDef(this.lineCap_) ? - goog.string.hashCode(this.lineCap_.toString()) : 0); - hash = hash * 23 + (!goog.isNull(this.lineDash_) ? - goog.string.hashCode(this.lineDash_.toString()) : 0); - hash = hash * 23 + (goog.isDef(this.lineJoin_) ? - goog.string.hashCode(this.lineJoin_) : 0); - hash = hash * 23 + (goog.isDef(this.miterLimit_) ? - goog.string.hashCode(this.miterLimit_.toString()) : 0); - hash = hash * 23 + (goog.isDef(this.width_) ? - goog.string.hashCode(this.width_.toString()) : 0); + var md5 = new goog.crypt.Md5(); + md5.update(raw); + this.checksum_ = goog.crypt.byteArrayToString(md5.digest()); + } - return hash; + return this.checksum_; }; diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js index 566164e199..00eb1765a6 100644 --- a/test/spec/ol/style/circlestyle.test.js +++ b/test/spec/ol/style/circlestyle.test.js @@ -3,12 +3,12 @@ goog.provide('ol.test.style.Circle'); describe('ol.style.Circle', function() { - describe('#hashCode', function() { + describe('#getChecksum', function() { it('calculates the same hash code for default options', function() { var style1 = new ol.style.Circle(); var style2 = new ol.style.Circle(); - expect(style1.hashCode()).to.eql(style2.hashCode()); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); }); it('calculates not the same hash code (radius)', function() { @@ -16,7 +16,7 @@ describe('ol.style.Circle', function() { var style2 = new ol.style.Circle({ radius: 5 }); - expect(style1.hashCode()).to.not.eql(style2.hashCode()); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); }); it('calculates the same hash code (radius)', function() { @@ -26,7 +26,7 @@ describe('ol.style.Circle', function() { var style2 = new ol.style.Circle({ radius: 5 }); - expect(style1.hashCode()).to.eql(style2.hashCode()); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); }); it('calculates not the same hash code (color)', function() { @@ -42,7 +42,7 @@ describe('ol.style.Circle', function() { color: '#319FD3' }) }); - expect(style1.hashCode()).to.not.eql(style2.hashCode()); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); }); it('calculates the same hash code (everything set)', function() { @@ -74,7 +74,7 @@ describe('ol.style.Circle', function() { width: 2 }) }); - expect(style1.hashCode()).to.eql(style2.hashCode()); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); }); it('calculates not the same hash code (stroke width differs)', function() { @@ -106,7 +106,57 @@ describe('ol.style.Circle', function() { width: 2 }) }); - expect(style1.hashCode()).to.not.eql(style2.hashCode()); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('invalidates a cached checksum if values change (fill)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + + style1.getFill().setColor('red'); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('invalidates a cached checksum if values change (stroke)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + + style1.getStroke().setWidth(4); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); }); }); From fb24c68b9c2add96bf9098f5775d590cbe123bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 6 Nov 2014 15:27:25 +0100 Subject: [PATCH 27/98] Support image rotation --- examples/icon-sprite-webgl.js | 27 ++++++++++++++----- src/ol/render/webgl/webglreplay.js | 42 +++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 03886ffec4..f8d9e219b3 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -8,11 +8,25 @@ goog.require('ol.style.Icon'); goog.require('ol.style.Style'); -var iconInfo = [ - {size: [55, 55], offset: [0, 0], opacity: 1.0, scale: 1.0}, - {size: [55, 55], offset: [110, 86], opacity: 0.75, scale: 1.25}, - {size: [55, 86], offset: [55, 0], opacity: 0.5, scale: 1.5} -]; +var iconInfo = [{ + size: [55, 55], + offset: [0, 0], + opacity: 1.0, + scale: 1.0, + rotation: 0.0 +}, { + size: [55, 55], + offset: [110, 86], + opacity: 0.75, + scale: 1.25, + rotation: Math.PI / 2.0 +}, { + size: [55, 86], + offset: [55, 0], + opacity: 0.5, + scale: 1.5, + rotation: Math.PI / 3.0 +}]; var i; @@ -25,7 +39,8 @@ for (i = 0; i < iconCount; ++i) { size: info.size, offset: info.offset, opacity: info.opacity, - scale: info.scale + scale: info.scale, + rotation: info.rotation }); } diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 8725e1cbe5..d01a4b777e 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -128,6 +128,12 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** + * @private + * @type {number|undefined} + */ + this.rotation_ = undefined; + /** * @private * @type {number|undefined} @@ -216,6 +222,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = goog.asserts.assert(goog.isDef(this.opacity_)); goog.asserts.assert(goog.isDef(this.originX_)); goog.asserts.assert(goog.isDef(this.originY_)); + goog.asserts.assert(goog.isDef(this.rotation_)); goog.asserts.assert(goog.isDef(this.scale_)); goog.asserts.assert(goog.isDef(this.width_)); var anchorX = this.anchorX_; @@ -226,11 +233,14 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var opacity = this.opacity_; var originX = this.originX_; var originY = this.originY_; + var rotation = this.rotation_; var scale = this.scale_; var width = this.width_; + var cos = Math.cos(rotation); + var sin = Math.sin(rotation); var numIndices = this.indices_.length; var numVertices = this.vertices_.length; - var i, x, y, n; + var i, n, offsetX, offsetY, x, y; for (i = offset; i < end; i += stride) { x = flatCoordinates[i] - this.origin_[0]; y = flatCoordinates[i + 1] - this.origin_[1]; @@ -239,34 +249,42 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = // 4 vertices per coordinate, with 7 values per vertex + offsetX = -scale * anchorX; + offsetY = -scale * (height - anchorY); this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -2 * scale * anchorX; - this.vertices_[numVertices++] = -2 * scale * (height - anchorY); + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; + offsetX = scale * (width - anchorX); + offsetY = -scale * (height - anchorY); this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = 2 * scale * (width - anchorX); - this.vertices_[numVertices++] = -2 * scale * (height - anchorY); + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; + offsetX = scale * (width - anchorX); + offsetY = scale * anchorY; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = 2 * scale * (width - anchorX); - this.vertices_[numVertices++] = 2 * scale * anchorY; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; + offsetX = -scale * anchorX; + offsetY = scale * anchorY; this.vertices_[numVertices++] = x; this.vertices_[numVertices++] = y; - this.vertices_[numVertices++] = -2 * scale * anchorX; - this.vertices_[numVertices++] = 2 * scale * anchorY; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; @@ -413,6 +431,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { this.opacity_ = undefined; this.originX_ = undefined; this.originY_ = undefined; + this.rotation_ = undefined; this.scale_ = undefined; this.vertices_ = null; this.width_ = undefined; @@ -484,7 +503,7 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix); gl.uniformMatrix2fv(locations.u_sizeMatrix, false, - new Float32Array([1 / size[0], 0.0, 0.0, 1 / size[1]])); + new Float32Array([2 / size[0], 0.0, 0.0, 2 / size[1]])); gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); @@ -521,6 +540,8 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { goog.asserts.assert(goog.isDef(opacity)); var origin = imageStyle.getOrigin(); goog.asserts.assert(!goog.isNull(origin)); + var rotation = imageStyle.getRotation(); + goog.asserts.assert(goog.isDef(rotation)); var size = imageStyle.getSize(); goog.asserts.assert(!goog.isNull(size)); var scale = imageStyle.getScale(); @@ -545,6 +566,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { this.opacity_ = opacity; this.originX_ = origin[0]; this.originY_ = origin[1]; + this.rotation_ = rotation; this.scale_ = scale; this.width_ = size[0]; }; From 5ba6ddcecf54cf7c5f214609020f2fd8d111cf38 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 15:28:44 +0100 Subject: [PATCH 28/98] Only create new atlas after testing all existing --- src/ol/renderer/webgl/webglatlasmanager.js | 10 +++++----- .../spec/ol/renderer/webgl/webglatlasmanager.test.js | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js index a9cc08689d..1d1dd67592 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -72,7 +72,7 @@ ol.renderer.webgl.AtlasManager = function(opt_size, opt_maxSize, opt_space) { */ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { var atlas, info; - for (var i = 0, l = this.atlases_.length; i < l; i++) { + for (var i = 0, ii = this.atlases_.length; i < ii; i++) { atlas = this.atlases_[i]; info = atlas.get(hash); if (info !== null) { @@ -105,18 +105,18 @@ ol.renderer.webgl.AtlasManager.prototype.add = } var atlas, info; - for (var i = 0, l = this.atlases_.length; i < l; i++) { + for (var i = 0, ii = this.atlases_.length; i < ii; i++) { atlas = this.atlases_[i]; info = atlas.add(hash, width, height, renderCallback, opt_this); if (info !== null) { return info; - } else { + } else if (info === null && i === ii - 1) { // the entry could not be added to one of the existing atlases, // create a new atlas that is twice as big and try to add to this one. this.currentSize_ = Math.min(this.currentSize_ * 2, this.maxSize_); atlas = new ol.renderer.webgl.Atlas(this.currentSize_, this.space_); this.atlases_.push(atlas); - l++; + ii++; } } }; @@ -200,7 +200,7 @@ ol.renderer.webgl.Atlas.prototype.get = function(hash) { ol.renderer.webgl.Atlas.prototype.add = function(hash, width, height, renderCallback, opt_this) { var block; - for (var i = 0, l = this.emptyBlocks_.length; i < l; i++) { + for (var i = 0, ii = this.emptyBlocks_.length; i < ii; i++) { block = this.emptyBlocks_[i]; if (block.width >= width + this.space_ && block.height >= height + this.space_) { diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js index f47520a449..e65bc290c7 100644 --- a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js +++ b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js @@ -207,12 +207,24 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('creates new atlases until one is large enough', function() { var manager = new ol.renderer.webgl.AtlasManager(128); expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + expect(manager.atlases_).to.have.length(1); var info = manager.add(2, 500, 500, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(512); expect(manager.atlases_).to.have.length(3); }); + it('checks all existing atlases and create a new if needed', function() { + var manager = new ol.renderer.webgl.AtlasManager(128); + expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + expect(manager.add(2, 100, 100, defaultRender)).to.be.ok(); + expect(manager.atlases_).to.have.length(2); + var info = manager.add(3, 500, 500, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(512); + expect(manager.atlases_).to.have.length(3); + }); + it('returns null if the size exceeds the maximum size', function() { var manager = new ol.renderer.webgl.AtlasManager(128); expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); From f0841b38cdc6774f15e05cdd8314af6ec6fb9fc7 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 16:22:46 +0100 Subject: [PATCH 29/98] Use id string instead of hash code --- src/ol/renderer/webgl/webglatlasmanager.js | 28 +++--- .../renderer/webgl/webglatlasmanager.test.js | 90 +++++++++---------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js index 1d1dd67592..edf1f5ffdb 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -67,14 +67,14 @@ ol.renderer.webgl.AtlasManager = function(opt_size, opt_maxSize, opt_space) { /** - * @param {number} hash The hash of the entry to check. + * @param {string} id The identifier of the entry to check. * @return {ol.renderer.webgl.AtlasInfo} */ -ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { +ol.renderer.webgl.AtlasManager.prototype.getInfo = function(id) { var atlas, info; for (var i = 0, ii = this.atlases_.length; i < ii; i++) { atlas = this.atlases_[i]; - info = atlas.get(hash); + info = atlas.get(id); if (info !== null) { return info; } @@ -86,10 +86,10 @@ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { /** * Add an image to the atlas manager. * - * If an entry for the given hash already exists, the entry will + * If an entry for the given id already exists, the entry will * be overridden (but the space on the atlas graphic will not be freed). * - * @param {number} hash The hash of the entry to add. + * @param {string} id The identifier of the entry to add. * @param {number} width The width. * @param {number} height The height. * @param {function(*)} renderCallback Called to render the new sprite entry @@ -99,7 +99,7 @@ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(hash) { * @return {ol.renderer.webgl.AtlasInfo} */ ol.renderer.webgl.AtlasManager.prototype.add = - function(hash, width, height, renderCallback, opt_this) { + function(id, width, height, renderCallback, opt_this) { if (width > this.maxSize_ || height > this.maxSize_) { return null; } @@ -107,7 +107,7 @@ ol.renderer.webgl.AtlasManager.prototype.add = var atlas, info; for (var i = 0, ii = this.atlases_.length; i < ii; i++) { atlas = this.atlases_[i]; - info = atlas.add(hash, width, height, renderCallback, opt_this); + info = atlas.add(id, width, height, renderCallback, opt_this); if (info !== null) { return info; } else if (info === null && i === ii - 1) { @@ -179,16 +179,16 @@ ol.renderer.webgl.Atlas = function(size, space) { /** - * @param {number} hash The hash of the entry to check. + * @param {string} id The identifier of the entry to check. * @return {ol.renderer.webgl.AtlasInfo} */ -ol.renderer.webgl.Atlas.prototype.get = function(hash) { - return goog.object.get(this.entries_, hash, null); +ol.renderer.webgl.Atlas.prototype.get = function(id) { + return goog.object.get(this.entries_, id, null); }; /** - * @param {number} hash The hash of the entry to add. + * @param {string} id The identifier of the entry to add. * @param {number} width The width. * @param {number} height The height. * @param {function(*)} renderCallback Called to render the new sprite entry @@ -198,7 +198,7 @@ ol.renderer.webgl.Atlas.prototype.get = function(hash) { * @return {ol.renderer.webgl.AtlasInfo} */ ol.renderer.webgl.Atlas.prototype.add = - function(hash, width, height, renderCallback, opt_this) { + function(id, width, height, renderCallback, opt_this) { var block; for (var i = 0, ii = this.emptyBlocks_.length; i < ii; i++) { block = this.emptyBlocks_[i]; @@ -210,7 +210,7 @@ ol.renderer.webgl.Atlas.prototype.add = offsetY: block.y + this.space_, image: this.canvas_ }; - this.entries_[hash] = entry; + this.entries_[id] = entry; // render the image on the atlas image renderCallback.call(opt_this, this.context_, @@ -240,7 +240,9 @@ ol.renderer.webgl.Atlas.prototype.split_ = var deltaWidth = block.width - width; var deltaHeight = block.height - height; + /** @type {ol.renderer.webgl.AtlasInfo} */ var newBlock1, newBlock2; + if (deltaWidth > deltaHeight) { // split vertically // block right of the inserted entry diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js index e65bc290c7..e9504b6686 100644 --- a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js +++ b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js @@ -19,59 +19,59 @@ describe('ol.renderer.webgl.Atlas', function() { it('adds one entry', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - var info = atlas.add(1, 32, 32, defaultRender); + var info = atlas.add('1', 32, 32, defaultRender); expect(info).to.eql( {offsetX: 1, offsetY: 1, image: atlas.canvas_}); - expect(atlas.get(1)).to.eql(info); + expect(atlas.get('1')).to.eql(info); }); it('adds two entries', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - atlas.add(1, 32, 32, defaultRender); - var info = atlas.add(2, 32, 32, defaultRender); + atlas.add('1', 32, 32, defaultRender); + var info = atlas.add('2', 32, 32, defaultRender); expect(info).to.eql( {offsetX: 34, offsetY: 1, image: atlas.canvas_}); - expect(atlas.get(2)).to.eql(info); + expect(atlas.get('2')).to.eql(info); }); it('adds three entries', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - atlas.add(1, 32, 32, defaultRender); - atlas.add(2, 32, 32, defaultRender); - var info = atlas.add(3, 32, 32, defaultRender); + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + var info = atlas.add('3', 32, 32, defaultRender); expect(info).to.eql( {offsetX: 67, offsetY: 1, image: atlas.canvas_}); - expect(atlas.get(3)).to.eql(info); + expect(atlas.get('3')).to.eql(info); }); it('adds four entries (new row)', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - atlas.add(1, 32, 32, defaultRender); - atlas.add(2, 32, 32, defaultRender); - atlas.add(3, 32, 32, defaultRender); - var info = atlas.add(4, 32, 32, defaultRender); + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + atlas.add('3', 32, 32, defaultRender); + var info = atlas.add('4', 32, 32, defaultRender); expect(info).to.eql( {offsetX: 1, offsetY: 34, image: atlas.canvas_}); - expect(atlas.get(4)).to.eql(info); + expect(atlas.get('4')).to.eql(info); }); it('returns null when an entry is too big', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - atlas.add(1, 32, 32, defaultRender); - atlas.add(2, 32, 32, defaultRender); - atlas.add(3, 32, 32, defaultRender); + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + atlas.add('3', 32, 32, defaultRender); var info = atlas.add(4, 100, 100, defaultRender); expect(info).to.eql(null); @@ -81,11 +81,11 @@ describe('ol.renderer.webgl.Atlas', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); for (var i = 1; i <= 16; i++) { - expect(atlas.add(i, 28, 28, defaultRender)).to.be.ok(); + expect(atlas.add(i.toString(), 28, 28, defaultRender)).to.be.ok(); } // there is no more space for items of this size, the next one will fail - expect(atlas.add(17, 28, 28, defaultRender)).to.eql(null); + expect(atlas.add('17', 28, 28, defaultRender)).to.eql(null); }); }); @@ -94,23 +94,23 @@ describe('ol.renderer.webgl.Atlas', function() { it('adds a bunch of rectangles', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); - expect(atlas.add(1, 64, 32, defaultRender)).to.eql( + expect(atlas.add('1', 64, 32, defaultRender)).to.eql( {offsetX: 1, offsetY: 1, image: atlas.canvas_}); - expect(atlas.add(2, 64, 32, defaultRender)).to.eql( + expect(atlas.add('2', 64, 32, defaultRender)).to.eql( {offsetX: 1, offsetY: 34, image: atlas.canvas_}); - expect(atlas.add(3, 64, 32, defaultRender)).to.eql( + expect(atlas.add('3', 64, 32, defaultRender)).to.eql( {offsetX: 1, offsetY: 67, image: atlas.canvas_}); // this one can not be added anymore - expect(atlas.add(4, 64, 32, defaultRender)).to.eql(null); + expect(atlas.add('4', 64, 32, defaultRender)).to.eql(null); // but there is still room for smaller ones - expect(atlas.add(5, 40, 32, defaultRender)).to.eql( + expect(atlas.add('5', 40, 32, defaultRender)).to.eql( {offsetX: 66, offsetY: 1, image: atlas.canvas_}); - expect(atlas.add(6, 40, 32, defaultRender)).to.eql( + expect(atlas.add('6', 40, 32, defaultRender)).to.eql( {offsetX: 66, offsetY: 34, image: atlas.canvas_}); }); @@ -118,22 +118,22 @@ describe('ol.renderer.webgl.Atlas', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); for (var i = 1; i <= 32; i++) { - expect(atlas.add(i, 28, 14, defaultRender)).to.be.ok(); + expect(atlas.add(i.toString(), 28, 14, defaultRender)).to.be.ok(); } // there is no more space for items of this size, the next one will fail - expect(atlas.add(33, 28, 14, defaultRender)).to.eql(null); + expect(atlas.add('33', 28, 14, defaultRender)).to.eql(null); }); it('fills up the whole atlas (rectangles in landscape format)', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); for (var i = 1; i <= 32; i++) { - expect(atlas.add(i, 14, 28, defaultRender)).to.be.ok(); + expect(atlas.add(i.toString(), 14, 28, defaultRender)).to.be.ok(); } // there is no more space for items of this size, the next one will fail - expect(atlas.add(33, 14, 28, defaultRender)).to.eql(null); + expect(atlas.add('33', 14, 28, defaultRender)).to.eql(null); }); }); @@ -142,13 +142,13 @@ describe('ol.renderer.webgl.Atlas', function() { it('calls the render callback with the right values', function() { var atlas = new ol.renderer.webgl.Atlas(128, 1); var rendererCallback = sinon.spy(); - atlas.add(1, 32, 32, rendererCallback); + atlas.add('1', 32, 32, rendererCallback); expect(rendererCallback.calledOnce).to.be.ok(); expect(rendererCallback.calledWith(atlas.context_, 1, 1)).to.be.ok(); rendererCallback = sinon.spy(); - atlas.add(2, 32, 32, rendererCallback); + atlas.add('2', 32, 32, rendererCallback); expect(rendererCallback.calledOnce).to.be.ok(); expect(rendererCallback.calledWith(atlas.context_, 34, 1)).to.be.ok(); @@ -162,8 +162,8 @@ describe('ol.renderer.webgl.Atlas', function() { context.fillRect(x, y, 32, 32); }; - expect(atlas.add(1, 32, 32, rendererCallback)).to.be.ok(); - expect(atlas.add(2, 32, 32, rendererCallback)).to.be.ok(); + expect(atlas.add('1', 32, 32, rendererCallback)).to.be.ok(); + expect(atlas.add('2', 32, 32, rendererCallback)).to.be.ok(); // no error, ok }); }); @@ -187,18 +187,18 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('adds one entry', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - var info = manager.add(1, 32, 32, defaultRender); + var info = manager.add('1', 32, 32, defaultRender); expect(info).to.eql( {offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_}); - expect(manager.getInfo(1)).to.eql(info); + expect(manager.getInfo('1')).to.eql(info); }); it('creates a new atlas if needed', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); - var info = manager.add(2, 100, 100, defaultRender); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + var info = manager.add('2', 100, 100, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(256); expect(manager.atlases_).to.have.length(2); @@ -206,9 +206,9 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('creates new atlases until one is large enough', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(1); - var info = manager.add(2, 500, 500, defaultRender); + var info = manager.add('2', 500, 500, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(512); expect(manager.atlases_).to.have.length(3); @@ -216,8 +216,8 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('checks all existing atlases and create a new if needed', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); - expect(manager.add(2, 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('2', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(2); var info = manager.add(3, 500, 500, defaultRender); expect(info).to.be.ok(); @@ -227,16 +227,16 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('returns null if the size exceeds the maximum size', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - expect(manager.add(1, 100, 100, defaultRender)).to.be.ok(); - expect(manager.add(2, 3000, 3000, defaultRender)).to.eql(null); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('2', 3000, 3000, defaultRender)).to.eql(null); }); }); describe('#getInfo', function() { - it('returns null if no entry for the given hash', function() { + it('returns null if no entry for the given id', function() { var manager = new ol.renderer.webgl.AtlasManager(128); - expect(manager.getInfo(123456)).to.eql(null); + expect(manager.getInfo('123456')).to.eql(null); }); }); }); From 64da2647a6ae63ef1f2d4246301e6db64809564f Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 17:09:34 +0100 Subject: [PATCH 30/98] Take `space` into account when checking size --- src/ol/renderer/webgl/webglatlasmanager.js | 3 ++- test/spec/ol/renderer/webgl/webglatlasmanager.test.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/renderer/webgl/webglatlasmanager.js index edf1f5ffdb..f46bce4226 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/renderer/webgl/webglatlasmanager.js @@ -100,7 +100,8 @@ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(id) { */ ol.renderer.webgl.AtlasManager.prototype.add = function(id, width, height, renderCallback, opt_this) { - if (width > this.maxSize_ || height > this.maxSize_) { + if (width + this.space_ > this.maxSize_ || + height + this.space_ > this.maxSize_) { return null; } diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js index e9504b6686..465f448218 100644 --- a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js +++ b/test/spec/ol/renderer/webgl/webglatlasmanager.test.js @@ -228,7 +228,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { it('returns null if the size exceeds the maximum size', function() { var manager = new ol.renderer.webgl.AtlasManager(128); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); - expect(manager.add('2', 3000, 3000, defaultRender)).to.eql(null); + expect(manager.add('2', 2048, 2048, defaultRender)).to.eql(null); }); }); From 0c6a40f5b5415ff7b4e902aa2b8188768c48f24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 6 Nov 2014 17:16:58 +0100 Subject: [PATCH 31/98] Support image rotateWithView --- examples/icon-sprite-webgl.js | 22 ++++++---- src/ol/render/webgl/webglimage.glsl | 12 +++-- src/ol/render/webgl/webglimageshader.js | 34 ++++++++++----- src/ol/render/webgl/webglreplay.js | 58 +++++++++++++++++++++---- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index f8d9e219b3..33c48856ec 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -9,23 +9,26 @@ goog.require('ol.style.Style'); var iconInfo = [{ - size: [55, 55], offset: [0, 0], opacity: 1.0, + rotateWithView: true, + rotation: 0.0, scale: 1.0, - rotation: 0.0 + size: [55, 55] }, { - size: [55, 55], offset: [110, 86], opacity: 0.75, + rotateWithView: false, + rotation: Math.PI / 2.0, scale: 1.25, - rotation: Math.PI / 2.0 + size: [55, 55] }, { - size: [55, 86], offset: [55, 0], opacity: 0.5, + rotateWithView: true, + rotation: Math.PI / 3.0, scale: 1.5, - rotation: Math.PI / 3.0 + size: [55, 86] }]; var i; @@ -35,12 +38,13 @@ var icons = new Array(iconCount); for (i = 0; i < iconCount; ++i) { var info = iconInfo[i]; icons[i] = new ol.style.Icon({ - src: 'data/Butterfly.png', - size: info.size, offset: info.offset, opacity: info.opacity, + rotateWithView: info.rotateWithView, + rotation: info.rotation, scale: info.scale, - rotation: info.rotation + size: info.size, + src: 'data/Butterfly.png' }); } diff --git a/src/ol/render/webgl/webglimage.glsl b/src/ol/render/webgl/webglimage.glsl index 5098ac2a3f..d5f5db9191 100644 --- a/src/ol/render/webgl/webglimage.glsl +++ b/src/ol/render/webgl/webglimage.glsl @@ -11,13 +11,19 @@ attribute vec2 a_position; attribute vec2 a_texCoord; attribute vec2 a_offsets; attribute float a_opacity; +attribute float a_rotateWithView; uniform mat4 u_projectionMatrix; -uniform mat2 u_sizeMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; void main(void) { - vec2 offsets = u_sizeMatrix * a_offsets; - gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.); + mat4 offsetMatrix = u_offsetScaleMatrix; + if (a_rotateWithView == 1.0) { + offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; + } + vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.); + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets; v_texCoord = a_texCoord; v_opacity = a_opacity; } diff --git a/src/ol/render/webgl/webglimageshader.js b/src/ol/render/webgl/webglimageshader.js index f50e358d84..69d59a09ce 100644 --- a/src/ol/render/webgl/webglimageshader.js +++ b/src/ol/render/webgl/webglimageshader.js @@ -28,7 +28,7 @@ ol.render.webgl.imagereplay.shader.Fragment.DEBUG_SOURCE = 'precision mediump fl * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform sampler2D i;void main(void){vec4 texColor=texture2D(i,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*b;}'; +ol.render.webgl.imagereplay.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform sampler2D k;void main(void){vec4 texColor=texture2D(k,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*b;}'; /** @@ -57,14 +57,14 @@ goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.Vertex); * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\n\nuniform mat4 u_projectionMatrix;\nuniform mat2 u_sizeMatrix;\n\nvoid main(void) {\n vec2 offsets = u_sizeMatrix * a_offsets;\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + vec4(offsets, 0., 0.);\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; +ol.render.webgl.imagereplay.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix;\n if (a_rotateWithView == 1.0) {\n offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n }\n vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; /** * @const * @type {string} */ -ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;uniform mat4 g;uniform mat2 h;void main(void){vec2 offsets=h*e;gl_Position=g*vec4(c,0.,1.)+vec4(offsets,0.,0.);a=d;b=f;}'; +ol.render.webgl.imagereplay.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}'; /** @@ -89,19 +89,25 @@ ol.render.webgl.imagereplay.shader.Locations = function(gl, program) { * @type {WebGLUniformLocation} */ this.u_image = gl.getUniformLocation( - program, goog.DEBUG ? 'u_image' : 'i'); + program, goog.DEBUG ? 'u_image' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i'); /** * @type {WebGLUniformLocation} */ this.u_projectionMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_projectionMatrix' : 'g'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_sizeMatrix = gl.getUniformLocation( - program, goog.DEBUG ? 'u_sizeMatrix' : 'h'); + program, goog.DEBUG ? 'u_projectionMatrix' : 'h'); /** * @type {number} @@ -121,6 +127,12 @@ ol.render.webgl.imagereplay.shader.Locations = function(gl, program) { this.a_position = gl.getAttribLocation( program, goog.DEBUG ? 'a_position' : 'c'); + /** + * @type {number} + */ + this.a_rotateWithView = gl.getAttribLocation( + program, goog.DEBUG ? 'a_rotateWithView' : 'g'); + /** * @type {number} */ diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index d01a4b777e..9a6ac203a0 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -110,6 +110,18 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.opacity_ = undefined; + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.offsetRotateMatrix_ = goog.vec.Mat4.createNumberIdentity(); + + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.offsetScaleMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** * @type {number|undefined} * @private @@ -128,6 +140,12 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** + * @private + * @type {boolean|undefined} + */ + this.rotateWithView_ = undefined; + /** * @private * @type {number|undefined} @@ -222,6 +240,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = goog.asserts.assert(goog.isDef(this.opacity_)); goog.asserts.assert(goog.isDef(this.originX_)); goog.asserts.assert(goog.isDef(this.originY_)); + goog.asserts.assert(goog.isDef(this.rotateWithView_)); goog.asserts.assert(goog.isDef(this.rotation_)); goog.asserts.assert(goog.isDef(this.scale_)); goog.asserts.assert(goog.isDef(this.width_)); @@ -233,6 +252,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = var opacity = this.opacity_; var originX = this.originX_; var originY = this.originY_; + var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0; var rotation = this.rotation_; var scale = this.scale_; var width = this.width_; @@ -245,9 +265,9 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = x = flatCoordinates[i] - this.origin_[0]; y = flatCoordinates[i + 1] - this.origin_[1]; - n = numVertices / 7; + n = numVertices / 8; - // 4 vertices per coordinate, with 7 values per vertex + // 4 vertices per coordinate, with 8 values per vertex offsetX = -scale * anchorX; offsetY = -scale * (height - anchorY); @@ -258,6 +278,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; offsetX = scale * (width - anchorX); offsetY = -scale * (height - anchorY); @@ -268,6 +289,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = (originY + height) / imageHeight; this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; offsetX = scale * (width - anchorX); offsetY = scale * anchorY; @@ -278,6 +300,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = originX / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; offsetX = -scale * anchorX; offsetY = scale * anchorY; @@ -288,6 +311,7 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = this.vertices_[numVertices++] = (originX + width) / imageWidth; this.vertices_[numVertices++] = originY / imageHeight; this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; this.indices_[numIndices++] = n; this.indices_[numIndices++] = n + 1; @@ -431,6 +455,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { this.opacity_ = undefined; this.originX_ = undefined; this.originY_ = undefined; + this.rotateWithView_ = undefined; this.rotation_ = undefined; this.scale_ = undefined; this.vertices_ = null; @@ -481,29 +506,43 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, -rotation, -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1])); + var offsetScaleMatrix = this.offsetScaleMatrix_; + goog.vec.Mat4.makeScale(offsetScaleMatrix, 2 / size[0], 2 / size[1], 1); + + var offsetRotateMatrix = this.offsetRotateMatrix_; + goog.vec.Mat4.makeIdentity(offsetRotateMatrix); + if (rotation !== 0) { + goog.vec.Mat4.rotateZ(offsetRotateMatrix, -rotation); + } + var locations = this.locations_; gl.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); gl.enableVertexAttribArray(locations.a_position); gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT, - false, 28, 0); + false, 32, 0); gl.enableVertexAttribArray(locations.a_offsets); gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT, - false, 28, 8); + false, 32, 8); gl.enableVertexAttribArray(locations.a_texCoord); gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT, - false, 28, 16); + false, 32, 16); gl.enableVertexAttribArray(locations.a_opacity); gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT, - false, 28, 24); + false, 32, 24); + + gl.enableVertexAttribArray(locations.a_rotateWithView); + gl.vertexAttribPointer(locations.a_rotateWithView, 1, goog.webgl.FLOAT, + false, 32, 28); gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix); - gl.uniformMatrix2fv(locations.u_sizeMatrix, false, - new Float32Array([2 / size[0], 0.0, 0.0, 2 / size[1]])); + gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, offsetScaleMatrix); + gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, + offsetRotateMatrix); gl.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); @@ -540,6 +579,8 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { goog.asserts.assert(goog.isDef(opacity)); var origin = imageStyle.getOrigin(); goog.asserts.assert(!goog.isNull(origin)); + var rotateWithView = imageStyle.getRotateWithView(); + goog.asserts.assert(goog.isDef(rotateWithView)); var rotation = imageStyle.getRotation(); goog.asserts.assert(goog.isDef(rotation)); var size = imageStyle.getSize(); @@ -567,6 +608,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { this.originX_ = origin[0]; this.originY_ = origin[1]; this.rotation_ = rotation; + this.rotateWithView_ = rotateWithView; this.scale_ = scale; this.width_ = size[0]; }; From ebe0c6a88c3b63e0acd9015e72a3774e404afbc0 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 6 Nov 2014 17:51:15 +0100 Subject: [PATCH 32/98] Move AtlasManager to `ol.style.AtlasManager` --- .../atlasmanager.js} | 50 +++++++++---------- .../atlasmanager.test.js} | 48 +++++++++--------- 2 files changed, 49 insertions(+), 49 deletions(-) rename src/ol/{renderer/webgl/webglatlasmanager.js => style/atlasmanager.js} (85%) rename test/spec/ol/{renderer/webgl/webglatlasmanager.test.js => style/atlasmanager.test.js} (83%) diff --git a/src/ol/renderer/webgl/webglatlasmanager.js b/src/ol/style/atlasmanager.js similarity index 85% rename from src/ol/renderer/webgl/webglatlasmanager.js rename to src/ol/style/atlasmanager.js index f46bce4226..31a52f5a4b 100644 --- a/src/ol/renderer/webgl/webglatlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -1,5 +1,5 @@ -goog.provide('ol.renderer.webgl.Atlas'); -goog.provide('ol.renderer.webgl.AtlasManager'); +goog.provide('ol.style.Atlas'); +goog.provide('ol.style.AtlasManager'); goog.require('goog.asserts'); goog.require('goog.dom'); @@ -10,7 +10,7 @@ goog.require('goog.object'); /** * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}} */ -ol.renderer.webgl.AtlasInfo; +ol.style.AtlasInfo; @@ -35,7 +35,7 @@ ol.renderer.webgl.AtlasInfo; * @param {number=} opt_space The space in pixels between images * (default: 1). */ -ol.renderer.webgl.AtlasManager = function(opt_size, opt_maxSize, opt_space) { +ol.style.AtlasManager = function(opt_size, opt_maxSize, opt_space) { /** * The size in pixels of the latest atlas image. @@ -60,17 +60,17 @@ ol.renderer.webgl.AtlasManager = function(opt_size, opt_maxSize, opt_space) { /** * @private - * @type {Array.} + * @type {Array.} */ - this.atlases_ = [new ol.renderer.webgl.Atlas(this.currentSize_, this.space_)]; + this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)]; }; /** * @param {string} id The identifier of the entry to check. - * @return {ol.renderer.webgl.AtlasInfo} + * @return {ol.style.AtlasInfo} */ -ol.renderer.webgl.AtlasManager.prototype.getInfo = function(id) { +ol.style.AtlasManager.prototype.getInfo = function(id) { var atlas, info; for (var i = 0, ii = this.atlases_.length; i < ii; i++) { atlas = this.atlases_[i]; @@ -96,9 +96,9 @@ ol.renderer.webgl.AtlasManager.prototype.getInfo = function(id) { * onto the sprite image. * @param {object=} opt_this Value to use as `this` when executing * `renderCallback`. - * @return {ol.renderer.webgl.AtlasInfo} + * @return {ol.style.AtlasInfo} */ -ol.renderer.webgl.AtlasManager.prototype.add = +ol.style.AtlasManager.prototype.add = function(id, width, height, renderCallback, opt_this) { if (width + this.space_ > this.maxSize_ || height + this.space_ > this.maxSize_) { @@ -115,7 +115,7 @@ ol.renderer.webgl.AtlasManager.prototype.add = // the entry could not be added to one of the existing atlases, // create a new atlas that is twice as big and try to add to this one. this.currentSize_ = Math.min(this.currentSize_ * 2, this.maxSize_); - atlas = new ol.renderer.webgl.Atlas(this.currentSize_, this.space_); + atlas = new ol.style.Atlas(this.currentSize_, this.space_); this.atlases_.push(atlas); ii++; } @@ -137,7 +137,7 @@ ol.renderer.webgl.AtlasManager.prototype.add = * @param {number} size The size in pixels of the sprite images. * @param {number} space The space in pixels between images. */ -ol.renderer.webgl.Atlas = function(size, space) { +ol.style.Atlas = function(size, space) { /** * @private @@ -151,13 +151,13 @@ ol.renderer.webgl.Atlas = function(size, space) { /** * @private - * @type {Array.} + * @type {Array.} */ this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}]; /** * @private - * @type {Object.} + * @type {Object.} */ this.entries_ = {}; @@ -181,9 +181,9 @@ ol.renderer.webgl.Atlas = function(size, space) { /** * @param {string} id The identifier of the entry to check. - * @return {ol.renderer.webgl.AtlasInfo} + * @return {ol.style.AtlasInfo} */ -ol.renderer.webgl.Atlas.prototype.get = function(id) { +ol.style.Atlas.prototype.get = function(id) { return goog.object.get(this.entries_, id, null); }; @@ -196,9 +196,9 @@ ol.renderer.webgl.Atlas.prototype.get = function(id) { * onto the sprite image. * @param {object=} opt_this Value to use as `this` when executing * `renderCallback`. - * @return {ol.renderer.webgl.AtlasInfo} + * @return {ol.style.AtlasInfo} */ -ol.renderer.webgl.Atlas.prototype.add = +ol.style.Atlas.prototype.add = function(id, width, height, renderCallback, opt_this) { var block; for (var i = 0, ii = this.emptyBlocks_.length; i < ii; i++) { @@ -232,16 +232,16 @@ ol.renderer.webgl.Atlas.prototype.add = /** * @private * @param {number} index The index of the block. - * @param {ol.renderer.webgl.Atlas.Block} block The block to split. + * @param {ol.style.Atlas.Block} block The block to split. * @param {number} width The width of the entry to insert. * @param {number} height The height of the entry to insert. */ -ol.renderer.webgl.Atlas.prototype.split_ = +ol.style.Atlas.prototype.split_ = function(index, block, width, height) { var deltaWidth = block.width - width; var deltaHeight = block.height - height; - /** @type {ol.renderer.webgl.AtlasInfo} */ + /** @type {ol.style.AtlasInfo} */ var newBlock1, newBlock2; if (deltaWidth > deltaHeight) { @@ -290,10 +290,10 @@ ol.renderer.webgl.Atlas.prototype.split_ = * blocks (that are potentially smaller) are filled first. * @private * @param {number} index The index of the block to remove. - * @param {ol.renderer.webgl.Atlas.Block} newBlock1 The 1st block to add. - * @param {ol.renderer.webgl.Atlas.Block} newBlock2 The 2nd block to add. + * @param {ol.style.Atlas.Block} newBlock1 The 1st block to add. + * @param {ol.style.Atlas.Block} newBlock2 The 2nd block to add. */ -ol.renderer.webgl.Atlas.prototype.updateBlocks_ = +ol.style.Atlas.prototype.updateBlocks_ = function(index, newBlock1, newBlock2) { var args = [index, 1]; if (newBlock1.width > 0 && newBlock1.height > 0) { @@ -309,4 +309,4 @@ ol.renderer.webgl.Atlas.prototype.updateBlocks_ = /** * @typedef {{x: number, y: number, width: number, height: number}} */ -ol.renderer.webgl.Atlas.Block; +ol.style.Atlas.Block; diff --git a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js similarity index 83% rename from test/spec/ol/renderer/webgl/webglatlasmanager.test.js rename to test/spec/ol/style/atlasmanager.test.js index 465f448218..1cb1e2c618 100644 --- a/test/spec/ol/renderer/webgl/webglatlasmanager.test.js +++ b/test/spec/ol/style/atlasmanager.test.js @@ -1,7 +1,7 @@ -goog.provide('ol.test.renderer.webgl.AtlasManager'); +goog.provide('ol.test.style.AtlasManager'); -describe('ol.renderer.webgl.Atlas', function() { +describe('ol.style.Atlas', function() { var defaultRender = function(context, x, y) { }; @@ -9,7 +9,7 @@ describe('ol.renderer.webgl.Atlas', function() { describe('#constructor', function() { it('inits the atlas', function() { - var atlas = new ol.renderer.webgl.Atlas(256, 1); + var atlas = new ol.style.Atlas(256, 1); expect(atlas.emptyBlocks_).to.eql( [{x: 0, y: 0, width: 256, height: 256}]); }); @@ -18,7 +18,7 @@ describe('ol.renderer.webgl.Atlas', function() { describe('#add (squares with same size)', function() { it('adds one entry', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); var info = atlas.add('1', 32, 32, defaultRender); expect(info).to.eql( @@ -28,7 +28,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('adds two entries', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); atlas.add('1', 32, 32, defaultRender); var info = atlas.add('2', 32, 32, defaultRender); @@ -40,7 +40,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('adds three entries', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); atlas.add('1', 32, 32, defaultRender); atlas.add('2', 32, 32, defaultRender); @@ -53,7 +53,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('adds four entries (new row)', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); atlas.add('1', 32, 32, defaultRender); atlas.add('2', 32, 32, defaultRender); @@ -67,7 +67,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('returns null when an entry is too big', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); atlas.add('1', 32, 32, defaultRender); atlas.add('2', 32, 32, defaultRender); @@ -78,7 +78,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('fills up the whole atlas', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); for (var i = 1; i <= 16; i++) { expect(atlas.add(i.toString(), 28, 28, defaultRender)).to.be.ok(); @@ -92,7 +92,7 @@ describe('ol.renderer.webgl.Atlas', function() { describe('#add (rectangles with different sizes)', function() { it('adds a bunch of rectangles', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); expect(atlas.add('1', 64, 32, defaultRender)).to.eql( {offsetX: 1, offsetY: 1, image: atlas.canvas_}); @@ -115,7 +115,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('fills up the whole atlas (rectangles in portrait format)', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); for (var i = 1; i <= 32; i++) { expect(atlas.add(i.toString(), 28, 14, defaultRender)).to.be.ok(); @@ -126,7 +126,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('fills up the whole atlas (rectangles in landscape format)', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); for (var i = 1; i <= 32; i++) { expect(atlas.add(i.toString(), 14, 28, defaultRender)).to.be.ok(); @@ -140,7 +140,7 @@ describe('ol.renderer.webgl.Atlas', function() { describe('#add (rendering)', function() { it('calls the render callback with the right values', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); var rendererCallback = sinon.spy(); atlas.add('1', 32, 32, rendererCallback); @@ -155,7 +155,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); it('is possible to actually draw on the canvas', function() { - var atlas = new ol.renderer.webgl.Atlas(128, 1); + var atlas = new ol.style.Atlas(128, 1); var rendererCallback = function(context, x, y) { context.fillStyle = '#FFA500'; @@ -170,7 +170,7 @@ describe('ol.renderer.webgl.Atlas', function() { }); -describe('ol.renderer.webgl.AtlasManager', function() { +describe('ol.style.AtlasManager', function() { var defaultRender = function(context, x, y) { }; @@ -178,7 +178,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { describe('#constructor', function() { it('inits the atlas manager', function() { - var manager = new ol.renderer.webgl.AtlasManager(); + var manager = new ol.style.AtlasManager(); expect(manager.atlases_).to.not.be.empty(); }); }); @@ -186,7 +186,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { describe('#add', function() { it('adds one entry', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); var info = manager.add('1', 32, 32, defaultRender); expect(info).to.eql( @@ -196,7 +196,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { }); it('creates a new atlas if needed', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); var info = manager.add('2', 100, 100, defaultRender); expect(info).to.be.ok(); @@ -205,7 +205,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { }); it('creates new atlases until one is large enough', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(1); var info = manager.add('2', 500, 500, defaultRender); @@ -215,7 +215,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { }); it('checks all existing atlases and create a new if needed', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.add('2', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(2); @@ -226,7 +226,7 @@ describe('ol.renderer.webgl.AtlasManager', function() { }); it('returns null if the size exceeds the maximum size', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.add('2', 2048, 2048, defaultRender)).to.eql(null); }); @@ -235,11 +235,11 @@ describe('ol.renderer.webgl.AtlasManager', function() { describe('#getInfo', function() { it('returns null if no entry for the given id', function() { - var manager = new ol.renderer.webgl.AtlasManager(128); + var manager = new ol.style.AtlasManager(128); expect(manager.getInfo('123456')).to.eql(null); }); }); }); -goog.require('ol.renderer.webgl.Atlas'); -goog.require('ol.renderer.webgl.AtlasManager'); +goog.require('ol.style.Atlas'); +goog.require('ol.style.AtlasManager'); From c0acc8d82568bb293fa351b0ef5df67dd9334c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 7 Nov 2014 09:59:56 +0100 Subject: [PATCH 33/98] Add comment describing the vertices sent to GPU --- src/ol/render/webgl/webglreplay.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 9a6ac203a0..d698d7645f 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -265,9 +265,17 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = x = flatCoordinates[i] - this.origin_[0]; y = flatCoordinates[i + 1] - this.origin_[1]; - n = numVertices / 8; + // There are 4 vertices per [x, y] point, one for each corner of the + // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if + // WebGL supported Geometry Shaders (which can emit new vertices), but that + // is not currently the case. + // + // And each vertex includes 8 values: the x and y coordinates, the x and + // y offsets used to calculate the position of the corner, the u and + // v texture coordinates for the corner, the opacity, and whether the + // the image should be rotated with the view (rotateWithView). - // 4 vertices per coordinate, with 8 values per vertex + n = numVertices / 8; offsetX = -scale * anchorX; offsetY = -scale * (height - anchorY); From 0a364b32edb1e260bc906fe273f9b6fb3f34028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 7 Nov 2014 10:49:58 +0100 Subject: [PATCH 34/98] Use LINEAR for icon textures --- src/ol/render/webgl/webglreplay.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index d698d7645f..a00f40c27e 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -443,9 +443,9 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.NEAREST); + goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.LINEAR); gl.texParameteri(goog.webgl.TEXTURE_2D, - goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.NEAREST); + goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.LINEAR); gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, goog.webgl.UNSIGNED_BYTE, image); this.textures_[i] = texture; From e0c5e742adfc1db18401d04c9f05bbf2be4f8cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 7 Nov 2014 10:52:05 +0100 Subject: [PATCH 35/98] Use 30000 points in icon-sprite-webgl example --- examples/icon-sprite-webgl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 33c48856ec..8b80ef826e 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -48,7 +48,7 @@ for (i = 0; i < iconCount; ++i) { }); } -var featureCount = 10000; +var featureCount = 30000; var features = new Array(featureCount); var feature, geometry; var e = 25000000; From 3268b2033332440dd9bcdeca8ee10568f1a26e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 7 Nov 2014 10:57:59 +0100 Subject: [PATCH 36/98] Make icon-sprite-webgl example usable with Canvas --- examples/icon-sprite-webgl.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 8b80ef826e..fbddd00490 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -71,8 +71,14 @@ var vector = new ol.layer.Vector({ source: vectorSource }); +// Use the "webgl" renderer by default. +var renderer = exampleNS.getRendererFromQueryString(); +if (!renderer) { + renderer = 'webgl'; +} + var map = new ol.Map({ - renderer: 'webgl', + renderer: renderer, layers: [vector], target: document.getElementById('map'), view: new ol.View({ From fe3cdd8d1bfc7ae8e8624850017cf8162073b4c6 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Fri, 7 Nov 2014 11:12:32 +0100 Subject: [PATCH 37/98] Add AtlasManagerOptions and improve docs --- externs/olx.js | 33 +++++++++ src/ol/style/atlasmanager.js | 91 ++++++++++++++----------- test/spec/ol/style/atlasmanager.test.js | 12 ++-- 3 files changed, 89 insertions(+), 47 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 57e5d11ac5..970f01b87a 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -6101,3 +6101,36 @@ olx.ViewState.prototype.resolution; * @api */ olx.ViewState.prototype.rotation; + + +/** + * @typedef {{size: (number|undefined), + * maxSize: (number|undefined), + * space: (number|undefined)}} + * @api + */ +olx.style.AtlasManagerOptions; + + +/** + * The size in pixels of the first atlas image (default: 256). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.size; + + +/** + * The maximum size in pixels of atlas images (default: 2048). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.maxSize; + + +/** + * The space in pixels between images (default: 1). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.space; diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index 31a52f5a4b..8c03c38d06 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -8,6 +8,9 @@ goog.require('goog.object'); /** + * Provides information for an image inside an atlas. + * `offsetX` and `offsetY` are the position of the image inside + * the atlas image `image`. * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}} */ ol.style.AtlasInfo; @@ -15,48 +18,48 @@ ol.style.AtlasInfo; /** - * Manages the creation of texture atlases. + * Manages the creation of image atlases. * * Images added to this manager will be inserted into an atlas, which * will be used for rendering. * The `size` given in the constructor is the size for the first * atlas. After that, when new atlases are created, they will have - * twice the size as the latest atlas (until `maxSize` is reached.) + * twice the size as the latest atlas (until `maxSize` is reached). * - * It is recommended to use `ol.has.WEBGL_MAX_TEXTURE_SIZE` as - * `maxSize` value. + * When used for WebGL, it is recommended to use `ol.has.WEBGL_MAX_TEXTURE_SIZE` + * as `maxSize` value. Also, if an application uses a lot, or a lot of + * large images, it is recommend to set a higher `size` value to avoid + * the creation of too many atlases. * * @constructor * @struct - * @param {number=} opt_size The size in pixels of the first atlas image - * (default: 256). - * @param {number=} opt_maxSize The maximum size in pixels of atlas images - * (default: 2048). - * @param {number=} opt_space The space in pixels between images - * (default: 1). + * @api + * @param {olx.style.AtlasManagerOptions=} opt_options Options. */ -ol.style.AtlasManager = function(opt_size, opt_maxSize, opt_space) { +ol.style.AtlasManager = function(opt_options) { + + var options = goog.isDef(opt_options) ? opt_options : {}; /** * The size in pixels of the latest atlas image. * @private * @type {number} */ - this.currentSize_ = goog.isDef(opt_size) ? opt_size : 256; + this.currentSize_ = goog.isDef(options.size) ? options.size : 256; /** * The maximum size in pixels of atlas images. * @private * @type {number} */ - this.maxSize_ = goog.isDef(opt_maxSize) ? opt_maxSize : 2048; + this.maxSize_ = goog.isDef(options.maxSize) ? options.maxSize : 2048; /** * The size in pixels between images. * @private * @type {number} */ - this.space_ = goog.isDef(opt_space) ? opt_space : 1; + this.space_ = goog.isDef(options.space) ? options.space : 1; /** * @private @@ -68,11 +71,12 @@ ol.style.AtlasManager = function(opt_size, opt_maxSize, opt_space) { /** * @param {string} id The identifier of the entry to check. - * @return {ol.style.AtlasInfo} + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry, + * or `null` if the entry is not part of the atlas manager. */ ol.style.AtlasManager.prototype.getInfo = function(id) { - var atlas, info; - for (var i = 0, ii = this.atlases_.length; i < ii; i++) { + var atlas, info, i, ii; + for (i = 0, ii = this.atlases_.length; i < ii; ++i) { atlas = this.atlases_[i]; info = atlas.get(id); if (info !== null) { @@ -92,11 +96,12 @@ ol.style.AtlasManager.prototype.getInfo = function(id) { * @param {string} id The identifier of the entry to add. * @param {number} width The width. * @param {number} height The height. - * @param {function(*)} renderCallback Called to render the new sprite entry - * onto the sprite image. - * @param {object=} opt_this Value to use as `this` when executing - * `renderCallback`. - * @return {ol.style.AtlasInfo} + * @param {function(CanvasRenderingContext2D, number, number)} renderCallback + * Called to render the new image onto an atlas image. + * @param {Object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry, + * or `null` if the image is too big. */ ol.style.AtlasManager.prototype.add = function(id, width, height, renderCallback, opt_this) { @@ -105,8 +110,8 @@ ol.style.AtlasManager.prototype.add = return null; } - var atlas, info; - for (var i = 0, ii = this.atlases_.length; i < ii; i++) { + var atlas, info, i, ii; + for (i = 0, ii = this.atlases_.length; i < ii; ++i) { atlas = this.atlases_[i]; info = atlas.add(id, width, height, renderCallback, opt_this); if (info !== null) { @@ -117,7 +122,8 @@ ol.style.AtlasManager.prototype.add = this.currentSize_ = Math.min(this.currentSize_ * 2, this.maxSize_); atlas = new ol.style.Atlas(this.currentSize_, this.space_); this.atlases_.push(atlas); - ii++; + // run the loop another time + ++ii; } } }; @@ -125,16 +131,16 @@ ol.style.AtlasManager.prototype.add = /** - * This class facilitates the creation of texture atlases. + * This class facilitates the creation of image atlases. * * Images added to an atlas will be rendered onto a single - * atlas canvas. The distribution of images on the canvas are + * atlas canvas. The distribution of images on the canvas is * managed with the bin packing algorithm described in: * http://www.blackpawn.com/texts/lightmaps/ * * @constructor * @struct - * @param {number} size The size in pixels of the sprite images. + * @param {number} size The size in pixels of the sprite image. * @param {number} space The space in pixels between images. */ ol.style.Atlas = function(size, space) { @@ -143,7 +149,7 @@ ol.style.Atlas = function(size, space) { * @private * @type {number} The space in pixels between images. * Because texture coordinates are float values, the edges of - * texture might not be completely correct (in a way that the + * images might not be completely correct (in a way that the * edges overlap when being rendered). To avoid this we add a * padding around each image. */ @@ -157,7 +163,7 @@ ol.style.Atlas = function(size, space) { /** * @private - * @type {Object.} + * @type {Object.} */ this.entries_ = {}; @@ -181,10 +187,11 @@ ol.style.Atlas = function(size, space) { /** * @param {string} id The identifier of the entry to check. - * @return {ol.style.AtlasInfo} + * @return {?ol.style.AtlasInfo} */ ol.style.Atlas.prototype.get = function(id) { - return goog.object.get(this.entries_, id, null); + return /** @type {?ol.style.AtlasInfo} */ ( + goog.object.get(this.entries_, id, null)); }; @@ -192,16 +199,16 @@ ol.style.Atlas.prototype.get = function(id) { * @param {string} id The identifier of the entry to add. * @param {number} width The width. * @param {number} height The height. - * @param {function(*)} renderCallback Called to render the new sprite entry - * onto the sprite image. - * @param {object=} opt_this Value to use as `this` when executing - * `renderCallback`. - * @return {ol.style.AtlasInfo} + * @param {function(CanvasRenderingContext2D, number, number)} renderCallback + * Called to render the new image onto an atlas image. + * @param {Object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry. */ ol.style.Atlas.prototype.add = function(id, width, height, renderCallback, opt_this) { - var block; - for (var i = 0, ii = this.emptyBlocks_.length; i < ii; i++) { + var block, i, ii; + for (i = 0, ii = this.emptyBlocks_.length; i < ii; ++i) { block = this.emptyBlocks_[i]; if (block.width >= width + this.space_ && block.height >= height + this.space_) { @@ -241,8 +248,10 @@ ol.style.Atlas.prototype.split_ = var deltaWidth = block.width - width; var deltaHeight = block.height - height; - /** @type {ol.style.AtlasInfo} */ - var newBlock1, newBlock2; + /** @type {ol.style.Atlas.Block} */ + var newBlock1; + /** @type {ol.style.Atlas.Block} */ + var newBlock2; if (deltaWidth > deltaHeight) { // split vertically diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js index 1cb1e2c618..b24d832dc0 100644 --- a/test/spec/ol/style/atlasmanager.test.js +++ b/test/spec/ol/style/atlasmanager.test.js @@ -186,7 +186,7 @@ describe('ol.style.AtlasManager', function() { describe('#add', function() { it('adds one entry', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); var info = manager.add('1', 32, 32, defaultRender); expect(info).to.eql( @@ -196,7 +196,7 @@ describe('ol.style.AtlasManager', function() { }); it('creates a new atlas if needed', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); var info = manager.add('2', 100, 100, defaultRender); expect(info).to.be.ok(); @@ -205,7 +205,7 @@ describe('ol.style.AtlasManager', function() { }); it('creates new atlases until one is large enough', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(1); var info = manager.add('2', 500, 500, defaultRender); @@ -215,7 +215,7 @@ describe('ol.style.AtlasManager', function() { }); it('checks all existing atlases and create a new if needed', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.add('2', 100, 100, defaultRender)).to.be.ok(); expect(manager.atlases_).to.have.length(2); @@ -226,7 +226,7 @@ describe('ol.style.AtlasManager', function() { }); it('returns null if the size exceeds the maximum size', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); expect(manager.add('2', 2048, 2048, defaultRender)).to.eql(null); }); @@ -235,7 +235,7 @@ describe('ol.style.AtlasManager', function() { describe('#getInfo', function() { it('returns null if no entry for the given id', function() { - var manager = new ol.style.AtlasManager(128); + var manager = new ol.style.AtlasManager({size: 128}); expect(manager.getInfo('123456')).to.eql(null); }); }); From 6abe3047ec85f5df2991f87ee0ede46f5631f606 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Fri, 7 Nov 2014 11:12:31 +0100 Subject: [PATCH 38/98] Add feature overlay support to webgl map renderer. --- examples/data/Butterfly.png | Bin 64098 -> 62674 bytes examples/icon-sprite-webgl.js | 25 ++++++++++++++++++- src/ol/renderer/webgl/webglmaprenderer.js | 28 ++++++++++++++++++---- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/examples/data/Butterfly.png b/examples/data/Butterfly.png index ef37aaad4d3f1637bbbebcfa583e750be6a68e63..0a32d018ce20d1b1e81e0af890d09ebb1410b029 100644 GIT binary patch literal 62674 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 literal 64098 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�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 Date: Fri, 7 Nov 2014 11:50:05 +0100 Subject: [PATCH 39/98] Dynamically selected renderer in draw feature example --- examples/draw-features.html | 2 +- examples/draw-features.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/draw-features.html b/examples/draw-features.html index 229155f863..edd2c1b8d0 100644 --- a/examples/draw-features.html +++ b/examples/draw-features.html @@ -32,7 +32,7 @@ From 88a3fda5147023a21d2bbf1fc96e3bae6812450b Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 4 Dec 2014 09:23:00 +0100 Subject: [PATCH 92/98] Remove star example in favor of #2976 --- examples/symbol-stars.html | 52 ---------------------- examples/symbol-stars.js | 89 -------------------------------------- 2 files changed, 141 deletions(-) delete mode 100644 examples/symbol-stars.html delete mode 100644 examples/symbol-stars.js diff --git a/examples/symbol-stars.html b/examples/symbol-stars.html deleted file mode 100644 index 68f0b1fab0..0000000000 --- a/examples/symbol-stars.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - Regular shape symbols - - - - - -
- -
-
-
-
-
- -
- -
-

Regular shape symbols

-

Drawing stars with regular shape symbols.

-
-

See the symbol-stars.js source to see how this is done.

-
-
symbol, regular, star, vector, point
-
- -
- -
- - - - - - - - diff --git a/examples/symbol-stars.js b/examples/symbol-stars.js deleted file mode 100644 index 4464e45176..0000000000 --- a/examples/symbol-stars.js +++ /dev/null @@ -1,89 +0,0 @@ -goog.require('ol.Feature'); -goog.require('ol.Map'); -goog.require('ol.View'); -goog.require('ol.geom.Point'); -goog.require('ol.layer.Vector'); -goog.require('ol.source.Vector'); -goog.require('ol.style.Fill'); -goog.require('ol.style.RegularShape'); -goog.require('ol.style.Stroke'); -goog.require('ol.style.Style'); - -var symbolInfo = [{ - opacity: 1.0, - scale: 1.0, - fillColor: 'rgba(255, 153, 0, 0.4)', - strokeColor: 'rgba(255, 204, 0, 0.2)' -}, { - opacity: 0.75, - scale: 1.25, - fillColor: 'rgba(70, 80, 224, 0.4)', - strokeColor: 'rgba(12, 21, 138, 0.2)' -}, { - opacity: 0.5, - scale: 1.5, - fillColor: 'rgba(66, 150, 79, 0.4)', - strokeColor: 'rgba(20, 99, 32, 0.2)' -}, { - opacity: 1.0, - scale: 1.0, - fillColor: 'rgba(176, 61, 35, 0.4)', - strokeColor: 'rgba(145, 43, 20, 0.2)' -}]; - -var radiuses = [3, 6, 9, 15, 19, 25]; -var symbolCount = symbolInfo.length * radiuses.length; -var symbols = []; -var i, j; -for (i = 0; i < symbolInfo.length; ++i) { - var info = symbolInfo[i]; - for (j = 0; j < radiuses.length; ++j) { - symbols.push(new ol.style.RegularShape({ - points: 8, - opacity: info.opacity, - scale: info.scale, - radius: radiuses[j], - radius2: radiuses[j] * 0.7, - angle: 1.4, - fill: new ol.style.Fill({ - color: info.fillColor - }), - stroke: new ol.style.Stroke({ - color: info.strokeColor, - width: 1 - }) - })); - } -} - -var featureCount = 5000; -var features = new Array(featureCount); -var feature, geometry; -var e = 25000000; -for (i = 0; i < featureCount; ++i) { - geometry = new ol.geom.Point( - [2 * e * Math.random() - e, 2 * e * Math.random() - e]); - feature = new ol.Feature(geometry); - feature.setStyle( - new ol.style.Style({ - image: symbols[i % symbolCount] - }) - ); - features[i] = feature; -} - -var vectorSource = new ol.source.Vector({ - features: features -}); -var vector = new ol.layer.Vector({ - source: vectorSource -}); - -var map = new ol.Map({ - layers: [vector], - target: document.getElementById('map'), - view: new ol.View({ - center: [0, 0], - zoom: 3 - }) -}); From b5d80679d882962f2129f9c8a7685d09dadf835c Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 4 Dec 2014 10:21:32 +0100 Subject: [PATCH 93/98] Add and fix documentation for atlas manager --- examples/symbol-atlas-webgl.html | 6 ++++++ src/ol/style/atlasmanager.js | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/symbol-atlas-webgl.html b/examples/symbol-atlas-webgl.html index 36774b6681..666bc43ab5 100644 --- a/examples/symbol-atlas-webgl.html +++ b/examples/symbol-atlas-webgl.html @@ -34,6 +34,12 @@

Symbols with WebGL example

Using symbols in an atlas with WebGL.

+

When using symbol styles with WebGL, OpenLayers would render the symbol + on a temporary image and would create a WebGL texture for each image. For a + better performance, it is recommended to use atlas images (similar to + image sprites with CSS), so that the number of textures is reduced. OpenLayers + provides an AtlasManager, which when passed to the constructor + of a symbol style, will create atlases for the symbols.

See the symbol-atlas-webgl.js source to see how this is done.

webgl, symbol, atlas, vector, point
diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index f53ccb3c24..990a98d622 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -30,8 +30,8 @@ ol.style.AtlasManagerInfo; * atlas. After that, when new atlases are created, they will have * twice the size as the latest atlas (until `maxSize` is reached). * - * If an application uses a lot, or a lot of large images, it is recommended to - * set a higher `size` value to avoid the creation of too many atlases. + * If an application uses many images or very large images, it is recommended + * to set a higher `size` value to avoid the creation of too many atlases. * * @constructor * @struct From aef11b7471578ecb920aed06973dd83577e266b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 4 Dec 2014 11:18:40 +0100 Subject: [PATCH 94/98] Better comment in ivectorcontext.js --- src/ol/render/ivectorcontext.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ol/render/ivectorcontext.js b/src/ol/render/ivectorcontext.js index be1a3f9a5d..e331bec0db 100644 --- a/src/ol/render/ivectorcontext.js +++ b/src/ol/render/ivectorcontext.js @@ -5,8 +5,8 @@ goog.provide('ol.render.IVectorContext'); /** - * VectorContext interface. Currently implemented by - * {@link ol.render.canvas.Immediate} and {@link ol.render.webgl.Immediate} + * VectorContext interface. Implemented by + * {@link ol.render.canvas.Immediate} and {@link ol.render.webgl.Immediate}. * @interface */ ol.render.IVectorContext = function() { From 6d1d47a918c843ccba8d2dffc4e17870b7723d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 4 Dec 2014 11:22:15 +0100 Subject: [PATCH 95/98] Remove @api for ol.render.webgl.Immediate#flush --- src/ol/render/webgl/webglimmediate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ol/render/webgl/webglimmediate.js b/src/ol/render/webgl/webglimmediate.js index 37b8a37e19..5095504b7a 100644 --- a/src/ol/render/webgl/webglimmediate.js +++ b/src/ol/render/webgl/webglimmediate.js @@ -73,7 +73,6 @@ ol.render.webgl.Immediate = function(context, /** * FIXME: empty description for jsdoc - * @api */ ol.render.webgl.Immediate.prototype.flush = function() { /** @type {Array.} */ From 0fd1a575a9a511c0d19e7f220db96d294866b60a Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 4 Dec 2014 11:48:43 +0100 Subject: [PATCH 96/98] Use string instead of ol.structs.Checksum --- src/ol/structs/checksum.js | 6 ------ src/ol/style/circlestyle.js | 2 +- src/ol/style/fillstyle.js | 8 ++++---- src/ol/style/regularshapestyle.js | 2 +- src/ol/style/strokestyle.js | 18 +++++++++--------- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/ol/structs/checksum.js b/src/ol/structs/checksum.js index 423da745cf..ff72308ad2 100644 --- a/src/ol/structs/checksum.js +++ b/src/ol/structs/checksum.js @@ -1,12 +1,6 @@ goog.provide('ol.structs.IHasChecksum'); -/** - * @typedef {string} - */ -ol.structs.Checksum; - - /** * @interface diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 6f976cc39e..1c1742811d 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -29,7 +29,7 @@ ol.style.Circle = function(opt_options) { /** * @private - * @type {Array.|null} + * @type {Array.} */ this.checksums_ = null; diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js index a54d8d2498..4fd62d77e7 100644 --- a/src/ol/style/fillstyle.js +++ b/src/ol/style/fillstyle.js @@ -26,9 +26,9 @@ ol.style.Fill = function(opt_options) { /** * @private - * @type {?ol.structs.Checksum} + * @type {string|undefined} */ - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -49,7 +49,7 @@ ol.style.Fill.prototype.getColor = function() { */ ol.style.Fill.prototype.setColor = function(color) { this.color_ = color; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -57,7 +57,7 @@ ol.style.Fill.prototype.setColor = function(color) { * @inheritDoc */ ol.style.Fill.prototype.getChecksum = function() { - if (goog.isNull(this.checksum_)) { + if (!goog.isDef(this.checksum_)) { this.checksum_ = 'f' + (!goog.isNull(this.color_) ? ol.color.asString(this.color_) : '-'); } diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js index 823ef86629..24f35ef5f4 100644 --- a/src/ol/style/regularshapestyle.js +++ b/src/ol/style/regularshapestyle.js @@ -28,7 +28,7 @@ ol.style.RegularShape = function(opt_options) { /** * @private - * @type {Array.|null} + * @type {Array.} */ this.checksums_ = null; diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js index 419e771cd3..a679df2876 100644 --- a/src/ol/style/strokestyle.js +++ b/src/ol/style/strokestyle.js @@ -61,9 +61,9 @@ ol.style.Stroke = function(opt_options) { /** * @private - * @type {?ol.structs.Checksum} + * @type {string|undefined} */ - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -129,7 +129,7 @@ ol.style.Stroke.prototype.getWidth = function() { */ ol.style.Stroke.prototype.setColor = function(color) { this.color_ = color; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -141,7 +141,7 @@ ol.style.Stroke.prototype.setColor = function(color) { */ ol.style.Stroke.prototype.setLineCap = function(lineCap) { this.lineCap_ = lineCap; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -153,7 +153,7 @@ ol.style.Stroke.prototype.setLineCap = function(lineCap) { */ ol.style.Stroke.prototype.setLineDash = function(lineDash) { this.lineDash_ = lineDash; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -165,7 +165,7 @@ ol.style.Stroke.prototype.setLineDash = function(lineDash) { */ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { this.lineJoin_ = lineJoin; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -177,7 +177,7 @@ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { */ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { this.miterLimit_ = miterLimit; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -189,7 +189,7 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { */ ol.style.Stroke.prototype.setWidth = function(width) { this.width_ = width; - this.checksum_ = null; + this.checksum_ = undefined; }; @@ -197,7 +197,7 @@ ol.style.Stroke.prototype.setWidth = function(width) { * @inheritDoc */ ol.style.Stroke.prototype.getChecksum = function() { - if (goog.isNull(this.checksum_)) { + if (!goog.isDef(this.checksum_)) { var raw = 's' + (!goog.isNull(this.color_) ? ol.color.asString(this.color_) : '-') + ',' + From e109be4b57384b09404b69579b29355323ca499a Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Thu, 4 Dec 2014 11:49:50 +0100 Subject: [PATCH 97/98] Rename `size` to `initialSize` --- examples/symbol-atlas-webgl.js | 4 ++-- externs/olx.js | 4 ++-- src/ol/style/atlasmanager.js | 3 ++- test/spec/ol/style/atlasmanager.test.js | 15 ++++++++------- test/spec/ol/style/circlestyle.test.js | 4 ++-- test/spec/ol/style/regularshapestyle.test.js | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/symbol-atlas-webgl.js b/examples/symbol-atlas-webgl.js index 77e9fda7da..22423c2adf 100644 --- a/examples/symbol-atlas-webgl.js +++ b/examples/symbol-atlas-webgl.js @@ -12,9 +12,9 @@ goog.require('ol.style.Stroke'); goog.require('ol.style.Style'); var atlasManager = new ol.style.AtlasManager({ - // we increase the default size so that all symbols fit into + // we increase the initial size so that all symbols fit into // a single atlas image - size: 512 + initialSize: 512 }); var symbolInfo = [{ diff --git a/externs/olx.js b/externs/olx.js index ed6df1401c..d0a5724a18 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -6254,7 +6254,7 @@ olx.ViewState.prototype.rotation; /** - * @typedef {{size: (number|undefined), + * @typedef {{initialSize: (number|undefined), * maxSize: (number|undefined), * space: (number|undefined)}} * @api @@ -6267,7 +6267,7 @@ olx.style.AtlasManagerOptions; * @type {number|undefined} * @api */ -olx.style.AtlasManagerOptions.prototype.size; +olx.style.AtlasManagerOptions.prototype.initialSize; /** diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index 990a98d622..7dd22fc611 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -47,7 +47,8 @@ ol.style.AtlasManager = function(opt_options) { * @private * @type {number} */ - this.currentSize_ = goog.isDef(options.size) ? options.size : 256; + this.currentSize_ = goog.isDef(options.initialSize) ? + options.initialSize : 256; /** * The maximum size in pixels of atlas images. diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js index 3e28fd8e4d..66f5707fd1 100644 --- a/test/spec/ol/style/atlasmanager.test.js +++ b/test/spec/ol/style/atlasmanager.test.js @@ -186,7 +186,7 @@ describe('ol.style.AtlasManager', function() { describe('#add', function() { it('adds one entry', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); var info = manager.add('1', 32, 32, defaultRender); expect(info).to.eql({ @@ -197,7 +197,7 @@ describe('ol.style.AtlasManager', function() { }); it('adds one entry (also to the hit detection atlas)', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); var info = manager.add('1', 32, 32, defaultRender, defaultRender); expect(info).to.eql({ @@ -209,7 +209,7 @@ describe('ol.style.AtlasManager', function() { }); it('creates a new atlas if needed', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); expect(manager.add('1', 100, 100, defaultRender, defaultRender)) .to.be.ok(); var info = manager.add('2', 100, 100, defaultRender, defaultRender); @@ -221,7 +221,7 @@ describe('ol.style.AtlasManager', function() { }); it('creates new atlases until one is large enough', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); expect(manager.add('1', 100, 100, defaultRender, defaultRender)) .to.be.ok(); expect(manager.atlases_).to.have.length(1); @@ -235,7 +235,7 @@ describe('ol.style.AtlasManager', function() { }); it('checks all existing atlases and create a new if needed', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); expect(manager.add('1', 100, 100, defaultRender, defaultRender)) .to.be.ok(); expect(manager.add('2', 100, 100, defaultRender, defaultRender)) @@ -251,7 +251,8 @@ describe('ol.style.AtlasManager', function() { }); it('returns null if the size exceeds the maximum size', function() { - var manager = new ol.style.AtlasManager({size: 128, maxSize: 2048}); + var manager = new ol.style.AtlasManager( + {initialSize: 128, maxSize: 2048}); expect(manager.add('1', 100, 100, defaultRender, defaultRender)) .to.be.ok(); expect(manager.add('2', 2048, 2048, defaultRender, defaultRender)) @@ -262,7 +263,7 @@ describe('ol.style.AtlasManager', function() { describe('#getInfo', function() { it('returns null if no entry for the given id', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({initialSize: 128}); expect(manager.getInfo('123456')).to.eql(null); }); }); diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js index a7bc6ba40d..4e5d0b985c 100644 --- a/test/spec/ol/style/circlestyle.test.js +++ b/test/spec/ol/style/circlestyle.test.js @@ -39,7 +39,7 @@ describe('ol.style.Circle', function() { }); it('adds itself to an atlas manager (no fill-style)', function() { - var atlasManager = new ol.style.AtlasManager({size: 512}); + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); var style = new ol.style.Circle({radius: 10, atlasManager: atlasManager}); expect(style.getImage()).to.be.an(HTMLCanvasElement); expect(style.getSize()).to.eql([21, 21]); @@ -54,7 +54,7 @@ describe('ol.style.Circle', function() { }); it('adds itself to an atlas manager (fill-style)', function() { - var atlasManager = new ol.style.AtlasManager({size: 512}); + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); var style = new ol.style.Circle({ radius: 10, atlasManager: atlasManager, diff --git a/test/spec/ol/style/regularshapestyle.test.js b/test/spec/ol/style/regularshapestyle.test.js index 585d34c2f6..1d8290b61d 100644 --- a/test/spec/ol/style/regularshapestyle.test.js +++ b/test/spec/ol/style/regularshapestyle.test.js @@ -39,7 +39,7 @@ describe('ol.style.RegularShape', function() { }); it('adds itself to an atlas manager (no fill-style)', function() { - var atlasManager = new ol.style.AtlasManager({size: 512}); + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); var style = new ol.style.RegularShape( {radius: 10, atlasManager: atlasManager}); expect(style.getImage()).to.be.an(HTMLCanvasElement); @@ -55,7 +55,7 @@ describe('ol.style.RegularShape', function() { }); it('adds itself to an atlas manager (fill-style)', function() { - var atlasManager = new ol.style.AtlasManager({size: 512}); + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); var style = new ol.style.RegularShape({ radius: 10, atlasManager: atlasManager, From b2e419654bffc1c1bc2844a52014e0d757267187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 4 Dec 2014 12:13:47 +0100 Subject: [PATCH 98/98] Add ol.INITIAL_ATLAS_SIZE and ol.MAX_ATLAS_SIZE --- externs/olx.js | 11 +++++++---- src/ol/ol.js | 15 +++++++++++++++ src/ol/style/atlasmanager.js | 9 ++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index d0a5724a18..4df39a1c48 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -6263,7 +6263,8 @@ olx.style.AtlasManagerOptions; /** - * The size in pixels of the first atlas image (default: 256). + * The size in pixels of the first atlas image. If no value is given the + * `ol.INITIAL_ATLAS_SIZE` compile-time constant will be used. * @type {number|undefined} * @api */ @@ -6271,9 +6272,11 @@ olx.style.AtlasManagerOptions.prototype.initialSize; /** - * The maximum size in pixels of atlas images. If no value is given, - * `ol.WEBGL_MAX_TEXTURE_SIZE` will be used (if WebGL is supported), otherwise - * the default is 2048. + * The maximum size in pixels of atlas images. If no value is given then + * the `ol.MAX_ATLAS_SIZE` compile-time constant will be used. And if + * `ol.MAX_ATLAS_SIZE` is set to `-1` (the default) then + * `ol.WEBGL_MAX_TEXTURE_SIZE` will used if WebGL is supported. Otherwise + * 2048 is used. * @type {number|undefined} * @api */ diff --git a/src/ol/ol.js b/src/ol/ol.js index 02c1f30ec1..01ca364939 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -152,6 +152,13 @@ ol.ENABLE_WEBGL = true; ol.LEGACY_IE_SUPPORT = false; +/** + * @define {number} The size in pixels of the first atlas image. Default is + * `256`. + */ +ol.INITIAL_ATLAS_SIZE = 256; + + /** * The page is loaded using HTTPS. * @const @@ -175,6 +182,14 @@ ol.IS_LEGACY_IE = goog.userAgent.IE && ol.KEYBOARD_PAN_DURATION = 100; +/** + * @define {number} The maximum size in pixels of atlas images. Default is + * `-1`, meaning it is not used (and `ol.ol.WEBGL_MAX_TEXTURE_SIZE` is + * used instead). + */ +ol.MAX_ATLAS_SIZE = -1; + + /** * @define {number} Maximum mouse wheel delta. */ diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index 7dd22fc611..a3cb260eca 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -5,6 +5,7 @@ goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.object'); +goog.require('ol'); /** @@ -48,15 +49,17 @@ ol.style.AtlasManager = function(opt_options) { * @type {number} */ this.currentSize_ = goog.isDef(options.initialSize) ? - options.initialSize : 256; + options.initialSize : ol.INITIAL_ATLAS_SIZE; /** * The maximum size in pixels of atlas images. * @private * @type {number} */ - this.maxSize_ = goog.isDef(options.maxSize) ? options.maxSize : - goog.isDef(ol.WEBGL_MAX_TEXTURE_SIZE) ? ol.WEBGL_MAX_TEXTURE_SIZE : 2048; + this.maxSize_ = goog.isDef(options.maxSize) ? + options.maxSize : ol.MAX_ATLAS_SIZE != -1 ? + ol.MAX_ATLAS_SIZE : goog.isDef(ol.WEBGL_MAX_TEXTURE_SIZE) ? + ol.WEBGL_MAX_TEXTURE_SIZE : 2048; /** * The size in pixels between images.

Draw features example

-

Example of using the ol.interaction.Draw interaction.

+

Example of using the ol.interaction.Draw interaction. Webgl renderering is experimental and limited to points.

From 336de4b1b111eb83262648aa76a24a7d791ca936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 15:13:51 +0100 Subject: [PATCH 81/98] Revert changes to the icon example The changes were made for testing WebGL, and we now have specific examples for WebGL point. --- examples/icon.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/icon.js b/examples/icon.js index 5ffb2e1ad7..d510cf16fa 100644 --- a/examples/icon.js +++ b/examples/icon.js @@ -5,7 +5,7 @@ goog.require('ol.View'); goog.require('ol.geom.Point'); goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); -goog.require('ol.source.OSM'); +goog.require('ol.source.TileJSON'); goog.require('ol.source.Vector'); goog.require('ol.style.Icon'); goog.require('ol.style.Style'); @@ -39,11 +39,12 @@ var vectorLayer = new ol.layer.Vector({ }); var rasterLayer = new ol.layer.Tile({ - source: new ol.source.OSM() + source: new ol.source.TileJSON({ + url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.jsonp' + }) }); var map = new ol.Map({ - renderer: exampleNS.getRendererFromQueryString(), layers: [rasterLayer, vectorLayer], target: document.getElementById('map'), view: new ol.View({ From 2e873d3e6d8c47fc0189ec3de057867f7f906209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 15:15:06 +0100 Subject: [PATCH 82/98] Revert changes to the synthetic-points example These changes were made for testing WebGL. We now have specific examples for WebGL point. --- examples/synthetic-points.js | 61 ++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/examples/synthetic-points.js b/examples/synthetic-points.js index c26a5615b8..4381fd355c 100644 --- a/examples/synthetic-points.js +++ b/examples/synthetic-points.js @@ -1,6 +1,7 @@ goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.View'); +goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); @@ -18,7 +19,7 @@ for (var i = 0; i < count; ++i) { 'geometry': new ol.geom.Point( [2 * e * Math.random() - e, 2 * e * Math.random() - e]), 'i': i, - 'size': 20 + 'size': i % 2 ? 10 : 20 }); } @@ -50,7 +51,6 @@ var vector = new ol.layer.Vector({ }); var map = new ol.Map({ - renderer: exampleNS.getRendererFromQueryString(), layers: [vector], target: document.getElementById('map'), view: new ol.View({ @@ -59,6 +59,63 @@ var map = new ol.Map({ }) }); +var point = null; +var line = null; +var displaySnap = function(coordinate) { + var closestFeature = vectorSource.getClosestFeatureToCoordinate(coordinate); + if (closestFeature === null) { + point = null; + line = null; + } else { + var geometry = closestFeature.getGeometry(); + var closestPoint = geometry.getClosestPoint(coordinate); + if (point === null) { + point = new ol.geom.Point(closestPoint); + } else { + point.setCoordinates(closestPoint); + } + if (line === null) { + line = new ol.geom.LineString([coordinate, closestPoint]); + } else { + line.setCoordinates([coordinate, closestPoint]); + } + } + map.render(); +}; + +$(map.getViewport()).on('mousemove', function(evt) { + var coordinate = map.getEventCoordinate(evt.originalEvent); + displaySnap(coordinate); +}); + +map.on('click', function(evt) { + displaySnap(evt.coordinate); +}); + +var imageStyle = new ol.style.Circle({ + radius: 10, + fill: null, + stroke: new ol.style.Stroke({ + color: 'rgba(255,255,0,0.9)', + width: 3 + }) +}); +var strokeStyle = new ol.style.Stroke({ + color: 'rgba(255,255,0,0.9)', + width: 3 +}); +map.on('postcompose', function(evt) { + var vectorContext = evt.vectorContext; + if (point !== null) { + vectorContext.setImageStyle(imageStyle); + vectorContext.drawPointGeometry(point); + } + if (line !== null) { + vectorContext.setFillStrokeStyle(null, strokeStyle); + vectorContext.drawLineStringGeometry(line); + } +}); + $(map.getViewport()).on('mousemove', function(e) { var pixel = map.getEventPixel(e.originalEvent); From 6360e4497d5c86ecf76340c5f3ba29883bd7d546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 15:35:59 +0100 Subject: [PATCH 83/98] Add a comment to WebGL ImageReplay A comment explaining why we don't need to call deleteProgram and deleteShader in the function returned by getDeleteResourcesFunction. --- src/ol/render/webgl/webglreplay.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index ee9d2190f3..d78be4b5f9 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -201,6 +201,10 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction = function(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other ImageReplay instances (for other layers). And + // they will be deleted when disposing of the ol.webgl.Context + // object. goog.asserts.assert(!goog.isNull(this.verticesBuffer_)); goog.asserts.assert(!goog.isNull(this.indicesBuffer_)); var verticesBuffer = this.verticesBuffer_; From 57db47ac18820c3df0bbcd7303963182b443fa0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 15:48:31 +0100 Subject: [PATCH 84/98] Rename ol.structs.Buffer to ol.webgl.Buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Buffer class is WebGL specific, and it's not really a "data structure", in the pure sense of the term. --- src/ol/render/webgl/webglreplay.js | 10 +++++----- src/ol/renderer/webgl/webgllayerrenderer.js | 6 +++--- src/ol/renderer/webgl/webgltilelayerrenderer.js | 6 +++--- src/ol/{structs => webgl}/buffer.js | 12 ++++++------ src/ol/webgl/context.js | 8 ++++---- test/spec/ol/{structs => webgl}/buffer.test.js | 12 ++++++------ 6 files changed, 27 insertions(+), 27 deletions(-) rename src/ol/{structs => webgl}/buffer.js (71%) rename test/spec/ol/{structs => webgl}/buffer.test.js (76%) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index ee9d2190f3..07d847eab8 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -11,8 +11,8 @@ goog.require('ol.extent'); goog.require('ol.render.IReplayGroup'); goog.require('ol.render.webgl.imagereplay.shader.Color'); goog.require('ol.render.webgl.imagereplay.shader.Default'); -goog.require('ol.structs.Buffer'); goog.require('ol.vec.Mat4'); +goog.require('ol.webgl.Buffer'); @@ -97,7 +97,7 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { this.indices_ = []; /** - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} * @private */ this.indicesBuffer_ = null; @@ -181,7 +181,7 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { this.vertices_ = []; /** - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} * @private */ this.verticesBuffer_ = null; @@ -429,7 +429,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { goog.asserts.assert(this.images_.length == this.groupIndices_.length); // create, bind, and populate the vertices buffer - this.verticesBuffer_ = new ol.structs.Buffer(this.vertices_); + this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_); context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); var indices = this.indices_; @@ -439,7 +439,7 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { indices[indices.length - 1], context.hasOESElementIndexUint); // create, bind, and populate the indices buffer - this.indicesBuffer_ = new ol.structs.Buffer(indices); + this.indicesBuffer_ = new ol.webgl.Buffer(indices); context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); goog.asserts.assert(this.textures_.length === 0); diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 39d5248f10..4f09f7b881 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -10,7 +10,7 @@ goog.require('ol.render.webgl.Immediate'); goog.require('ol.renderer.Layer'); goog.require('ol.renderer.webgl.map.shader.Color'); goog.require('ol.renderer.webgl.map.shader.Default'); -goog.require('ol.structs.Buffer'); +goog.require('ol.webgl.Buffer'); @@ -26,9 +26,9 @@ ol.renderer.webgl.Layer = function(mapRenderer, layer) { /** * @private - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} */ - this.arrayBuffer_ = new ol.structs.Buffer([ + this.arrayBuffer_ = new ol.webgl.Buffer([ -1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 426f0fbc4e..005eebcf38 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -16,8 +16,8 @@ goog.require('ol.layer.Tile'); goog.require('ol.math'); goog.require('ol.renderer.webgl.Layer'); goog.require('ol.renderer.webgl.tilelayer.shader'); -goog.require('ol.structs.Buffer'); goog.require('ol.tilecoord'); +goog.require('ol.webgl.Buffer'); @@ -52,9 +52,9 @@ ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) { /** * @private - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} */ - this.renderArrayBuffer_ = new ol.structs.Buffer([ + this.renderArrayBuffer_ = new ol.webgl.Buffer([ 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, diff --git a/src/ol/structs/buffer.js b/src/ol/webgl/buffer.js similarity index 71% rename from src/ol/structs/buffer.js rename to src/ol/webgl/buffer.js index 7faf1a4029..a39194ccf6 100644 --- a/src/ol/structs/buffer.js +++ b/src/ol/webgl/buffer.js @@ -1,4 +1,4 @@ -goog.provide('ol.structs.Buffer'); +goog.provide('ol.webgl.Buffer'); goog.require('goog.array'); goog.require('goog.webgl'); @@ -8,7 +8,7 @@ goog.require('ol'); /** * @enum {number} */ -ol.structs.BufferUsage = { +ol.webgl.BufferUsage = { STATIC_DRAW: goog.webgl.STATIC_DRAW, STREAM_DRAW: goog.webgl.STREAM_DRAW, DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW @@ -22,7 +22,7 @@ ol.structs.BufferUsage = { * @param {number=} opt_usage Usage. * @struct */ -ol.structs.Buffer = function(opt_arr, opt_usage) { +ol.webgl.Buffer = function(opt_arr, opt_usage) { /** * @private @@ -35,7 +35,7 @@ ol.structs.Buffer = function(opt_arr, opt_usage) { * @type {number} */ this.usage_ = goog.isDef(opt_usage) ? - opt_usage : ol.structs.BufferUsage.STATIC_DRAW; + opt_usage : ol.webgl.BufferUsage.STATIC_DRAW; }; @@ -43,7 +43,7 @@ ol.structs.Buffer = function(opt_arr, opt_usage) { /** * @return {Array.} Array. */ -ol.structs.Buffer.prototype.getArray = function() { +ol.webgl.Buffer.prototype.getArray = function() { return this.arr_; }; @@ -51,6 +51,6 @@ ol.structs.Buffer.prototype.getArray = function() { /** * @return {number} Usage. */ -ol.structs.Buffer.prototype.getUsage = function() { +ol.webgl.Buffer.prototype.getUsage = function() { return this.usage_; }; diff --git a/src/ol/webgl/context.js b/src/ol/webgl/context.js index 4920b290ef..cacb9aeb5f 100644 --- a/src/ol/webgl/context.js +++ b/src/ol/webgl/context.js @@ -6,12 +6,12 @@ goog.require('goog.events'); goog.require('goog.log'); goog.require('goog.object'); goog.require('ol.has'); -goog.require('ol.structs.Buffer'); +goog.require('ol.webgl.Buffer'); goog.require('ol.webgl.WebGLContextEventType'); /** - * @typedef {{buf: ol.structs.Buffer, + * @typedef {{buf: ol.webgl.Buffer, * buffer: WebGLBuffer}} */ ol.webgl.BufferCacheEntry; @@ -91,7 +91,7 @@ ol.webgl.Context = function(canvas, gl) { * the WebGL buffer, bind it, populate it, and add an entry to * the cache. * @param {number} target Target. - * @param {ol.structs.Buffer} buf Buffer. + * @param {ol.webgl.Buffer} buf Buffer. */ ol.webgl.Context.prototype.bindBuffer = function(target, buf) { var gl = this.getGL(); @@ -124,7 +124,7 @@ ol.webgl.Context.prototype.bindBuffer = function(target, buf) { /** - * @param {ol.structs.Buffer} buf Buffer. + * @param {ol.webgl.Buffer} buf Buffer. */ ol.webgl.Context.prototype.deleteBuffer = function(buf) { var gl = this.getGL(); diff --git a/test/spec/ol/structs/buffer.test.js b/test/spec/ol/webgl/buffer.test.js similarity index 76% rename from test/spec/ol/structs/buffer.test.js rename to test/spec/ol/webgl/buffer.test.js index 454e36ce77..7d7153e32e 100644 --- a/test/spec/ol/structs/buffer.test.js +++ b/test/spec/ol/webgl/buffer.test.js @@ -1,7 +1,7 @@ -goog.provide('ol.test.structs.Buffer'); +goog.provide('ol.test.webgl.Buffer'); -describe('ol.structs.Buffer', function() { +describe('ol.webgl.Buffer', function() { describe('constructor', function() { @@ -9,7 +9,7 @@ describe('ol.structs.Buffer', function() { var b; beforeEach(function() { - b = new ol.structs.Buffer(); + b = new ol.webgl.Buffer(); }); it('constructs an empty instance', function() { @@ -22,7 +22,7 @@ describe('ol.structs.Buffer', function() { var b; beforeEach(function() { - b = new ol.structs.Buffer([0, 1, 2, 3]); + b = new ol.webgl.Buffer([0, 1, 2, 3]); }); it('constructs a populated instance', function() { @@ -37,7 +37,7 @@ describe('ol.structs.Buffer', function() { var b; beforeEach(function() { - b = new ol.structs.Buffer(); + b = new ol.webgl.Buffer(); }); describe('getArray', function() { @@ -52,4 +52,4 @@ describe('ol.structs.Buffer', function() { }); -goog.require('ol.structs.Buffer'); +goog.require('ol.webgl.Buffer'); From 857a8ca2bb28c3fbd0936eb330fa57db53e65de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 16:04:04 +0100 Subject: [PATCH 85/98] Delete WebGL resources used at postcompose time --- src/ol/renderer/webgl/webglmaprenderer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index a5c6485b42..b7d6530670 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -302,6 +302,7 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = replayGroup.replay(context, center, resolution, rotation, size, extent, pixelRatio, opacity, brightness, contrast, hue, saturation, {}); } + replayGroup.getDeleteResourcesFunction(context)(); vectorContext.flush(); this.replayGroup = replayGroup; From b3369cd51699c47de7951082b59f7ea921cd9c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 16:09:46 +0100 Subject: [PATCH 86/98] Delete WebGL resources used by immediate API --- src/ol/render/webgl/webglimmediate.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ol/render/webgl/webglimmediate.js b/src/ol/render/webgl/webglimmediate.js index 483c341c35..37b8a37e19 100644 --- a/src/ol/render/webgl/webglimmediate.js +++ b/src/ol/render/webgl/webglimmediate.js @@ -171,11 +171,12 @@ ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry = */ ol.render.webgl.Immediate.prototype.drawPointGeometry = function(pointGeometry, data) { + var context = this.context_; var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE); replay.setImageStyle(this.imageStyle_); replay.drawPointGeometry(pointGeometry, data); - replay.finish(this.context_); + replay.finish(context); // default colors var opacity = 1; var brightness = 0; @@ -185,6 +186,7 @@ ol.render.webgl.Immediate.prototype.drawPointGeometry = replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, this.size_, this.extent_, this.pixelRatio_, opacity, brightness, contrast, hue, saturation, {}); + replay.getDeleteResourcesFunction(context)(); }; @@ -212,11 +214,12 @@ ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry = */ ol.render.webgl.Immediate.prototype.drawMultiPointGeometry = function(multiPointGeometry, data) { + var context = this.context_; var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE); replay.setImageStyle(this.imageStyle_); replay.drawMultiPointGeometry(multiPointGeometry, data); - replay.finish(this.context_); + replay.finish(context); // default colors var opacity = 1; var brightness = 0; @@ -226,6 +229,7 @@ ol.render.webgl.Immediate.prototype.drawMultiPointGeometry = replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, this.size_, this.extent_, this.pixelRatio_, opacity, brightness, contrast, hue, saturation, {}); + replay.getDeleteResourcesFunction(context)(); }; From c0bbb6c4fae826ae68d8f2b281883376c22ea4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 21 Nov 2014 17:48:58 +0100 Subject: [PATCH 87/98] Disable vertex attrib array after replay --- src/ol/render/webgl/webglreplay.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 766f734d55..a2263934ca 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -638,6 +638,13 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); start = end; } + + // disable the vertex attrib arrays + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_offsets); + gl.disableVertexAttribArray(locations.a_texCoord); + gl.disableVertexAttribArray(locations.a_opacity); + gl.disableVertexAttribArray(locations.a_rotateWithView); }; From e307579d49ed3986926327802c79aab710acef7f Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Mon, 24 Nov 2014 09:42:10 +0100 Subject: [PATCH 88/98] Move constants from `ol.has` to `ol` --- examples/symbol-atlas-webgl.js | 2 +- src/ol/has.js | 22 ++-------------------- src/ol/ol.js | 18 ++++++++++++++++++ src/ol/webgl/context.js | 3 +-- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/examples/symbol-atlas-webgl.js b/examples/symbol-atlas-webgl.js index 63dd568bb3..4b1ece67f7 100644 --- a/examples/symbol-atlas-webgl.js +++ b/examples/symbol-atlas-webgl.js @@ -15,7 +15,7 @@ var atlasManager = new ol.style.AtlasManager({ // we increase the default size so that all symbols fit into // a single atlas image size: 512, - maxSize: ol.has.WEBGL_MAX_TEXTURE_SIZE + maxSize: ol.WEBGL_MAX_TEXTURE_SIZE }); var symbolInfo = [{ diff --git a/src/ol/has.js b/src/ol/has.js index 720977cef4..f86249f976 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -122,24 +122,6 @@ ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled); ol.has.WEBGL; -/** - * The maximum supported WebGL texture size in pixels. If WebGL is not - * supported, the value is set to `undefined`. - * @const - * @type {number|undefined} - * @api - */ -ol.has.WEBGL_MAX_TEXTURE_SIZE; - - -/** - * List of supported WebGL extensions. - * @const - * @type {Array.} - */ -ol.has.WEBGL_EXTENSIONS; - - (function() { if (ol.ENABLE_WEBGL) { var hasWebGL = false; @@ -162,7 +144,7 @@ ol.has.WEBGL_EXTENSIONS; } catch (e) {} } ol.has.WEBGL = hasWebGL; - ol.has.WEBGL_MAX_TEXTURE_SIZE = textureSize; - ol.has.WEBGL_EXTENSIONS = extensions; + ol.WEBGL_EXTENSIONS = extensions; + ol.WEBGL_MAX_TEXTURE_SIZE = textureSize; } })(); diff --git a/src/ol/ol.js b/src/ol/ol.js index 6e0968f9d3..02c1f30ec1 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -219,6 +219,24 @@ ol.SIMPLIFY_TOLERANCE = 0.5; ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024; +/** + * The maximum supported WebGL texture size in pixels. If WebGL is not + * supported, the value is set to `undefined`. + * @const + * @type {number|undefined} + * @api + */ +ol.WEBGL_MAX_TEXTURE_SIZE; // value is set in `ol.has` + + +/** + * List of supported WebGL extensions. + * @const + * @type {Array.} + */ +ol.WEBGL_EXTENSIONS; // value is set in `ol.has` + + /** * @define {number} Zoom slider animation duration. */ diff --git a/src/ol/webgl/context.js b/src/ol/webgl/context.js index cacb9aeb5f..50403e2341 100644 --- a/src/ol/webgl/context.js +++ b/src/ol/webgl/context.js @@ -5,7 +5,6 @@ goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.log'); goog.require('goog.object'); -goog.require('ol.has'); goog.require('ol.webgl.Buffer'); goog.require('ol.webgl.WebGLContextEventType'); @@ -70,7 +69,7 @@ ol.webgl.Context = function(canvas, gl) { * @type {boolean} */ this.hasOESElementIndexUint = goog.array.contains( - ol.has.WEBGL_EXTENSIONS, 'OES_element_index_uint'); + ol.WEBGL_EXTENSIONS, 'OES_element_index_uint'); // use the OES_element_index_uint extension if available if (this.hasOESElementIndexUint) { From b8b48afc8237ed552de06234c238ab5b7ba86954 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Mon, 24 Nov 2014 09:57:09 +0100 Subject: [PATCH 89/98] Use WEBGL_MAX_TEXTURE_SIZE as default --- examples/symbol-atlas-webgl.js | 3 +-- externs/olx.js | 4 +++- src/ol/style/atlasmanager.js | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/symbol-atlas-webgl.js b/examples/symbol-atlas-webgl.js index 4b1ece67f7..77e9fda7da 100644 --- a/examples/symbol-atlas-webgl.js +++ b/examples/symbol-atlas-webgl.js @@ -14,8 +14,7 @@ goog.require('ol.style.Style'); var atlasManager = new ol.style.AtlasManager({ // we increase the default size so that all symbols fit into // a single atlas image - size: 512, - maxSize: ol.WEBGL_MAX_TEXTURE_SIZE + size: 512 }); var symbolInfo = [{ diff --git a/externs/olx.js b/externs/olx.js index 9697bb9332..ed6df1401c 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -6271,7 +6271,9 @@ olx.style.AtlasManagerOptions.prototype.size; /** - * The maximum size in pixels of atlas images (default: 2048). + * The maximum size in pixels of atlas images. If no value is given, + * `ol.WEBGL_MAX_TEXTURE_SIZE` will be used (if WebGL is supported), otherwise + * the default is 2048. * @type {number|undefined} * @api */ diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index 5eb7bf9ce4..f300ee0cce 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -30,10 +30,8 @@ ol.style.AtlasManagerInfo; * atlas. After that, when new atlases are created, they will have * twice the size as the latest atlas (until `maxSize` is reached). * - * When used for WebGL, it is recommended to use `ol.has.WEBGL_MAX_TEXTURE_SIZE` - * as `maxSize` value. Also, if an application uses a lot, or a lot of - * large images, it is recommend to set a higher `size` value to avoid - * the creation of too many atlases. + * If an application uses a lot, or a lot of large images, it is recommend to + * set a higher `size` value to avoid the creation of too many atlases. * * @constructor * @struct @@ -56,7 +54,8 @@ ol.style.AtlasManager = function(opt_options) { * @private * @type {number} */ - this.maxSize_ = goog.isDef(options.maxSize) ? options.maxSize : 2048; + this.maxSize_ = goog.isDef(options.maxSize) ? options.maxSize : + goog.isDef(ol.WEBGL_MAX_TEXTURE_SIZE) ? ol.WEBGL_MAX_TEXTURE_SIZE : 2048; /** * The size in pixels between images. From d6841e6d9d75eccff3f43d63aba3de5ccbad8b20 Mon Sep 17 00:00:00 2001 From: tsauerwein Date: Mon, 24 Nov 2014 13:26:45 +0100 Subject: [PATCH 90/98] Fix tests for when not run with WebGL support --- src/ol/style/atlasmanager.js | 2 +- src/ol/style/regularshapestyle.js | 2 +- src/ol/webgl/context.js | 1 + test/spec/ol/style/atlasmanager.test.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index f300ee0cce..f53ccb3c24 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -30,7 +30,7 @@ ol.style.AtlasManagerInfo; * atlas. After that, when new atlases are created, they will have * twice the size as the latest atlas (until `maxSize` is reached). * - * If an application uses a lot, or a lot of large images, it is recommend to + * If an application uses a lot, or a lot of large images, it is recommended to * set a higher `size` value to avoid the creation of too many atlases. * * @constructor diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js index 9eb5864ade..823ef86629 100644 --- a/src/ol/style/regularshapestyle.js +++ b/src/ol/style/regularshapestyle.js @@ -322,7 +322,7 @@ ol.style.RegularShape.prototype.render_ = function(atlasManager) { var info = atlasManager.add( id, size, size, goog.bind(this.draw_, this, renderOptions), renderHitDetectionCallback); - goog.asserts.assert(info !== null, 'shape size is too large'); + goog.asserts.assert(!goog.isNull(info), 'shape size is too large'); this.canvas_ = info.image; this.origin_ = [info.offsetX, info.offsetY]; diff --git a/src/ol/webgl/context.js b/src/ol/webgl/context.js index 50403e2341..6b91f6c2a7 100644 --- a/src/ol/webgl/context.js +++ b/src/ol/webgl/context.js @@ -5,6 +5,7 @@ goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.log'); goog.require('goog.object'); +goog.require('ol'); goog.require('ol.webgl.Buffer'); goog.require('ol.webgl.WebGLContextEventType'); diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js index 69c414ba07..3e28fd8e4d 100644 --- a/test/spec/ol/style/atlasmanager.test.js +++ b/test/spec/ol/style/atlasmanager.test.js @@ -251,7 +251,7 @@ describe('ol.style.AtlasManager', function() { }); it('returns null if the size exceeds the maximum size', function() { - var manager = new ol.style.AtlasManager({size: 128}); + var manager = new ol.style.AtlasManager({size: 128, maxSize: 2048}); expect(manager.add('1', 100, 100, defaultRender, defaultRender)) .to.be.ok(); expect(manager.add('2', 2048, 2048, defaultRender, defaultRender)) From 1845665306ad288c90e59c904050a036e5487032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 27 Nov 2014 17:13:25 +0100 Subject: [PATCH 91/98] Correct spelling for "performance" --- examples/icon-sprite-webgl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html index b6c90d1cbb..7f07409746 100644 --- a/examples/icon-sprite-webgl.html +++ b/examples/icon-sprite-webgl.html @@ -35,7 +35,7 @@

Icon sprite with WebGL.

See the icon-sprite-webgl.js source to see how this is done.

-

In this example a sprite image is used for the icon styles. Using a sprite is required to get good performances with WebGL.

+

In this example a sprite image is used for the icon styles. Using a sprite is required to get good performance with WebGL.

webgl, icon, sprite, vector, point