From acc97a53eb2f3255211a0319e8a288ebd85e02c7 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 5 Feb 2015 14:15:45 -0700 Subject: [PATCH] Raster source for composing pixels from other sources --- examples/raster.html | 13 ++ examples/raster.js | 28 +++ externs/olx.js | 25 +++ src/ol/raster/operation.js | 24 +++ src/ol/raster/pixel.js | 9 + src/ol/renderer/layerrenderer.js | 1 - src/ol/source/rastersource.js | 316 +++++++++++++++++++++++++++++++ 7 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 examples/raster.html create mode 100644 examples/raster.js create mode 100644 src/ol/raster/operation.js create mode 100644 src/ol/raster/pixel.js create mode 100644 src/ol/source/rastersource.js diff --git a/examples/raster.html b/examples/raster.html new file mode 100644 index 0000000000..52553ef66d --- /dev/null +++ b/examples/raster.html @@ -0,0 +1,13 @@ +--- +template: example.html +title: Raster Source +shortdesc: Demonstrates pixelwise operations with a raster source. +docs: > + A dynamically generated raster source. +tags: "raster, pixel" +--- +
+
+
+
+
diff --git a/examples/raster.js b/examples/raster.js new file mode 100644 index 0000000000..34a95f852e --- /dev/null +++ b/examples/raster.js @@ -0,0 +1,28 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Image'); +goog.require('ol.source.OSM'); +goog.require('ol.source.Raster'); + + +var map = new ol.Map({ + layers: [ + new ol.layer.Image({ + source: new ol.source.Raster({ + sources: [new ol.source.OSM()], + operations: [function(pixels) { + var pixel = pixels[0]; + var b = pixel[2]; + pixel[2] = pixel[0]; + pixel[0] = b; + return pixels; + }] + }) + }) + ], + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 2 + }) +}); diff --git a/externs/olx.js b/externs/olx.js index 4f8aa300cf..da432b6c26 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4498,6 +4498,31 @@ olx.source.ImageVectorOptions.prototype.source; olx.source.ImageVectorOptions.prototype.style; +/** + * @typedef {{sources: Array., + * operations: (Array.|undefined)}} + * @api + */ +olx.source.RasterOptions; + + +/** + * Input sources. + * @type {Array.} + * @api + */ +olx.source.RasterOptions.prototype.sources; + + +/** + * Pixel operations. Operations will be called with pixels from input sources + * and the final output will be assigned to the raster source. + * @type {Array.|undefined} + * @api + */ +olx.source.RasterOptions.prototype.operations; + + /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js new file mode 100644 index 0000000000..b0430fba6b --- /dev/null +++ b/src/ol/raster/operation.js @@ -0,0 +1,24 @@ +goog.provide('ol.raster.IdentityOp'); +goog.provide('ol.raster.Operation'); + + +/** + * A function that takes an array of {@link ol.raster.Pixel} as inputs, performs + * some operation on them, and returns an array of {@link ol.raster.Pixel} as + * outputs. + * + * @typedef {function(Array.): Array.} + * @api + */ +ol.raster.Operation; + + +/** + * The identity operation for pixels. Returns the supplied input pixels as + * outputs. + * @param {Array.} inputs Input pixels. + * @return {Array.} The input pixels as output. + */ +ol.raster.IdentityOp = function(inputs) { + return inputs; +}; diff --git a/src/ol/raster/pixel.js b/src/ol/raster/pixel.js new file mode 100644 index 0000000000..d6b2d2bfd4 --- /dev/null +++ b/src/ol/raster/pixel.js @@ -0,0 +1,9 @@ +goog.provide('ol.raster.Pixel'); + + +/** + * An array of numbers representing pixel values. + * @typedef {Array.} ol.raster.Pixel + * @api + */ +ol.raster.Pixel; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index f1557c9f2e..6809d8c97e 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -113,7 +113,6 @@ ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { /** - * @protected * @return {ol.layer.Layer} Layer. */ ol.renderer.Layer.prototype.getLayer = function() { diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js new file mode 100644 index 0000000000..8acfdbf9fd --- /dev/null +++ b/src/ol/source/rastersource.js @@ -0,0 +1,316 @@ +goog.provide('ol.source.Raster'); + +goog.require('goog.asserts'); +goog.require('goog.functions'); +goog.require('goog.vec.Mat4'); +goog.require('ol.ImageCanvas'); +goog.require('ol.TileQueue'); +goog.require('ol.dom'); +goog.require('ol.extent'); +goog.require('ol.layer.Image'); +goog.require('ol.layer.Tile'); +goog.require('ol.raster.IdentityOp'); +goog.require('ol.renderer.canvas.ImageLayer'); +goog.require('ol.renderer.canvas.TileLayer'); +goog.require('ol.source.Image'); +goog.require('ol.source.Tile'); + + + +/** + * @classdesc + * An source that transforms data from any number of input sources source using + * an array of {@link ol.raster.Operation} functions to transform input pixel + * values into output pixel values. + * + * @constructor + * @extends {ol.source.Image} + * @param {olx.source.RasterOptions} options Options. + * @api + */ +ol.source.Raster = function(options) { + + /** + * @private + * @type {Array.} + */ + this.operations_ = goog.isDef(options.operations) ? + options.operations : [ol.raster.IdentityOp]; + + /** + * @private + * @type {Array.} + */ + this.renderers_ = ol.source.Raster.createRenderers_(options.sources); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.canvasContext_ = ol.dom.createCanvasContext2D(); + + /** + * @private + * @type {ol.TileQueue} + */ + this.tileQueue_ = new ol.TileQueue( + goog.functions.constant(1), + goog.bind(this.changed, this)); + + var layerStatesArray = ol.source.Raster.getLayerStatesArray_(this.renderers_); + var layerStates = {}; + for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) { + layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; + } + + /** + * @private + * @type {olx.FrameState} + */ + this.frameState_ = { + animate: false, + attributions: {}, + coordinateToPixelMatrix: goog.vec.Mat4.createNumber(), + extent: null, + focus: null, + index: 0, + layerStates: layerStates, + layerStatesArray: layerStatesArray, + logos: {}, + pixelRatio: 1, + pixelToCoordinateMatrix: goog.vec.Mat4.createNumber(), + postRenderFunctions: [], + size: [0, 0], + skippedFeatureUids: {}, + tileQueue: this.tileQueue_, + time: Date.now(), + usedTiles: {}, + viewState: /** @type {olx.ViewState} */ ({ + rotation: 0 + }), + viewHints: [], + wantedTiles: {} + }; + + goog.base(this, { + // TODO: pass along any relevant options + }); + + +}; +goog.inherits(ol.source.Raster, ol.source.Image); + + +/** + * Update the stored frame state. + * @param {ol.Extent} extent The view extent (in map units). + * @param {number} resolution The view resolution. + * @param {ol.proj.Projection} projection The view projection. + * @return {olx.FrameState} The updated frame state. + * @private + */ +ol.source.Raster.prototype.updateFrameState_ = + function(extent, resolution, projection) { + var frameState = this.frameState_; + + var center = ol.extent.getCenter(extent); + var width = ol.extent.getWidth(extent) / resolution; + var height = ol.extent.getHeight(extent) / resolution; + + frameState.extent = extent; + frameState.focus = ol.extent.getCenter(extent); + frameState.size[0] = width; + frameState.size[1] = height; + + var viewState = frameState.viewState; + viewState.center = center; + viewState.projection = projection; + viewState.resolution = resolution; + return frameState; +}; + + +/** + * @inheritDoc + */ +ol.source.Raster.prototype.getImage = + function(extent, resolution, pixelRatio, projection) { + + var context = this.canvasContext_; + var canvas = context.canvas; + + var width = ol.extent.getWidth(extent) / resolution; + var height = ol.extent.getHeight(extent) / resolution; + + if (width !== canvas.width || + height !== canvas.height) { + canvas.width = width; + canvas.height = height; + } + + var frameState = this.updateFrameState_(extent, resolution, projection); + this.composeFrame_(frameState); + + var imageCanvas = new ol.ImageCanvas(extent, resolution, 1, + this.getAttributions(), canvas); + + return imageCanvas; +}; + + +/** + * Compose the frame. This renders data from all sources, runs pixel-wise + * operations, and renders the result to the stored canvas context. + * @param {olx.FrameState} frameState The frame state. + * @private + */ +ol.source.Raster.prototype.composeFrame_ = function(frameState) { + var len = this.renderers_.length; + var imageDatas = new Array(len); + var pixels = new Array(len); + + var context = this.canvasContext_; + var canvas = context.canvas; + + for (var i = 0; i < len; ++i) { + pixels[i] = [0, 0, 0, 0]; + imageDatas[i] = ol.source.Raster.getImageData_( + this.renderers_[i], canvas.width, canvas.height, + frameState, frameState.layerStatesArray[i]); + } + + var targetImageData = context.getImageData(0, 0, canvas.width, canvas.height); + var target = targetImageData.data; + + var source, pixel; + for (var j = 0, jj = target.length; j < jj; j += 4) { + for (var k = 0; k < len; ++k) { + source = imageDatas[k].data; + pixel = pixels[k]; + pixel[0] = source[j]; + pixel[1] = source[j + 1]; + pixel[2] = source[j + 2]; + pixel[3] = source[j + 3]; + } + this.transformPixels_(pixels); + pixel = pixels[0]; + target[j] = pixel[0]; + target[j + 1] = pixel[1]; + target[j + 2] = pixel[2]; + target[j + 3] = pixel[3]; + } + context.putImageData(targetImageData, 0, 0); + + frameState.tileQueue.loadMoreTiles(16, 16); +}; + + +/** + * Run pixel-wise operations to transform pixels. + * @param {Array.} pixels The input pixels. + * @return {Array.} The modified pixels. + * @private + */ +ol.source.Raster.prototype.transformPixels_ = function(pixels) { + for (var i = 0, ii = this.operations_.length; i < ii; ++i) { + pixels = this.operations_[i](pixels); + } + return pixels; +}; + + +/** + * Get image data from a renderer. + * @param {ol.renderer.canvas.Layer} renderer Layer renderer. + * @param {number} width Data width. + * @param {number} height Data height. + * @param {olx.FrameState} frameState The frame state. + * @param {ol.layer.LayerState} layerState The layer state. + * @return {ImageData} The image data. + * @private + */ +ol.source.Raster.getImageData_ = + function(renderer, width, height, frameState, layerState) { + renderer.prepareFrame(frameState, layerState); + var canvas = renderer.getImage(); + var imageTransform = renderer.getImageTransform(); + var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3); + var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); + return canvas.getContext('2d').getImageData( + Math.round(-dx), Math.round(-dy), + width, height); +}; + + +/** + * Get a list of layer states from a list of renderers. + * @param {Array.} renderers Layer renderers. + * @return {Array.} The layer states. + * @private + */ +ol.source.Raster.getLayerStatesArray_ = function(renderers) { + return renderers.map(function(renderer) { + return renderer.getLayer().getLayerState(); + }); +}; + + +/** + * Create renderers for all sources. + * @param {Array.} sources The sources. + * @return {Array.} Array of layer renderers. + * @private + */ +ol.source.Raster.createRenderers_ = function(sources) { + var len = sources.length; + var renderers = new Array(len); + for (var i = 0; i < len; ++i) { + renderers[i] = ol.source.Raster.createRenderer_(sources[i]); + } + return renderers; +}; + + +/** + * Create a renderer for the provided source. + * @param {ol.source.Source} source The source. + * @return {ol.renderer.canvas.Layer} The renderer. + * @private + */ +ol.source.Raster.createRenderer_ = function(source) { + var renderer = null; + if (source instanceof ol.source.Tile) { + renderer = ol.source.Raster.createTileRenderer_( + /** @type {ol.source.Tile} */ (source)); + } else if (source instanceof ol.source.Image) { + renderer = ol.source.Raster.createImageRenderer_( + /** @type {ol.source.Image} */ (source)); + } else { + goog.asserts.fail('Unsupported source type: ' + source); + } + return renderer; +}; + + +/** + * Create an image renderer for the provided source. + * @param {ol.source.Image} source The source. + * @return {ol.renderer.canvas.Layer} The renderer. + * @private + */ +ol.source.Raster.createImageRenderer_ = function(source) { + var layer = new ol.layer.Image({source: source}); + return new ol.renderer.canvas.ImageLayer(layer); +}; + + +/** + * Create a tile renderer for the provided source. + * @param {ol.source.Tile} source The source. + * @return {ol.renderer.canvas.Layer} The renderer. + * @private + */ +ol.source.Raster.createTileRenderer_ = function(source) { + var layer = new ol.layer.Tile({source: source}); + return new ol.renderer.canvas.TileLayer(layer); +};