From acc97a53eb2f3255211a0319e8a288ebd85e02c7 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 5 Feb 2015 14:15:45 -0700 Subject: [PATCH 01/28] 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); +}; From b7ad9160ef14c2139bc65b08728d3d04e0a2fa44 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 6 Feb 2015 17:52:24 -0700 Subject: [PATCH 02/28] Nicer example --- examples/raster.html | 16 +++++++++- examples/raster.js | 70 +++++++++++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/examples/raster.html b/examples/raster.html index 52553ef66d..810be90a76 100644 --- a/examples/raster.html +++ b/examples/raster.html @@ -3,7 +3,21 @@ template: example.html title: Raster Source shortdesc: Demonstrates pixelwise operations with a raster source. docs: > - A dynamically generated raster source. +

+ This example uses a ol.source.Raster to generate data + based on another source. The raster source accepts any number of + input sources (tile or image based) and runs a pipeline of + operations on the input pixels. The return from the final + operation is used as the data for the output source. +

+

+ In this case, a single tiled source of imagery is used as input. + For each pixel, the Triangular Greenness Index + (TGI) + is calculated from the input pixels. A second operation colors + those pixels based on a threshold value (values above the + threshold are green and those below are transparent). +

tags: "raster, pixel" ---
diff --git a/examples/raster.js b/examples/raster.js index 34a95f852e..873fd25e25 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -1,28 +1,58 @@ goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.layer.Image'); -goog.require('ol.source.OSM'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.BingMaps'); goog.require('ol.source.Raster'); +function tgi(pixels) { + var pixel = pixels[0]; + var r = pixel[0] / 255; + var g = pixel[1] / 255; + var b = pixel[2] / 255; + var index = (120 * (r - b) - (190 * (r - g))) / 2; + pixel[0] = index; + return pixels; +} -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 +var threshold = 10; + +function color(pixels) { + var pixel = pixels[0]; + var index = pixel[0]; + if (index > threshold) { + pixel[0] = 0; + pixel[1] = 255; + pixel[2] = 0; + pixel[3] = 255; + } else { + pixel[3] = 0; + } + return pixels; +} + +var bing = new ol.source.BingMaps({ + key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3', + imagerySet: 'Aerial' +}); + +var imagery = new ol.layer.Tile({ + source: bing +}); + +var greenness = new ol.layer.Image({ + source: new ol.source.Raster({ + sources: [bing], + operations: [tgi, color] + }) +}); + +var map = new ol.Map({ + layers: [imagery, greenness], + target: 'map', + view: new ol.View({ + center: [-9651695.964309687, 4937351.719788862], + zoom: 13, + minZoom: 12 }) }); From 2c82ca86f0399996d91a2d88dd98f09b414321cc Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 7 Feb 2015 11:51:41 -0700 Subject: [PATCH 03/28] Fire events before and after running ops --- externs/oli.js | 12 +++++++ src/ol/source/rastersource.js | 62 +++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/externs/oli.js b/externs/oli.js index 659148c77c..d8d4b86cb1 100644 --- a/externs/oli.js +++ b/externs/oli.js @@ -266,6 +266,18 @@ oli.source.ImageEvent = function() {}; oli.source.ImageEvent.prototype.image; +/** + * @interface + */ +oli.source.RasterEvent = function() {}; + + +/** + * @type {number} + */ +oli.source.RasterEvent.prototype.resolution; + + /** * @interface */ diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 8acfdbf9fd..861fe86605 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -1,6 +1,9 @@ goog.provide('ol.source.Raster'); +goog.provide('ol.source.RasterEvent'); +goog.provide('ol.source.RasterEventType'); goog.require('goog.asserts'); +goog.require('goog.events.Event'); goog.require('goog.functions'); goog.require('goog.vec.Mat4'); goog.require('ol.ImageCanvas'); @@ -182,6 +185,11 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { var targetImageData = context.getImageData(0, 0, canvas.width, canvas.height); var target = targetImageData.data; + + var resolution = frameState.viewState.resolution / frameState.pixelRatio; + this.dispatchEvent(new ol.source.RasterEvent( + ol.source.RasterEventType.BEFOREOPERATIONS, resolution)); + var source, pixel; for (var j = 0, jj = target.length; j < jj; j += 4) { for (var k = 0; k < len; ++k) { @@ -192,13 +200,17 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { pixel[2] = source[j + 2]; pixel[3] = source[j + 3]; } - this.transformPixels_(pixels); + this.runOperations_(pixels); pixel = pixels[0]; target[j] = pixel[0]; target[j + 1] = pixel[1]; target[j + 2] = pixel[2]; target[j + 3] = pixel[3]; } + + this.dispatchEvent(new ol.source.RasterEvent( + ol.source.RasterEventType.AFTEROPERATIONS, resolution)); + context.putImageData(targetImageData, 0, 0); frameState.tileQueue.loadMoreTiles(16, 16); @@ -211,7 +223,7 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { * @return {Array.} The modified pixels. * @private */ -ol.source.Raster.prototype.transformPixels_ = function(pixels) { +ol.source.Raster.prototype.runOperations_ = function(pixels) { for (var i = 0, ii = this.operations_.length; i < ii; ++i) { pixels = this.operations_[i](pixels); } @@ -314,3 +326,49 @@ ol.source.Raster.createTileRenderer_ = function(source) { var layer = new ol.layer.Tile({source: source}); return new ol.renderer.canvas.TileLayer(layer); }; + + + +/** + * @classdesc + * Events emitted by {@link ol.source.Raster} instances are instances of this + * type. + * + * @constructor + * @extends {goog.events.Event} + * @implements {oli.source.RasterEvent} + * @param {string} type Type. + * @param {number} resolution Map units per pixel. + */ +ol.source.RasterEvent = function(type, resolution) { + goog.base(this, type); + + /** + * Map units per pixel. + * @type {number} + * @api + */ + this.resolution = resolution; + +}; +goog.inherits(ol.source.RasterEvent, goog.events.Event); + + +/** + * @enum {string} + */ +ol.source.RasterEventType = { + /** + * Triggered before operations are run. + * @event ol.source.RasterEvent#beforeoperations + * @api + */ + BEFOREOPERATIONS: 'beforeoperations', + + /** + * Triggered after operations are run. + * @event ol.source.RasterEvent#afteroperations + * @api + */ + AFTEROPERATIONS: 'afteroperations' +}; From 6740ca9ee81e4347665d982c0523dc124917349f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 7 Feb 2015 11:52:03 -0700 Subject: [PATCH 04/28] More interactive example --- examples/raster.css | 31 +++++++++ examples/raster.html | 6 +- examples/raster.js | 157 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 examples/raster.css diff --git a/examples/raster.css b/examples/raster.css new file mode 100644 index 0000000000..e3bc43721c --- /dev/null +++ b/examples/raster.css @@ -0,0 +1,31 @@ +.rel { + position: relative +} + +#plot { + pointer-events: none; + position: absolute; + bottom: 10px; + left: 10px; +} + +.bar { + pointer-events: auto; + fill: #AFAFB9; +} + +.bar.selected { + fill: green; +} + +.tip { + position: absolute; + background: black; + color: white; + padding: 6px; + font-size: 12px; + border-radius: 4px; + margin-bottom: 10px; + display: none; + opacity: 0; +} diff --git a/examples/raster.html b/examples/raster.html index 810be90a76..b16689882f 100644 --- a/examples/raster.html +++ b/examples/raster.html @@ -19,9 +19,13 @@ docs: > threshold are green and those below are transparent).

tags: "raster, pixel" +resources: + - http://d3js.org/d3.v3.min.js + - raster.css ---
-
+
+
diff --git a/examples/raster.js b/examples/raster.js index 873fd25e25..39d8d64a3b 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -1,3 +1,5 @@ +// NOCOMPILE +// this example uses d3 for which we don't have an externs file. goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.layer.Image'); @@ -10,8 +12,16 @@ function tgi(pixels) { var r = pixel[0] / 255; var g = pixel[1] / 255; var b = pixel[2] / 255; - var index = (120 * (r - b) - (190 * (r - g))) / 2; - pixel[0] = index; + var value = (120 * (r - b) - (190 * (r - g))) / 2; + pixel[0] = value; + return pixels; +} + +var counts = new Counts(0, 25); + +function summarize(pixels) { + var value = pixels[0][0]; + counts.increment(value); return pixels; } @@ -19,8 +29,8 @@ var threshold = 10; function color(pixels) { var pixel = pixels[0]; - var index = pixel[0]; - if (index > threshold) { + var value = pixel[0]; + if (value > threshold) { pixel[0] = 0; pixel[1] = 255; pixel[2] = 0; @@ -36,19 +46,28 @@ var bing = new ol.source.BingMaps({ imagerySet: 'Aerial' }); -var imagery = new ol.layer.Tile({ - source: bing +var raster = new ol.source.Raster({ + sources: [bing], + operations: [tgi, summarize, color] }); -var greenness = new ol.layer.Image({ - source: new ol.source.Raster({ - sources: [bing], - operations: [tgi, color] - }) +raster.on('beforeoperations', function() { + counts.clear(); +}); + +raster.on('afteroperations', function(event) { + schedulePlot(event.resolution); }); var map = new ol.Map({ - layers: [imagery, greenness], + layers: [ + new ol.layer.Tile({ + source: bing + }), + new ol.layer.Image({ + source: raster + }) + ], target: 'map', view: new ol.View({ center: [-9651695.964309687, 4937351.719788862], @@ -56,3 +75,117 @@ var map = new ol.Map({ minZoom: 12 }) }); + + + +/** + * Maintain counts of values between a min and max. + * @param {number} min The minimum value (inclusive). + * @param {[type]} max The maximum value (exclusive). + * @constructor + */ +function Counts(min, max) { + this.min = min; + this.max = max; + this.values = new Array(max - min); +} + + +/** + * Clear all counts. + */ +Counts.prototype.clear = function() { + for (var i = 0, ii = this.values.length; i < ii; ++i) { + this.values[i] = 0; + } +}; + + +/** + * Increment the count for a value. + * @param {number} value The value. + */ +Counts.prototype.increment = function(value) { + value = Math.floor(value); + if (value >= this.min && value < this.max) { + this.values[value - this.min] += 1; + } +}; + +var timer = null; +function schedulePlot(resolution) { + if (timer) { + clearTimeout(timer); + timer = null; + } + timer = setTimeout(plot.bind(null, resolution), 1000 / 60); +} + +var barWidth = 15; +var plotHeight = 150; +var chart = d3.select('#plot').append('svg') + .attr('width', barWidth * counts.values.length) + .attr('height', plotHeight); + +var chartRect = chart[0][0].getBoundingClientRect(); + +var tip = d3.select(document.body).append('div') + .attr('class', 'tip'); + +function plot(resolution) { + var yScale = d3.scale.linear() + .domain([0, d3.max(counts.values)]) + .range([0, plotHeight]); + + var bar = chart.selectAll('rect').data(counts.values); + + bar.enter().append('rect'); + + bar.attr('class', function(value, index) { + return 'bar' + (index - counts.min >= threshold ? ' selected' : ''); + }) + .attr('width', barWidth - 2); + + bar.transition() + .attr('transform', function(value, index) { + return 'translate(' + (index * barWidth) + ', ' + + (plotHeight - yScale(value)) + ')'; + }) + .attr('height', yScale); + + bar.on('mousemove', function() { + var old = threshold; + threshold = counts.min + + Math.floor((d3.event.pageX - chartRect.left) / barWidth); + if (old !== threshold) { + map.render(); + } + }); + + bar.on('mouseover', function() { + var index = Math.floor((d3.event.pageX - chartRect.left) / barWidth); + var area = 0; + for (var i = counts.values.length - 1; i >= index; --i) { + area += resolution * resolution * counts.values[i]; + } + tip.html(message(index + counts.min, area)); + tip.style('display', 'block'); + tip.transition().style({ + left: (chartRect.left + (index * barWidth) + (barWidth / 2)) + 'px', + top: (d3.event.y - 60) + 'px', + opacity: 1 + }); + }); + + bar.on('mouseout', function() { + tip.transition().style('opacity', 0).each('end', function() { + tip.style('display', 'none'); + }); + }); + +} + +function message(value, area) { + var acres = (area / 4046.86).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return acres + ' acres at
' + value + ' TGI or above'; +} From de107c5502af196335fad863bb559caca0b8e404 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 7 Feb 2015 15:28:50 -0700 Subject: [PATCH 05/28] Frame and canvas have equal size --- src/ol/source/rastersource.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 861fe86605..f9d6cd7fdb 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -117,8 +117,8 @@ ol.source.Raster.prototype.updateFrameState_ = var frameState = this.frameState_; var center = ol.extent.getCenter(extent); - var width = ol.extent.getWidth(extent) / resolution; - var height = ol.extent.getHeight(extent) / resolution; + var width = Math.round(ol.extent.getWidth(extent) / resolution); + var height = Math.round(ol.extent.getHeight(extent) / resolution); frameState.extent = extent; frameState.focus = ol.extent.getCenter(extent); @@ -142,8 +142,8 @@ ol.source.Raster.prototype.getImage = var context = this.canvasContext_; var canvas = context.canvas; - var width = ol.extent.getWidth(extent) / resolution; - var height = ol.extent.getHeight(extent) / resolution; + var width = Math.round(ol.extent.getWidth(extent) / resolution); + var height = Math.round(ol.extent.getHeight(extent) / resolution); if (width !== canvas.width || height !== canvas.height) { @@ -178,8 +178,7 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { 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]); + this.renderers_[i], frameState, frameState.layerStatesArray[i]); } var targetImageData = context.getImageData(0, 0, canvas.width, canvas.height); @@ -234,15 +233,12 @@ ol.source.Raster.prototype.runOperations_ = function(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) { +ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) { renderer.prepareFrame(frameState, layerState); var canvas = renderer.getImage(); var imageTransform = renderer.getImageTransform(); @@ -250,7 +246,7 @@ ol.source.Raster.getImageData_ = var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); return canvas.getContext('2d').getImageData( Math.round(-dx), Math.round(-dy), - width, height); + frameState.size[0], frameState.size[1]); }; From c6dedbc40b1778dd84949032107501461babc95d Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 23 Feb 2015 23:14:04 -0800 Subject: [PATCH 06/28] Use the first pixel for rendering, allow setting operations --- src/ol/source/rastersource.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index f9d6cd7fdb..425817d268 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -104,6 +104,15 @@ ol.source.Raster = function(options) { goog.inherits(ol.source.Raster, ol.source.Image); +/** + * Reset the operations. + * @param {Array.} operations New operations. + */ +ol.source.Raster.prototype.setOperations = function(operations) { + this.operations_ = operations; +}; + + /** * Update the stored frame state. * @param {ol.Extent} extent The view extent (in map units). @@ -199,8 +208,7 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { pixel[2] = source[j + 2]; pixel[3] = source[j + 3]; } - this.runOperations_(pixels); - pixel = pixels[0]; + pixel = this.runOperations_(pixels)[0]; target[j] = pixel[0]; target[j + 1] = pixel[1]; target[j + 2] = pixel[2]; From d17d470d48dab2a95edbffe1a800d73a7fe01c2c Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 24 Feb 2015 10:22:47 -0800 Subject: [PATCH 07/28] Fire change when updating operations --- src/ol/source/rastersource.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 425817d268..31d09e712d 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -107,9 +107,11 @@ goog.inherits(ol.source.Raster, ol.source.Image); /** * Reset the operations. * @param {Array.} operations New operations. + * @api */ ol.source.Raster.prototype.setOperations = function(operations) { this.operations_ = operations; + this.changed(); }; From 23e2fcefef2cbe5fe9d880ef9a595c87a08111b3 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 8 May 2015 13:09:01 -0600 Subject: [PATCH 08/28] Only render if sources are ready --- examples/raster.js | 7 ++++--- src/ol/source/rastersource.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/raster.js b/examples/raster.js index 39d8d64a3b..37ea3d5596 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -34,7 +34,7 @@ function color(pixels) { pixel[0] = 0; pixel[1] = 255; pixel[2] = 0; - pixel[3] = 255; + pixel[3] = 128; } else { pixel[3] = 0; } @@ -70,9 +70,10 @@ var map = new ol.Map({ ], target: 'map', view: new ol.View({ - center: [-9651695.964309687, 4937351.719788862], + center: [-9651695, 4937351], zoom: 13, - minZoom: 12 + minZoom: 12, + maxZoom: 19 }) }); diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 31d09e712d..596e67ee1c 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -16,6 +16,7 @@ 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.State'); goog.require('ol.source.Tile'); @@ -172,6 +173,25 @@ ol.source.Raster.prototype.getImage = }; +/** + * Determine if all sources are ready. + * @return {boolean} All sources are ready. + * @private + */ +ol.source.Raster.prototype.allSourcesReady_ = function() { + var ready = true; + var source; + for (var i = 0, ii = this.renderers_.length; i < ii; ++i) { + source = this.renderers_[i].getLayer().getSource(); + if (source.getState() !== ol.source.State.READY) { + ready = false; + break; + } + } + return ready; +}; + + /** * Compose the frame. This renders data from all sources, runs pixel-wise * operations, and renders the result to the stored canvas context. @@ -179,6 +199,9 @@ ol.source.Raster.prototype.getImage = * @private */ ol.source.Raster.prototype.composeFrame_ = function(frameState) { + if (!this.allSourcesReady_()) { + return; + } var len = this.renderers_.length; var imageDatas = new Array(len); var pixels = new Array(len); From a721ce03c90a86aa92b8cc028a752b84543666b5 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 25 Jun 2015 16:20:13 -0600 Subject: [PATCH 09/28] Support for image or pixel operations --- externs/olx.js | 16 +++++++-- src/ol/raster/operation.js | 32 ++++++++++++----- src/ol/source/rastersource.js | 67 +++++++++++++++++++++++++---------- 3 files changed, 86 insertions(+), 29 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index da432b6c26..467d2eb6ea 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4500,7 +4500,8 @@ olx.source.ImageVectorOptions.prototype.style; /** * @typedef {{sources: Array., - * operations: (Array.|undefined)}} + * operations: (Array.|undefined), + * operationType: (ol.raster.OperationType|undefined)}} * @api */ olx.source.RasterOptions; @@ -4515,7 +4516,7 @@ olx.source.RasterOptions.prototype.sources; /** - * Pixel operations. Operations will be called with pixels from input sources + * Pixel operations. Operations will be called with data from input sources * and the final output will be assigned to the raster source. * @type {Array.|undefined} * @api @@ -4523,6 +4524,17 @@ olx.source.RasterOptions.prototype.sources; olx.source.RasterOptions.prototype.operations; +/** + * Operation type. Supported values are `'pixel'` and `'image'`. By default, + * `'pixel'` operations are assumed, and operations will be called with an + * array of pixels from input sources. If set to `'image'`, operations will + * be called with an array of ImageData objects from input sources. + * @type {ol.raster.OperationType|undefined} + * @api + */ +olx.source.RasterOptions.prototype.operationType; + + /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js index b0430fba6b..b88d99c8af 100644 --- a/src/ol/raster/operation.js +++ b/src/ol/raster/operation.js @@ -1,23 +1,39 @@ goog.provide('ol.raster.IdentityOp'); goog.provide('ol.raster.Operation'); +goog.provide('ol.raster.OperationType'); /** - * 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. + * Raster operation type. Supported values are `'pixel'` and `'image'`. + * @enum {string} + * @api + */ +ol.raster.OperationType = { + PIXEL: 'pixel', + IMAGE: 'image' +}; + + +/** + * A function that takes an array of input data, performs some operation, and + * returns an array of ouput data. For `'pixel'` type operations, functions + * will be called with an array of {@link ol.raster.Pixel} data and should + * return an array of the same. For `'image'` type operations, functions will + * be called with an array of {@link ImageData + * https://developer.mozilla.org/en-US/docs/Web/API/ImageData} and should return + * an array of the same. * - * @typedef {function(Array.): Array.} + * @typedef {function((Array.|Array.)): + * (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. + * The identity operation. Returns the supplied input data as output. + * @param {(Array.|Array.)} inputs Input data. + * @return {(Array.|Array.)} The output data. */ ol.raster.IdentityOp = function(inputs) { return inputs; diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 596e67ee1c..e51e013743 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -13,6 +13,7 @@ goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Tile'); goog.require('ol.raster.IdentityOp'); +goog.require('ol.raster.OperationType'); goog.require('ol.renderer.canvas.ImageLayer'); goog.require('ol.renderer.canvas.TileLayer'); goog.require('ol.source.Image'); @@ -41,6 +42,13 @@ ol.source.Raster = function(options) { this.operations_ = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; + /** + * @private + * @type {ol.raster.OperationType} + */ + this.operationType_ = goog.isDef(options.operationType) ? + options.operationType : ol.raster.OperationType.PIXEL; + /** * @private * @type {Array.} @@ -215,29 +223,36 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { this.renderers_[i], frameState, frameState.layerStatesArray[i]); } - var targetImageData = context.getImageData(0, 0, canvas.width, canvas.height); - var target = targetImageData.data; - - var resolution = frameState.viewState.resolution / frameState.pixelRatio; this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.BEFOREOPERATIONS, resolution)); - 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]; + var targetImageData = null; + if (this.operationType_ === ol.raster.OperationType.PIXEL) { + 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]; + } + pixel = this.runPixelOperations_(pixels)[0]; + target[j] = pixel[0]; + target[j + 1] = pixel[1]; + target[j + 2] = pixel[2]; + target[j + 3] = pixel[3]; } - pixel = this.runOperations_(pixels)[0]; - target[j] = pixel[0]; - target[j + 1] = pixel[1]; - target[j + 2] = pixel[2]; - target[j + 3] = pixel[3]; + } else if (this.operationType_ === ol.raster.OperationType.IMAGE) { + targetImageData = this.runImageOperations_(imageDatas)[0]; + } else { + goog.asserts.fail('unsupported operation type: ' + this.operationType_); } this.dispatchEvent(new ol.source.RasterEvent( @@ -255,7 +270,7 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { * @return {Array.} The modified pixels. * @private */ -ol.source.Raster.prototype.runOperations_ = function(pixels) { +ol.source.Raster.prototype.runPixelOperations_ = function(pixels) { for (var i = 0, ii = this.operations_.length; i < ii; ++i) { pixels = this.operations_[i](pixels); } @@ -263,6 +278,20 @@ ol.source.Raster.prototype.runOperations_ = function(pixels) { }; +/** + * Run image operations. + * @param {Array.} imageDatas The input image data. + * @return {Array.} The output image data. + * @private + */ +ol.source.Raster.prototype.runImageOperations_ = function(imageDatas) { + for (var i = 0, ii = this.operations_.length; i < ii; ++i) { + imageDatas = this.operations_[i](imageDatas); + } + return imageDatas; +}; + + /** * Get image data from a renderer. * @param {ol.renderer.canvas.Layer} renderer Layer renderer. From 52677766275e119fffef20011dea99097b370e86 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 25 Jun 2015 16:28:17 -0600 Subject: [PATCH 10/28] Provide extent and resolution in raster events --- externs/oli.js | 6 ++++++ src/ol/source/rastersource.js | 20 +++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/externs/oli.js b/externs/oli.js index d8d4b86cb1..56b8afb229 100644 --- a/externs/oli.js +++ b/externs/oli.js @@ -272,6 +272,12 @@ oli.source.ImageEvent.prototype.image; oli.source.RasterEvent = function() {}; +/** + * @type {ol.Extent} + */ +oli.source.RasterEvent.prototype.extent; + + /** * @type {number} */ diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index e51e013743..cc51c561b4 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -223,9 +223,8 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { this.renderers_[i], frameState, frameState.layerStatesArray[i]); } - var resolution = frameState.viewState.resolution / frameState.pixelRatio; this.dispatchEvent(new ol.source.RasterEvent( - ol.source.RasterEventType.BEFOREOPERATIONS, resolution)); + ol.source.RasterEventType.BEFOREOPERATIONS, frameState)); var targetImageData = null; if (this.operationType_ === ol.raster.OperationType.PIXEL) { @@ -256,7 +255,7 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { } this.dispatchEvent(new ol.source.RasterEvent( - ol.source.RasterEventType.AFTEROPERATIONS, resolution)); + ol.source.RasterEventType.AFTEROPERATIONS, frameState)); context.putImageData(targetImageData, 0, 0); @@ -396,17 +395,24 @@ ol.source.Raster.createTileRenderer_ = function(source) { * @extends {goog.events.Event} * @implements {oli.source.RasterEvent} * @param {string} type Type. - * @param {number} resolution Map units per pixel. + * @param {olx.FrameState} frameState The frame state. */ -ol.source.RasterEvent = function(type, resolution) { +ol.source.RasterEvent = function(type, frameState) { goog.base(this, type); /** - * Map units per pixel. + * The raster extent. + * @type {ol.Extent} + * @api + */ + this.extent = frameState.extent; + + /** + * The pixel resolution (map units per pixel). * @type {number} * @api */ - this.resolution = resolution; + this.resolution = frameState.viewState.resolution / frameState.pixelRatio; }; goog.inherits(ol.source.RasterEvent, goog.events.Event); From 65fee5b7ac821e214a053261b43d4365110f84c6 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 25 Jun 2015 17:32:34 -0600 Subject: [PATCH 11/28] Pass data object to operations --- examples/raster.js | 28 +++++++++++++++------------- externs/oli.js | 6 ++++++ src/ol/raster/operation.js | 7 +++++-- src/ol/source/rastersource.js | 30 +++++++++++++++++++++--------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/examples/raster.js b/examples/raster.js index 37ea3d5596..e62c55b208 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -7,7 +7,7 @@ goog.require('ol.layer.Tile'); goog.require('ol.source.BingMaps'); goog.require('ol.source.Raster'); -function tgi(pixels) { +function tgi(pixels, data) { var pixel = pixels[0]; var r = pixel[0] / 255; var g = pixel[1] / 255; @@ -17,20 +17,17 @@ function tgi(pixels) { return pixels; } -var counts = new Counts(0, 25); -function summarize(pixels) { +function summarize(pixels, data) { var value = pixels[0][0]; - counts.increment(value); + data.counts.increment(value); return pixels; } -var threshold = 10; - -function color(pixels) { +function color(pixels, data) { var pixel = pixels[0]; var value = pixel[0]; - if (value > threshold) { + if (value > data.threshold) { pixel[0] = 0; pixel[1] = 255; pixel[2] = 0; @@ -51,12 +48,17 @@ var raster = new ol.source.Raster({ operations: [tgi, summarize, color] }); -raster.on('beforeoperations', function() { +var counts = new Counts(0, 25); +var threshold = 10; + +raster.on('beforeoperations', function(event) { counts.clear(); + event.data.counts = counts; + event.data.threshold = threshold; }); raster.on('afteroperations', function(event) { - schedulePlot(event.resolution); + schedulePlot(event.resolution, event.data.counts); }); var map = new ol.Map({ @@ -114,12 +116,12 @@ Counts.prototype.increment = function(value) { }; var timer = null; -function schedulePlot(resolution) { +function schedulePlot(resolution, counts) { if (timer) { clearTimeout(timer); timer = null; } - timer = setTimeout(plot.bind(null, resolution), 1000 / 60); + timer = setTimeout(plot.bind(null, resolution, counts), 1000 / 60); } var barWidth = 15; @@ -133,7 +135,7 @@ var chartRect = chart[0][0].getBoundingClientRect(); var tip = d3.select(document.body).append('div') .attr('class', 'tip'); -function plot(resolution) { +function plot(resolution, counts) { var yScale = d3.scale.linear() .domain([0, d3.max(counts.values)]) .range([0, plotHeight]); diff --git a/externs/oli.js b/externs/oli.js index 56b8afb229..7d1631024d 100644 --- a/externs/oli.js +++ b/externs/oli.js @@ -284,6 +284,12 @@ oli.source.RasterEvent.prototype.extent; oli.source.RasterEvent.prototype.resolution; +/** + * @type {Object} + */ +oli.source.RasterEvent.prototype.data; + + /** * @interface */ diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js index b88d99c8af..340bc37ff7 100644 --- a/src/ol/raster/operation.js +++ b/src/ol/raster/operation.js @@ -21,9 +21,12 @@ ol.raster.OperationType = { * return an array of the same. For `'image'` type operations, functions will * be called with an array of {@link ImageData * https://developer.mozilla.org/en-US/docs/Web/API/ImageData} and should return - * an array of the same. + * an array of the same. The operations are called with a second "data" + * argument, which can be used for storage. The data object is accessible + * from raster events, where it can be initialized in "beforeoperations" and + * accessed again in "afteroperations." * - * @typedef {function((Array.|Array.)): + * @typedef {function((Array.|Array.), Object): * (Array.|Array.)} * @api */ diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index cc51c561b4..09bc5f28f3 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -223,8 +223,9 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { this.renderers_[i], frameState, frameState.layerStatesArray[i]); } + var data = {}; this.dispatchEvent(new ol.source.RasterEvent( - ol.source.RasterEventType.BEFOREOPERATIONS, frameState)); + ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data)); var targetImageData = null; if (this.operationType_ === ol.raster.OperationType.PIXEL) { @@ -242,20 +243,20 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { pixel[2] = source[j + 2]; pixel[3] = source[j + 3]; } - pixel = this.runPixelOperations_(pixels)[0]; + pixel = this.runPixelOperations_(pixels, data)[0]; target[j] = pixel[0]; target[j + 1] = pixel[1]; target[j + 2] = pixel[2]; target[j + 3] = pixel[3]; } } else if (this.operationType_ === ol.raster.OperationType.IMAGE) { - targetImageData = this.runImageOperations_(imageDatas)[0]; + targetImageData = this.runImageOperations_(imageDatas, data)[0]; } else { goog.asserts.fail('unsupported operation type: ' + this.operationType_); } this.dispatchEvent(new ol.source.RasterEvent( - ol.source.RasterEventType.AFTEROPERATIONS, frameState)); + ol.source.RasterEventType.AFTEROPERATIONS, frameState, data)); context.putImageData(targetImageData, 0, 0); @@ -266,12 +267,13 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState) { /** * Run pixel-wise operations to transform pixels. * @param {Array.} pixels The input pixels. + * @param {Object} data User storage. * @return {Array.} The modified pixels. * @private */ -ol.source.Raster.prototype.runPixelOperations_ = function(pixels) { +ol.source.Raster.prototype.runPixelOperations_ = function(pixels, data) { for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - pixels = this.operations_[i](pixels); + pixels = this.operations_[i](pixels, data); } return pixels; }; @@ -280,12 +282,13 @@ ol.source.Raster.prototype.runPixelOperations_ = function(pixels) { /** * Run image operations. * @param {Array.} imageDatas The input image data. + * @param {Object} data User storage. * @return {Array.} The output image data. * @private */ -ol.source.Raster.prototype.runImageOperations_ = function(imageDatas) { +ol.source.Raster.prototype.runImageOperations_ = function(imageDatas, data) { for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - imageDatas = this.operations_[i](imageDatas); + imageDatas = this.operations_[i](imageDatas, data); } return imageDatas; }; @@ -396,8 +399,9 @@ ol.source.Raster.createTileRenderer_ = function(source) { * @implements {oli.source.RasterEvent} * @param {string} type Type. * @param {olx.FrameState} frameState The frame state. + * @param {Object} data An object made available to operations. */ -ol.source.RasterEvent = function(type, frameState) { +ol.source.RasterEvent = function(type, frameState, data) { goog.base(this, type); /** @@ -414,6 +418,14 @@ ol.source.RasterEvent = function(type, frameState) { */ this.resolution = frameState.viewState.resolution / frameState.pixelRatio; + /** + * An object made available to all operations. This can be used by operations + * as a storage object (e.g. for calculating statistics). + * @type {Object} + * @api + */ + this.data = data; + }; goog.inherits(ol.source.RasterEvent, goog.events.Event); From 1d94d71a5bc48f763c9ed1e484d6a83f9cf0663f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 26 Jun 2015 13:40:01 -0600 Subject: [PATCH 12/28] Shaded relief example --- examples/shaded-relief.html | 19 ++++ examples/shaded-relief.js | 183 ++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 examples/shaded-relief.html create mode 100644 examples/shaded-relief.js diff --git a/examples/shaded-relief.html b/examples/shaded-relief.html new file mode 100644 index 0000000000..a7fb5cfc87 --- /dev/null +++ b/examples/shaded-relief.html @@ -0,0 +1,19 @@ +--- +template: example.html +title: Shaded Relief +shortdesc: Calculate shaded relief from elevation data +docs: > + With a `ol.source.Raster`, it is possible to run operations on input data from other sources. +tags: "raster" +--- +
+
+
+
+ + + + +
+
+
diff --git a/examples/shaded-relief.js b/examples/shaded-relief.js new file mode 100644 index 0000000000..98edda4b54 --- /dev/null +++ b/examples/shaded-relief.js @@ -0,0 +1,183 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.Raster'); +goog.require('ol.source.TileWMS'); + + +function read3x3(imageData, callback) { + var size = 3; + var mid = 1; + var width = imageData.width; + var height = imageData.height; + var data = imageData.data; + var kernel = new Array(size * size); + for (var n = 0, nn = kernel.length; n < nn; ++n) { + kernel[n] = [0, 0, 0, 0]; + } + var offsetMin = (1 - size) / 2; + for (var pixelY = 0; pixelY < height; ++j) { + for (var pixelX = 0; pixelX < width; ++i) { + for (var kernelY = 0; kernelY < size; ++kernelY) { + var neighborY = Math.max(pixelY - (kernelY - mix), 0); + for (var kernelX = 0; kernelX < size; ++kernelX) { + var neighborX = Math.max(pixelX - (kernelX - mid), 0); + var kernelIndex = kernelX + kernelY * size; + var dataIndex = 4 * (neighborY * width + neighborX); + kernel[kernelIndex][0] = data[dataIndex]; + kernel[kernelIndex][1] = data[dataIndex + 1]; + kernel[kernelIndex][2] = data[dataIndex + 2]; + kernel[kernelIndex][3] = data[dataIndex + 3]; + } + } + callback(kernel, pixelX, pixelY); + } + } +} + +/** + * The NED dataset is symbolized by a color ramp that maps the following + * elevations to corresponding RGB values. This operation is used to + * invert the mapping - returning elevations in meters for a pixel RGB array. + * + * -20m : 0, 0, 0 + * 400m : 0, 0, 255 + * 820m : 0, 255, 255 + * 1240m : 255, 255, 255 + * + */ +function getElevation(pixel) { + return (420 * (pixel[0] + pixel[1] + pixel[2]) / 255) - 20; +} + +/** + * Generates a shaded relief image given elevation data. Uses a 3x3 + * neighborhood for determining slope and aspect. + * @param {Array.} inputs Array of input images. + * @param {Object} data Data with resolution property. + * @return {Array.} Output images (only the first is rendered). + */ +function shade(inputs, data) { + var elevationImage = inputs[0]; + var width = elevationImage.width; + var height = elevationImage.height; + var elevationData = elevationImage.data; + var shadeData = new Uint8ClampedArray(elevationData.length); + var dx = dy = data.resolution * 2; + var maxX = width - 1; + var maxY = height - 1; + var pixel = [0, 0, 0, 0]; + var offset, z0, z1, dzdx, dzdy, slope, aspect, scaled; + for (var pixelY = 0; pixelY <= maxY; ++pixelY) { + var y0 = pixelY === 0 ? 0 : pixelY - 1; + var y1 = pixelY === maxY ? maxY : pixelY + 1; + for (var pixelX = 0; pixelX <= maxX; ++pixelX) { + var x0 = pixelX === 0 ? 0 : pixelX - 1; + var x1 = pixelX === maxX ? maxX : pixelX + 1; + + // determine x0, pixelY elevation + offset = (pixelY * width + x0) * 4; + pixel[0] = elevationData[offset]; + pixel[1] = elevationData[offset + 1]; + pixel[2] = elevationData[offset + 2]; + pixel[3] = elevationData[offset + 3]; + z0 = getElevation(pixel); + + // determine x1, pixelY elevation + offset = (pixelY * width + x1) * 4; + pixel[0] = elevationData[offset]; + pixel[1] = elevationData[offset + 1]; + pixel[2] = elevationData[offset + 2]; + pixel[3] = elevationData[offset + 3]; + z1 = getElevation(pixel); + + dzdx = (z1 - z0) / dx; + + // determine pixelX, y0 elevation + offset = (y0 * width + pixelX) * 4; + pixel[0] = elevationData[offset]; + pixel[1] = elevationData[offset + 1]; + pixel[2] = elevationData[offset + 2]; + pixel[3] = elevationData[offset + 3]; + z0 = getElevation(pixel); + + // determine pixelX, y1 elevation + offset = (y1 * width + pixelX) * 4; + pixel[0] = elevationData[offset]; + pixel[1] = elevationData[offset + 1]; + pixel[2] = elevationData[offset + 2]; + pixel[3] = elevationData[offset + 3]; + z1 = getElevation(pixel); + + dzdy = (z1 - z0) / dy; + + slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy)); + aspect = Math.atan2(dzdy, -dzdx); + if (aspect < 0) { + aspect = (Math.PI / 2) - aspect; + } else if (aspect > Math.PI / 2) { + aspect = (2 * Math.PI) - aspect + (Math.PI / 2); + } else { + aspect = Math.PI / 2 - aspect; + } + + cosIncidence = Math.sin(data.sunEl) * Math.cos(slope) + + Math.cos(data.sunEl) * Math.sin(slope) * Math.cos(data.sunAz - aspect); + + + scaled = 255 * cosIncidence; + + offset = (pixelY * width + pixelX) * 4; + shadeData[offset] = scaled; + shadeData[offset + 1] = scaled; + shadeData[offset + 2] = scaled; + shadeData[offset + 3] = elevationData[offset + 3]; + } + } + + return [new ImageData(shadeData, width, height)]; +} + +var elevation = new ol.source.TileWMS({ + url: 'http://demo.opengeo.org/geoserver/wms', + params: {'LAYERS': 'usgs:ned', 'TILED': true, 'FORMAT': 'image/png'}, + crossOrigin: 'anonymous', + serverType: 'geoserver' +}); + +var raster = new ol.source.Raster({ + sources: [elevation], + operationType: 'image', + operations: [shade] +}); + +var sunElevationInput = document.getElementById('sun-el'); +var sunAzimuthInput = document.getElementById('sun-az'); + +sunElevationInput.addEventListener('input', function() { + raster.changed(); +}); + +sunAzimuthInput.addEventListener('input', function() { + raster.changed(); +}); + +raster.on('beforeoperations', function(event) { + // the event.data object will be passed to operations + event.data.resolution = event.resolution; + event.data.sunEl = Math.PI * sunElevationInput.value / 180; + event.data.sunAz = Math.PI * sunAzimuthInput.value / 180; +}); + +var map = new ol.Map({ + target: 'map', + layers: [ + new ol.layer.Image({ + source: raster + }) + ], + view: new ol.View({ + center: [-8610263, 4747090], + zoom: 10 + }) +}); From 6da6cef7600f21b63517d3cb9e11aea15c567ec4 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 26 Jun 2015 15:44:39 -0600 Subject: [PATCH 13/28] Improved shaded relief example --- examples/shaded-relief.html | 23 +++++- examples/shaded-relief.js | 141 ++++++++++++++---------------------- 2 files changed, 75 insertions(+), 89 deletions(-) diff --git a/examples/shaded-relief.html b/examples/shaded-relief.html index a7fb5cfc87..ffe416692c 100644 --- a/examples/shaded-relief.html +++ b/examples/shaded-relief.html @@ -3,8 +3,27 @@ template: example.html title: Shaded Relief shortdesc: Calculate shaded relief from elevation data docs: > - With a `ol.source.Raster`, it is possible to run operations on input data from other sources. -tags: "raster" +

+ This example uses a ol.source.Raster to generate data + based on another source. The raster source accepts any number of + input sources (tile or image based) and runs a pipeline of + operations on the input data. The return from the final + operation is used as the data for the output source. +

+

+ In this case, a single tiled source of elevation data is used as input. + The shaded relief is calculated in a single "image" operation. By setting + operationType: 'image' on the raster source, operations are + called with an ImageData object for each of the input sources. + Operations are also called with a general purpose data object. + In this example, the sun elevation and azimuth data from the inputs above + are assigned to this data object and accessed in the shading + operation. The shading operation returns an array of ImageData + objects. When the raster source is used by an image layer, the first + ImageData object returned by the last operation in the pipeline + is used for rendering. +

+tags: "raster, shaded relief" ---
diff --git a/examples/shaded-relief.js b/examples/shaded-relief.js index 98edda4b54..ff33184fc0 100644 --- a/examples/shaded-relief.js +++ b/examples/shaded-relief.js @@ -1,55 +1,11 @@ goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.layer.Tile'); +goog.require('ol.source.TileJSON'); goog.require('ol.source.Raster'); -goog.require('ol.source.TileWMS'); +goog.require('ol.source.XYZ'); -function read3x3(imageData, callback) { - var size = 3; - var mid = 1; - var width = imageData.width; - var height = imageData.height; - var data = imageData.data; - var kernel = new Array(size * size); - for (var n = 0, nn = kernel.length; n < nn; ++n) { - kernel[n] = [0, 0, 0, 0]; - } - var offsetMin = (1 - size) / 2; - for (var pixelY = 0; pixelY < height; ++j) { - for (var pixelX = 0; pixelX < width; ++i) { - for (var kernelY = 0; kernelY < size; ++kernelY) { - var neighborY = Math.max(pixelY - (kernelY - mix), 0); - for (var kernelX = 0; kernelX < size; ++kernelX) { - var neighborX = Math.max(pixelX - (kernelX - mid), 0); - var kernelIndex = kernelX + kernelY * size; - var dataIndex = 4 * (neighborY * width + neighborX); - kernel[kernelIndex][0] = data[dataIndex]; - kernel[kernelIndex][1] = data[dataIndex + 1]; - kernel[kernelIndex][2] = data[dataIndex + 2]; - kernel[kernelIndex][3] = data[dataIndex + 3]; - } - } - callback(kernel, pixelX, pixelY); - } - } -} - -/** - * The NED dataset is symbolized by a color ramp that maps the following - * elevations to corresponding RGB values. This operation is used to - * invert the mapping - returning elevations in meters for a pixel RGB array. - * - * -20m : 0, 0, 0 - * 400m : 0, 0, 255 - * 820m : 0, 255, 255 - * 1240m : 255, 255, 255 - * - */ -function getElevation(pixel) { - return (420 * (pixel[0] + pixel[1] + pixel[2]) / 255) - 20; -} - /** * Generates a shaded relief image given elevation data. Uses a 3x3 * neighborhood for determining slope and aspect. @@ -67,67 +23,71 @@ function shade(inputs, data) { var maxX = width - 1; var maxY = height - 1; var pixel = [0, 0, 0, 0]; - var offset, z0, z1, dzdx, dzdy, slope, aspect, scaled; - for (var pixelY = 0; pixelY <= maxY; ++pixelY) { - var y0 = pixelY === 0 ? 0 : pixelY - 1; - var y1 = pixelY === maxY ? maxY : pixelY + 1; - for (var pixelX = 0; pixelX <= maxX; ++pixelX) { - var x0 = pixelX === 0 ? 0 : pixelX - 1; - var x1 = pixelX === maxX ? maxX : pixelX + 1; + var twoPi = 2 * Math.PI; + var halfPi = Math.PI / 2; + var cosSunEl = Math.cos(data.sunEl); + var sinSunEl = Math.sin(data.sunEl); + var pixelX, pixelY, x0, x1, y0, y1, offset, + z0, z1, dzdx, dzdy, slope, aspect, cosIncidence, scaled; + for (pixelY = 0; pixelY <= maxY; ++pixelY) { + y0 = pixelY === 0 ? 0 : pixelY - 1; + y1 = pixelY === maxY ? maxY : pixelY + 1; + for (pixelX = 0; pixelX <= maxX; ++pixelX) { + x0 = pixelX === 0 ? 0 : pixelX - 1; + x1 = pixelX === maxX ? maxX : pixelX + 1; - // determine x0, pixelY elevation + // determine elevation for (x0, pixelY) offset = (pixelY * width + x0) * 4; pixel[0] = elevationData[offset]; pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z0 = getElevation(pixel); + z0 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; - // determine x1, pixelY elevation + // determine elevation for (x1, pixelY) offset = (pixelY * width + x1) * 4; pixel[0] = elevationData[offset]; pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z1 = getElevation(pixel); + z1 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; dzdx = (z1 - z0) / dx; - // determine pixelX, y0 elevation + // determine elevation for (pixelX, y0) offset = (y0 * width + pixelX) * 4; pixel[0] = elevationData[offset]; pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z0 = getElevation(pixel); + z0 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; - // determine pixelX, y1 elevation + // determine elevation for (pixelX, y1) offset = (y1 * width + pixelX) * 4; pixel[0] = elevationData[offset]; pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z1 = getElevation(pixel); + z1 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; dzdy = (z1 - z0) / dy; slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy)); + aspect = Math.atan2(dzdy, -dzdx); if (aspect < 0) { - aspect = (Math.PI / 2) - aspect; + aspect = halfPi - aspect; } else if (aspect > Math.PI / 2) { - aspect = (2 * Math.PI) - aspect + (Math.PI / 2); + aspect = twoPi - aspect + halfPi; } else { - aspect = Math.PI / 2 - aspect; + aspect = halfPi - aspect; } - cosIncidence = Math.sin(data.sunEl) * Math.cos(slope) + - Math.cos(data.sunEl) * Math.sin(slope) * Math.cos(data.sunAz - aspect); - - - scaled = 255 * cosIncidence; + cosIncidence = sinSunEl * Math.cos(slope) + + cosSunEl * Math.sin(slope) * Math.cos(data.sunAz - aspect); offset = (pixelY * width + pixelX) * 4; + scaled = 255 * cosIncidence; shadeData[offset] = scaled; shadeData[offset + 1] = scaled; shadeData[offset + 2] = scaled; @@ -138,11 +98,9 @@ function shade(inputs, data) { return [new ImageData(shadeData, width, height)]; } -var elevation = new ol.source.TileWMS({ - url: 'http://demo.opengeo.org/geoserver/wms', - params: {'LAYERS': 'usgs:ned', 'TILED': true, 'FORMAT': 'image/png'}, - crossOrigin: 'anonymous', - serverType: 'geoserver' +var elevation = new ol.source.XYZ({ + url: 'https://{a-d}.tiles.mapbox.com/v3/aj.sf-dem/{z}/{x}/{y}.png', + crossOrigin: 'anonymous' }); var raster = new ol.source.Raster({ @@ -151,6 +109,28 @@ var raster = new ol.source.Raster({ operations: [shade] }); +var map = new ol.Map({ + target: 'map', + layers: [ + new ol.layer.Tile({ + source: new ol.source.TileJSON({ + url: 'http://api.tiles.mapbox.com/v3/tschaub.miapgppd.jsonp' + }) + }), + new ol.layer.Image({ + opacity: 0.3, + source: raster + }) + ], + view: new ol.View({ + extent: [-13675026, 4439648, -13580856, 4580292], + center: [-13606539, 4492849], + minZoom: 10, + maxZoom: 16, + zoom: 12 + }) +}); + var sunElevationInput = document.getElementById('sun-el'); var sunAzimuthInput = document.getElementById('sun-az'); @@ -168,16 +148,3 @@ raster.on('beforeoperations', function(event) { event.data.sunEl = Math.PI * sunElevationInput.value / 180; event.data.sunAz = Math.PI * sunAzimuthInput.value / 180; }); - -var map = new ol.Map({ - target: 'map', - layers: [ - new ol.layer.Image({ - source: raster - }) - ], - view: new ol.View({ - center: [-8610263, 4747090], - zoom: 10 - }) -}); From c50d775330a763ca93e3aefb5adeb4763c459536 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 26 Jun 2015 16:33:58 -0600 Subject: [PATCH 14/28] Vertical exaggeration control --- examples/shaded-relief.css | 4 +++ examples/shaded-relief.html | 20 +++++++++---- examples/shaded-relief.js | 59 +++++++++++++++++++++---------------- 3 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 examples/shaded-relief.css diff --git a/examples/shaded-relief.css b/examples/shaded-relief.css new file mode 100644 index 0000000000..f829965da2 --- /dev/null +++ b/examples/shaded-relief.css @@ -0,0 +1,4 @@ +table.controls td { + text-align: center; + padding: 2px 5px; +} diff --git a/examples/shaded-relief.html b/examples/shaded-relief.html index ffe416692c..bd8fe05cd9 100644 --- a/examples/shaded-relief.html +++ b/examples/shaded-relief.html @@ -28,11 +28,19 @@ tags: "raster, shaded relief"
-
- - - - -
+ + + + + + + + + + + + + +
vertical exaggeration: x
sun elevation: °
sun azimuth: °
diff --git a/examples/shaded-relief.js b/examples/shaded-relief.js index ff33184fc0..4d9456173b 100644 --- a/examples/shaded-relief.js +++ b/examples/shaded-relief.js @@ -1,8 +1,9 @@ goog.require('ol.Map'); goog.require('ol.View'); +goog.require('ol.layer.Image'); goog.require('ol.layer.Tile'); -goog.require('ol.source.TileJSON'); goog.require('ol.source.Raster'); +goog.require('ol.source.TileJSON'); goog.require('ol.source.XYZ'); @@ -10,7 +11,7 @@ goog.require('ol.source.XYZ'); * Generates a shaded relief image given elevation data. Uses a 3x3 * neighborhood for determining slope and aspect. * @param {Array.} inputs Array of input images. - * @param {Object} data Data with resolution property. + * @param {Object} data Data added in the "beforeoperations" event. * @return {Array.} Output images (only the first is rendered). */ function shade(inputs, data) { @@ -19,14 +20,16 @@ function shade(inputs, data) { var height = elevationImage.height; var elevationData = elevationImage.data; var shadeData = new Uint8ClampedArray(elevationData.length); - var dx = dy = data.resolution * 2; + var dp = data.resolution * 2; var maxX = width - 1; var maxY = height - 1; var pixel = [0, 0, 0, 0]; var twoPi = 2 * Math.PI; var halfPi = Math.PI / 2; - var cosSunEl = Math.cos(data.sunEl); - var sinSunEl = Math.sin(data.sunEl); + var sunEl = Math.PI * data.sunEl / 180; + var sunAz = Math.PI * data.sunAz / 180; + var cosSunEl = Math.cos(sunEl); + var sinSunEl = Math.sin(sunEl); var pixelX, pixelY, x0, x1, y0, y1, offset, z0, z1, dzdx, dzdy, slope, aspect, cosIncidence, scaled; for (pixelY = 0; pixelY <= maxY; ++pixelY) { @@ -42,7 +45,7 @@ function shade(inputs, data) { pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z0 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; + z0 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3); // determine elevation for (x1, pixelY) offset = (pixelY * width + x1) * 4; @@ -50,9 +53,9 @@ function shade(inputs, data) { pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z1 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; + z1 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3); - dzdx = (z1 - z0) / dx; + dzdx = (z1 - z0) / dp; // determine elevation for (pixelX, y0) offset = (y0 * width + pixelX) * 4; @@ -60,7 +63,7 @@ function shade(inputs, data) { pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z0 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; + z0 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3); // determine elevation for (pixelX, y1) offset = (y1 * width + pixelX) * 4; @@ -68,9 +71,9 @@ function shade(inputs, data) { pixel[1] = elevationData[offset + 1]; pixel[2] = elevationData[offset + 2]; pixel[3] = elevationData[offset + 3]; - z1 = pixel[0] + pixel[1] * 2 + pixel[2] * 3; + z1 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3); - dzdy = (z1 - z0) / dy; + dzdy = (z1 - z0) / dp; slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy)); @@ -84,7 +87,7 @@ function shade(inputs, data) { } cosIncidence = sinSunEl * Math.cos(slope) + - cosSunEl * Math.sin(slope) * Math.cos(data.sunAz - aspect); + cosSunEl * Math.sin(slope) * Math.cos(sunAz - aspect); offset = (pixelY * width + pixelX) * 4; scaled = 255 * cosIncidence; @@ -124,27 +127,31 @@ var map = new ol.Map({ ], view: new ol.View({ extent: [-13675026, 4439648, -13580856, 4580292], - center: [-13606539, 4492849], + center: [-13615645, 4497969], minZoom: 10, maxZoom: 16, - zoom: 12 + zoom: 13 }) }); -var sunElevationInput = document.getElementById('sun-el'); -var sunAzimuthInput = document.getElementById('sun-az'); - -sunElevationInput.addEventListener('input', function() { - raster.changed(); -}); - -sunAzimuthInput.addEventListener('input', function() { - raster.changed(); +var controlIds = ['vert', 'sunEl', 'sunAz']; +var controls = {}; +controlIds.forEach(function(id) { + var control = document.getElementById(id); + var output = document.getElementById(id + 'Out'); + control.addEventListener('input', function() { + output.innerText = control.value; + raster.changed(); + }); + output.innerText = control.value; + controls[id] = control; }); raster.on('beforeoperations', function(event) { // the event.data object will be passed to operations - event.data.resolution = event.resolution; - event.data.sunEl = Math.PI * sunElevationInput.value / 180; - event.data.sunAz = Math.PI * sunAzimuthInput.value / 180; + var data = event.data; + data.resolution = event.resolution; + for (var id in controls) { + data[id] = Number(controls[id].value); + } }); From ef90f5a0977eaad927f1f79b17f7be42c07ebf90 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sun, 28 Jun 2015 23:58:00 -0600 Subject: [PATCH 15/28] Run operations in a worker --- package.json | 8 +- src/ol/imagecanvas.js | 73 +++++++++++++- src/ol/source/rastersource.js | 182 ++++++++++++++++++---------------- 3 files changed, 174 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index cf4de3382e..4c7ecc38c0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "async": "0.9.0", + "browserify": "9.0.3", "closure-util": "1.5.0", "fs-extra": "0.12.0", "glob": "5.0.3", @@ -37,11 +38,11 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", + "pixelworks": "^0.8.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", - "wrench": "1.5.8", - "browserify": "9.0.3" + "wrench": "1.5.8" }, "devDependencies": { "clean-css": "2.2.16", @@ -61,6 +62,7 @@ "slimerjs-edge": "0.10.0-pre-2" }, "ext": [ - "rbush" + "rbush", + {"module": "pixelworks", "browserify": true} ] } diff --git a/src/ol/imagecanvas.js b/src/ol/imagecanvas.js index 6cc9b7295b..3d903bab06 100644 --- a/src/ol/imagecanvas.js +++ b/src/ol/imagecanvas.js @@ -1,5 +1,6 @@ goog.provide('ol.ImageCanvas'); +goog.require('goog.asserts'); goog.require('ol.ImageBase'); goog.require('ol.ImageState'); @@ -13,12 +14,23 @@ goog.require('ol.ImageState'); * @param {number} pixelRatio Pixel ratio. * @param {Array.} attributions Attributions. * @param {HTMLCanvasElement} canvas Canvas. + * @param {ol.ImageCanvasLoader=} opt_loader Optional loader function to + * support asynchronous canvas drawing. */ ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions, - canvas) { + canvas, opt_loader) { - goog.base(this, extent, resolution, pixelRatio, ol.ImageState.LOADED, - attributions); + /** + * Optional canvas loader function. + * @type {?ol.ImageCanvasLoader} + * @private + */ + this.loader_ = goog.isDef(opt_loader) ? opt_loader : null; + + var state = goog.isDef(opt_loader) ? + ol.ImageState.IDLE : ol.ImageState.LOADED; + + goog.base(this, extent, resolution, pixelRatio, state, attributions); /** * @private @@ -26,13 +38,68 @@ ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions, */ this.canvas_ = canvas; + /** + * @private + * @type {Error} + */ + this.error_ = null; + }; goog.inherits(ol.ImageCanvas, ol.ImageBase); +/** + * Get any error associated with asynchronous rendering. + * @return {Error} Any error that occurred during rendering. + */ +ol.ImageCanvas.prototype.getError = function() { + return this.error_; +}; + + +/** + * Handle async drawing complete. + * @param {Error} err Any error during drawing. + * @private + */ +ol.ImageCanvas.prototype.handleLoad_ = function(err) { + if (err) { + this.error_ = err; + this.state = ol.ImageState.ERROR; + } else { + this.state = ol.ImageState.LOADED; + } + this.changed(); +}; + + +/** + * Trigger drawing on canvas. + */ +ol.ImageCanvas.prototype.load = function() { + if (this.state == ol.ImageState.IDLE) { + goog.asserts.assert(!goog.isNull(this.loader_)); + this.state = ol.ImageState.LOADING; + this.changed(); + this.loader_(goog.bind(this.handleLoad_, this)); + } +}; + + /** * @inheritDoc */ ol.ImageCanvas.prototype.getImage = function(opt_context) { return this.canvas_; }; + + +/** + * A function that is called to trigger asynchronous canvas drawing. It is + * called with a "done" callback that should be called when drawing is done. + * If any error occurs during drawing, the "done" callback should be called with + * that error. + * + * @typedef {function(function(Error))} + */ +ol.ImageCanvasLoader; diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 09bc5f28f3..8a8f0cb39f 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -5,10 +5,12 @@ goog.provide('ol.source.RasterEventType'); goog.require('goog.asserts'); goog.require('goog.events.Event'); goog.require('goog.functions'); +goog.require('goog.object'); goog.require('goog.vec.Mat4'); goog.require('ol.ImageCanvas'); goog.require('ol.TileQueue'); goog.require('ol.dom'); +goog.require('ol.ext.pixelworks'); goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Tile'); @@ -35,19 +37,14 @@ goog.require('ol.source.Tile'); */ ol.source.Raster = function(options) { - /** - * @private - * @type {Array.} - */ - this.operations_ = goog.isDef(options.operations) ? + var operations = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; - /** - * @private - * @type {ol.raster.OperationType} - */ - this.operationType_ = goog.isDef(options.operationType) ? - options.operationType : ol.raster.OperationType.PIXEL; + this.worker_ = new ol.ext.pixelworks.Processor({ + operations: operations, + imageOps: options.operationType === ol.raster.OperationType.IMAGE, + queue: 1 + }); /** * @private @@ -75,6 +72,20 @@ ol.source.Raster = function(options) { layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; } + /** + * The most recently rendered state. + * @type {?ol.source.Raster.RenderedState} + * @private + */ + this.renderedState_ = null; + + /** + * The most recently rendered image canvas. + * @type {ol.ImageCanvas} + * @private + */ + this.renderedImageCanvas_ = null; + /** * @private * @type {olx.FrameState} @@ -119,7 +130,7 @@ goog.inherits(ol.source.Raster, ol.source.Image); * @api */ ol.source.Raster.prototype.setOperations = function(operations) { - this.operations_ = operations; + this.worker_.setOperations(operations); this.changed(); }; @@ -134,7 +145,12 @@ ol.source.Raster.prototype.setOperations = function(operations) { */ ol.source.Raster.prototype.updateFrameState_ = function(extent, resolution, projection) { - var frameState = this.frameState_; + + var frameState = /** @type {olx.FrameState} */ ( + goog.object.clone(this.frameState_)); + + frameState.viewState = /** @type {olx.ViewState} */ ( + goog.object.clone(frameState.viewState)); var center = ol.extent.getCenter(extent); var width = Math.round(ol.extent.getWidth(extent) / resolution); @@ -153,12 +169,36 @@ ol.source.Raster.prototype.updateFrameState_ = }; +/** + * Determine if the most recently rendered image canvas is dirty. + * @param {ol.Extent} extent The requested extent. + * @param {number} resolution The requested resolution. + * @return {boolean} The image is dirty. + * @private + */ +ol.source.Raster.prototype.isDirty_ = function(extent, resolution) { + var state = this.renderedState_; + return !state || + this.getRevision() !== state.revision || + resolution !== state.resolution || + !ol.extent.equals(extent, state.extent); +}; + + /** * @inheritDoc */ ol.source.Raster.prototype.getImage = function(extent, resolution, pixelRatio, projection) { + if (!this.allSourcesReady_()) { + return null; + } + + if (!this.isDirty_(extent, resolution)) { + return this.renderedImageCanvas_; + } + var context = this.canvasContext_; var canvas = context.canvas; @@ -172,10 +212,18 @@ ol.source.Raster.prototype.getImage = } var frameState = this.updateFrameState_(extent, resolution, projection); - this.composeFrame_(frameState); - var imageCanvas = new ol.ImageCanvas(extent, resolution, 1, - this.getAttributions(), canvas); + var imageCanvas = new ol.ImageCanvas( + extent, resolution, 1, this.getAttributions(), canvas, + this.composeFrame_.bind(this, frameState)); + + this.renderedImageCanvas_ = imageCanvas; + + this.renderedState_ = { + extent: extent, + resolution: resolution, + revision: this.getRevision() + }; return imageCanvas; }; @@ -204,93 +252,53 @@ ol.source.Raster.prototype.allSourcesReady_ = function() { * 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. + * @param {function(Error)} callback Called when composition is complete. * @private */ -ol.source.Raster.prototype.composeFrame_ = function(frameState) { - if (!this.allSourcesReady_()) { - return; - } +ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { 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], frameState, frameState.layerStatesArray[i]); } + frameState.tileQueue.loadMoreTiles(16, 16); var data = {}; this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data)); - var targetImageData = null; - if (this.operationType_ === ol.raster.OperationType.PIXEL) { - targetImageData = context.getImageData(0, 0, canvas.width, - canvas.height); - var target = targetImageData.data; + this.worker_.process(imageDatas, data, + this.onWorkerComplete_.bind(this, frameState, data, callback)); +}; - 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]; - } - pixel = this.runPixelOperations_(pixels, data)[0]; - target[j] = pixel[0]; - target[j + 1] = pixel[1]; - target[j + 2] = pixel[2]; - target[j + 3] = pixel[3]; - } - } else if (this.operationType_ === ol.raster.OperationType.IMAGE) { - targetImageData = this.runImageOperations_(imageDatas, data)[0]; - } else { - goog.asserts.fail('unsupported operation type: ' + this.operationType_); + +/** + * Called when pixel processing is complete. + * @param {olx.FrameState} frameState The frame state. + * @param {Object} data The user data. + * @param {function(Error)} callback Called when rendering is complete. + * @param {Error} err Any error during processing. + * @param {ImageData} output The output image data. + * @private + */ +ol.source.Raster.prototype.onWorkerComplete_ = + function(frameState, data, callback, err, output) { + if (err) { + callback(err); + return; + } + if (goog.isNull(output)) { + // job aborted + return; } this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.AFTEROPERATIONS, frameState, data)); - context.putImageData(targetImageData, 0, 0); + this.canvasContext_.putImageData(output, 0, 0); - frameState.tileQueue.loadMoreTiles(16, 16); -}; - - -/** - * Run pixel-wise operations to transform pixels. - * @param {Array.} pixels The input pixels. - * @param {Object} data User storage. - * @return {Array.} The modified pixels. - * @private - */ -ol.source.Raster.prototype.runPixelOperations_ = function(pixels, data) { - for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - pixels = this.operations_[i](pixels, data); - } - return pixels; -}; - - -/** - * Run image operations. - * @param {Array.} imageDatas The input image data. - * @param {Object} data User storage. - * @return {Array.} The output image data. - * @private - */ -ol.source.Raster.prototype.runImageOperations_ = function(imageDatas, data) { - for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - imageDatas = this.operations_[i](imageDatas, data); - } - return imageDatas; + callback(null); }; @@ -388,6 +396,14 @@ ol.source.Raster.createTileRenderer_ = function(source) { }; +/** + * @typedef {{revision: number, + * resolution: number, + * extent: ol.Extent}} + */ +ol.source.Raster.RenderedState; + + /** * @classdesc From 9d28549b2ba4732824f24487562c44c48fe39479 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 2 Jul 2015 13:45:09 -0600 Subject: [PATCH 16/28] Pass along potentially modified data --- package.json | 2 +- src/ol/source/rastersource.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4c7ecc38c0..59e9de0dc7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", - "pixelworks": "^0.8.0", + "pixelworks": "^0.9.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 8a8f0cb39f..ccc197c54c 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -269,21 +269,21 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data)); this.worker_.process(imageDatas, data, - this.onWorkerComplete_.bind(this, frameState, data, callback)); + this.onWorkerComplete_.bind(this, frameState, callback)); }; /** * Called when pixel processing is complete. * @param {olx.FrameState} frameState The frame state. - * @param {Object} data The user data. * @param {function(Error)} callback Called when rendering is complete. * @param {Error} err Any error during processing. * @param {ImageData} output The output image data. + * @param {Object} data The user data. * @private */ ol.source.Raster.prototype.onWorkerComplete_ = - function(frameState, data, callback, err, output) { + function(frameState, callback, err, output, data) { if (err) { callback(err); return; From 793b27e9f5339fb16e1fb8b9139d472db61b29a6 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 2 Jul 2015 14:24:36 -0600 Subject: [PATCH 17/28] Allow operations to be updated --- src/ol/source/rastersource.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index ccc197c54c..0bbe181d81 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -37,14 +37,21 @@ goog.require('ol.source.Tile'); */ ol.source.Raster = function(options) { + /** + * @private + * @type {ol.raster.OperationType} + */ + this.operationType_ = goog.isDef(options.operationType) ? + options.operationType : ol.raster.OperationType.PIXEL; + var operations = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; - this.worker_ = new ol.ext.pixelworks.Processor({ - operations: operations, - imageOps: options.operationType === ol.raster.OperationType.IMAGE, - queue: 1 - }); + /** + * @private + * @type {*} + */ + this.worker_ = this.createWorker_(operations); /** * @private @@ -124,13 +131,28 @@ ol.source.Raster = function(options) { goog.inherits(ol.source.Raster, ol.source.Image); +/** + * Create a worker. + * @param {Array.} operations The operations. + * @return {*} The worker. + * @private + */ +ol.source.Raster.prototype.createWorker_ = function(operations) { + return new ol.ext.pixelworks.Processor({ + operations: operations, + imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, + queue: 1 + }); +}; + + /** * Reset the operations. * @param {Array.} operations New operations. * @api */ ol.source.Raster.prototype.setOperations = function(operations) { - this.worker_.setOperations(operations); + this.worker_ = this.createWorker_(operations); this.changed(); }; From d5aa0d9a8eb1d8384837d8a895b8914394999110 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 6 Jul 2015 22:38:22 -0600 Subject: [PATCH 18/28] Update example to work with the latest pixelworks --- examples/raster.js | 79 ++++++++++++++++++---------------------------- package.json | 2 +- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/examples/raster.js b/examples/raster.js index e62c55b208..33630dce2f 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -7,6 +7,9 @@ goog.require('ol.layer.Tile'); goog.require('ol.source.BingMaps'); goog.require('ol.source.Raster'); +var minTgi = 0; +var maxTgi = 25; + function tgi(pixels, data) { var pixel = pixels[0]; var r = pixel[0] / 255; @@ -17,10 +20,12 @@ function tgi(pixels, data) { return pixels; } - function summarize(pixels, data) { - var value = pixels[0][0]; - data.counts.increment(value); + var value = Math.floor(pixels[0][0]); + var counts = data.counts; + if (value >= counts.min && value < counts.max) { + counts.values[value - counts.min] += 1; + } return pixels; } @@ -47,18 +52,28 @@ var raster = new ol.source.Raster({ sources: [bing], operations: [tgi, summarize, color] }); +raster.set('threshold', 10); -var counts = new Counts(0, 25); -var threshold = 10; +function createCounts(min, max) { + var len = max - min; + var values = new Array(len); + for (var i = 0; i < len; ++i) { + values[i] = 0; + } + return { + min: min, + max: max, + values: values + }; +} raster.on('beforeoperations', function(event) { - counts.clear(); - event.data.counts = counts; - event.data.threshold = threshold; + event.data.counts = createCounts(minTgi, maxTgi); + event.data.threshold = raster.get('threshold'); }); raster.on('afteroperations', function(event) { - schedulePlot(event.resolution, event.data.counts); + schedulePlot(event.resolution, event.data.counts, event.data.threshold); }); var map = new ol.Map({ @@ -80,54 +95,19 @@ var map = new ol.Map({ }); - -/** - * Maintain counts of values between a min and max. - * @param {number} min The minimum value (inclusive). - * @param {[type]} max The maximum value (exclusive). - * @constructor - */ -function Counts(min, max) { - this.min = min; - this.max = max; - this.values = new Array(max - min); -} - - -/** - * Clear all counts. - */ -Counts.prototype.clear = function() { - for (var i = 0, ii = this.values.length; i < ii; ++i) { - this.values[i] = 0; - } -}; - - -/** - * Increment the count for a value. - * @param {number} value The value. - */ -Counts.prototype.increment = function(value) { - value = Math.floor(value); - if (value >= this.min && value < this.max) { - this.values[value - this.min] += 1; - } -}; - var timer = null; -function schedulePlot(resolution, counts) { +function schedulePlot(resolution, counts, threshold) { if (timer) { clearTimeout(timer); timer = null; } - timer = setTimeout(plot.bind(null, resolution, counts), 1000 / 60); + timer = setTimeout(plot.bind(null, resolution, counts, threshold), 1000 / 60); } var barWidth = 15; var plotHeight = 150; var chart = d3.select('#plot').append('svg') - .attr('width', barWidth * counts.values.length) + .attr('width', barWidth * (maxTgi - minTgi)) .attr('height', plotHeight); var chartRect = chart[0][0].getBoundingClientRect(); @@ -135,7 +115,7 @@ var chartRect = chart[0][0].getBoundingClientRect(); var tip = d3.select(document.body).append('div') .attr('class', 'tip'); -function plot(resolution, counts) { +function plot(resolution, counts, threshold) { var yScale = d3.scale.linear() .domain([0, d3.max(counts.values)]) .range([0, plotHeight]); @@ -161,7 +141,8 @@ function plot(resolution, counts) { threshold = counts.min + Math.floor((d3.event.pageX - chartRect.left) / barWidth); if (old !== threshold) { - map.render(); + raster.set('threshold', threshold); + raster.changed(); } }); diff --git a/package.json b/package.json index 59e9de0dc7..07f3e6da85 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", - "pixelworks": "^0.9.0", + "pixelworks": "^0.10.1", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", From 643c2e6f219b01dc2fd9c8afc3f00d01595c6ddb Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 9 Jul 2015 16:31:40 -0600 Subject: [PATCH 19/28] Only update canvas if not dirty --- src/ol/source/rastersource.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 0bbe181d81..02ed2c6879 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -318,7 +318,10 @@ ol.source.Raster.prototype.onWorkerComplete_ = this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.AFTEROPERATIONS, frameState, data)); - this.canvasContext_.putImageData(output, 0, 0); + var resolution = frameState.viewState.resolution / frameState.pixelRatio; + if (!this.isDirty_(frameState.extent, resolution)) { + this.canvasContext_.putImageData(output, 0, 0); + } callback(null); }; From f1ff39cc8b67827cdce54099f0e7ea05e0475239 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 10 Jul 2015 14:18:00 -0600 Subject: [PATCH 20/28] Avoid compiling the shaded relief example The compiler doesn't support the use of the ImageData constructor. --- examples/shaded-relief.js | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/shaded-relief.js b/examples/shaded-relief.js index 4d9456173b..175102140b 100644 --- a/examples/shaded-relief.js +++ b/examples/shaded-relief.js @@ -1,3 +1,4 @@ +// NOCOMPILE goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.layer.Image'); From af3c38052ec3a573e6266a92b0131d415bcdce40 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 10 Jul 2015 15:45:01 -0600 Subject: [PATCH 21/28] Avoid examples that cannot be run in Phantom --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 77c6e55ad7..ffe3604869 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ BUILD_HOSTED := build/hosted/$(BRANCH) BUILD_HOSTED_EXAMPLES := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES)) BUILD_HOSTED_EXAMPLES_JS := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES_JS)) -CHECK_EXAMPLE_TIMESTAMPS = $(patsubst examples/%.html,build/timestamps/check-%-timestamp,$(EXAMPLES_HTML)) +UNPHANTOMABLE_EXAMPLES = examples/shaded-relief.html examples/raster.html +CHECK_EXAMPLE_TIMESTAMPS = $(patsubst examples/%.html,build/timestamps/check-%-timestamp,$(filter-out $(UNPHANTOMABLE_EXAMPLES),$(EXAMPLES_HTML))) TASKS_JS := $(shell find tasks -name '*.js') From 0c486c522acb3915307e126a84865e7c49bba0b0 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 21 Jul 2015 17:10:44 -0600 Subject: [PATCH 22/28] Allow UI thread to be used Where workers are not available, or if operations are trivial to run, the main UI thread can be used instead. This also adds tests that run in real browsers. --- externs/olx.js | 22 ++ package.json | 2 +- src/ol/source/rastersource.js | 87 ++++++- test/spec/ol/source/rastersource.test.js | 307 +++++++++++++++++++++++ 4 files changed, 404 insertions(+), 14 deletions(-) create mode 100644 test/spec/ol/source/rastersource.test.js diff --git a/externs/olx.js b/externs/olx.js index 467d2eb6ea..51f8a54c38 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4501,6 +4501,8 @@ olx.source.ImageVectorOptions.prototype.style; /** * @typedef {{sources: Array., * operations: (Array.|undefined), + * lib: (Object|undefined), + * threads: (number|undefined), * operationType: (ol.raster.OperationType|undefined)}} * @api */ @@ -4524,6 +4526,26 @@ olx.source.RasterOptions.prototype.sources; olx.source.RasterOptions.prototype.operations; +/** + * Functions that will be made available to operations run in a worker. + * @type {Object|undefined} + * @api + */ +olx.source.RasterOptions.prototype.lib; + + +/** + * By default, operations will be run in a single worker thread. To avoid using + * workers altogether, set `threads: 0`. For pixel operations, operations can + * be run in multiple worker threads. Note that there some additional overhead + * in transferring data to multiple workers, and that depending on the user's + * system, it may not be possible to parallelize the work. + * @type {number|undefined} + * @api + */ +olx.source.RasterOptions.prototype.threads; + + /** * Operation type. Supported values are `'pixel'` and `'image'`. By default, * `'pixel'` operations are assumed, and operations will be called with an diff --git a/package.json b/package.json index 07f3e6da85..a0dafaeda5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", - "pixelworks": "^0.10.1", + "pixelworks": "^0.11.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 02ed2c6879..46b4d13502 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -3,7 +3,9 @@ goog.provide('ol.source.RasterEvent'); goog.provide('ol.source.RasterEventType'); goog.require('goog.asserts'); +goog.require('goog.events'); goog.require('goog.events.Event'); +goog.require('goog.events.EventType'); goog.require('goog.functions'); goog.require('goog.object'); goog.require('goog.vec.Mat4'); @@ -47,11 +49,17 @@ ol.source.Raster = function(options) { var operations = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; + /** + * @private + * @type {number} + */ + this.threads_ = goog.isDef(options.threads) ? options.threads : 1; + /** * @private * @type {*} */ - this.worker_ = this.createWorker_(operations); + this.worker_ = this.createWorker_(operations, options.lib, this.threads_); /** * @private @@ -59,6 +67,11 @@ ol.source.Raster = function(options) { */ this.renderers_ = ol.source.Raster.createRenderers_(options.sources); + for (var r = 0, rr = this.renderers_.length; r < rr; ++r) { + goog.events.listen(this.renderers_[r], goog.events.EventType.CHANGE, + this.changed, false, this); + } + /** * @private * @type {CanvasRenderingContext2D} @@ -134,14 +147,19 @@ goog.inherits(ol.source.Raster, ol.source.Image); /** * Create a worker. * @param {Array.} operations The operations. + * @param {Object=} opt_lib Optional lib functions. + * @param {number=} opt_threads Number of threads. * @return {*} The worker. * @private */ -ol.source.Raster.prototype.createWorker_ = function(operations) { +ol.source.Raster.prototype.createWorker_ = + function(operations, opt_lib, opt_threads) { return new ol.ext.pixelworks.Processor({ operations: operations, imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, - queue: 1 + queue: 1, + lib: opt_lib, + threads: opt_threads }); }; @@ -149,10 +167,12 @@ ol.source.Raster.prototype.createWorker_ = function(operations) { /** * Reset the operations. * @param {Array.} operations New operations. + * @param {Object=} opt_lib Functions that will be available to operations run + * in a worker. * @api */ -ol.source.Raster.prototype.setOperations = function(operations) { - this.worker_ = this.createWorker_(operations); +ol.source.Raster.prototype.setOperations = function(operations, opt_lib) { + this.worker_ = this.createWorker_(operations, opt_lib, this.threads_); this.changed(); }; @@ -281,10 +301,15 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { var len = this.renderers_.length; var imageDatas = new Array(len); for (var i = 0; i < len; ++i) { - imageDatas[i] = ol.source.Raster.getImageData_( + var imageData = ol.source.Raster.getImageData_( this.renderers_[i], frameState, frameState.layerStatesArray[i]); + if (imageData) { + imageDatas[i] = imageData; + } else { + // image not yet ready + return; + } } - frameState.tileQueue.loadMoreTiles(16, 16); var data = {}; this.dispatchEvent(new ol.source.RasterEvent( @@ -292,6 +317,8 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { this.worker_.process(imageDatas, data, this.onWorkerComplete_.bind(this, frameState, callback)); + + frameState.tileQueue.loadMoreTiles(16, 16); }; @@ -337,16 +364,50 @@ ol.source.Raster.prototype.onWorkerComplete_ = */ ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) { renderer.prepareFrame(frameState, layerState); - var canvas = renderer.getImage(); + // We should be able to call renderer.composeFrame(), but this is inefficient + // for tiled sources (we've already rendered to an intermediate canvas in the + // prepareFrame call and we don't need to render again to the output canvas). + // TODO: make all canvas renderers render to a single canvas + var image = renderer.getImage(); + if (!image) { + return null; + } 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), - frameState.size[0], frameState.size[1]); + var dx = Math.round(goog.vec.Mat4.getElement(imageTransform, 0, 3)); + var dy = Math.round(goog.vec.Mat4.getElement(imageTransform, 1, 3)); + var width = frameState.size[0]; + var height = frameState.size[1]; + if (image instanceof Image) { + if (!ol.source.Raster.context_) { + ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height); + } else { + var canvas = ol.source.Raster.context_.canvas; + if (canvas.width !== width || canvas.height !== height) { + ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height); + } else { + ol.source.Raster.context_.clearRect(0, 0, width, height); + } + } + var dw = Math.round( + image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0)); + var dh = Math.round( + image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1)); + ol.source.Raster.context_.drawImage(image, dx, dy, dw, dh); + return ol.source.Raster.context_.getImageData(0, 0, width, height); + } else { + return image.getContext('2d').getImageData(-dx, -dy, width, height); + } }; +/** + * A reusable canvas context. + * @type {CanvasRenderingContext2D} + * @private + */ +ol.source.Raster.context_ = null; + + /** * Get a list of layer states from a list of renderers. * @param {Array.} renderers Layer renderers. diff --git a/test/spec/ol/source/rastersource.test.js b/test/spec/ol/source/rastersource.test.js new file mode 100644 index 0000000000..69e8f71914 --- /dev/null +++ b/test/spec/ol/source/rastersource.test.js @@ -0,0 +1,307 @@ +goog.provide('ol.test.source.RasterSource'); + +var red = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAA' + + 'BAAEAAAICRAEAOw=='; + +var green = 'data:image/gif;base64,R0lGODlhAQABAPAAAAD/AP///yH5BAAAAAAALAAAA' + + 'AABAAEAAAICRAEAOw=='; + +var blue = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAA/////yH5BAAAAAAALAAAAA' + + 'ABAAEAAAICRAEAOw=='; + +function itNoPhantom() { + if (window.mochaPhantomJS) { + return xit.apply(this, arguments); + } else { + return it.apply(this, arguments); + } +} + +describe('ol.source.Raster', function() { + + var target, map, redSource, greenSource, blueSource; + + beforeEach(function() { + target = document.createElement('div'); + + var style = target.style; + style.position = 'absolute'; + style.left = '-1000px'; + style.top = '-1000px'; + style.width = '2px'; + style.height = '2px'; + document.body.appendChild(target); + + var extent = [-1, -1, 1, 1]; + + redSource = new ol.source.ImageStatic({ + url: red, + imageExtent: extent + }); + + greenSource = new ol.source.ImageStatic({ + url: green, + imageExtent: extent + }); + + blueSource = new ol.source.ImageStatic({ + url: blue, + imageExtent: extent + }); + + raster = new ol.source.Raster({ + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + return inputs; + }] + }); + + map = new ol.Map({ + target: target, + view: new ol.View({ + resolutions: [1], + projection: new ol.proj.Projection({ + code: 'image', + units: 'pixels', + extent: extent + }) + }), + layers: [ + new ol.layer.Image({ + source: raster + }) + ] + }); + }); + + afterEach(function() { + goog.dispose(map); + document.body.removeChild(target); + }); + + describe('constructor', function() { + + it('returns a tile source', function() { + var source = new ol.source.Raster({ + threads: 0, + sources: [new ol.source.Tile({})] + }); + expect(source).to.be.a(ol.source.Source); + expect(source).to.be.a(ol.source.Raster); + }); + + itNoPhantom('defaults to "pixel" operations', function(done) { + + var log = []; + + raster = new ol.source.Raster({ + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + log.push(inputs); + return inputs; + }] + }); + + raster.on('afteroperations', function() { + expect(log.length).to.equal(4); + var inputs = log[0]; + var pixel = inputs[0]; + expect(pixel).to.be.an('array'); + done(); + }); + + map.getLayers().item(0).setSource(raster); + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + itNoPhantom('allows operation type to be set to "image"', function(done) { + + var log = []; + + raster = new ol.source.Raster({ + operationType: ol.raster.OperationType.IMAGE, + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + log.push(inputs); + return inputs; + }] + }); + + raster.on('afteroperations', function() { + expect(log.length).to.equal(1); + var inputs = log[0]; + expect(inputs[0]).to.be.an(ImageData); + done(); + }); + + map.getLayers().item(0).setSource(raster); + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + + describe('#setOperations()', function() { + + itNoPhantom('allows operations to be set', function(done) { + + var count = 0; + raster.setOperations([function(pixels) { + ++count; + var redPixel = pixels[0]; + var greenPixel = pixels[1]; + var bluePixel = pixels[2]; + expect(redPixel).to.eql([255, 0, 0, 255]); + expect(greenPixel).to.eql([0, 255, 0, 255]); + expect(bluePixel).to.eql([0, 0, 255, 255]); + return pixels; + }]); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + raster.on('afteroperations', function(event) { + expect(count).to.equal(4); + done(); + }); + + }); + + itNoPhantom('updates and re-runs the operations', function(done) { + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + var count = 0; + raster.on('afteroperations', function(event) { + ++count; + if (count === 1) { + raster.setOperations([function(inputs) { + return inputs; + }]); + } else { + done(); + } + }); + + }); + + }); + + describe('beforeoperations', function() { + + itNoPhantom('gets called before operations are run', function(done) { + + var count = 0; + raster.setOperations([function(inputs) { + ++count; + return inputs; + }]); + + raster.on('beforeoperations', function(event) { + expect(count).to.equal(0); + expect(!!event).to.be(true); + expect(event.extent).to.be.an('array'); + expect(event.resolution).to.be.a('number'); + expect(event.data).to.be.an('object'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + + itNoPhantom('allows data to be set for the operations', function(done) { + + raster.setOperations([function(inputs, data) { + ++data.count; + return inputs; + }]); + + raster.on('beforeoperations', function(event) { + event.data.count = 0; + }); + + raster.on('afteroperations', function(event) { + expect(event.data.count).to.equal(4); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + + describe('afteroperations', function() { + + itNoPhantom('gets called after operations are run', function(done) { + + var count = 0; + raster.setOperations([function(inputs) { + ++count; + return inputs; + }]); + + raster.on('afteroperations', function(event) { + expect(count).to.equal(4); + expect(!!event).to.be(true); + expect(event.extent).to.be.an('array'); + expect(event.resolution).to.be.a('number'); + expect(event.data).to.be.an('object'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + itNoPhantom('receives data set by the operations', function(done) { + + raster.setOperations([function(inputs, data) { + data.message = 'hello world'; + return inputs; + }]); + + raster.on('afteroperations', function(event) { + expect(event.data.message).to.equal('hello world'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + +}); + +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Image'); +goog.require('ol.proj.Projection'); +goog.require('ol.raster.OperationType'); +goog.require('ol.source.Image'); +goog.require('ol.source.ImageStatic'); +goog.require('ol.source.Raster'); +goog.require('ol.source.Source'); +goog.require('ol.source.Tile'); From 4320b07c5d51e455b95d747e2cb632729f29f6a3 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 1 Aug 2015 16:33:43 -0600 Subject: [PATCH 23/28] Doc corrections --- externs/olx.js | 4 ++-- src/ol/raster/operation.js | 2 +- src/ol/source/rastersource.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 51f8a54c38..c65ba8247b 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4537,8 +4537,8 @@ olx.source.RasterOptions.prototype.lib; /** * By default, operations will be run in a single worker thread. To avoid using * workers altogether, set `threads: 0`. For pixel operations, operations can - * be run in multiple worker threads. Note that there some additional overhead - * in transferring data to multiple workers, and that depending on the user's + * be run in multiple worker threads. Note that there is additional overhead in + * transferring data to multiple workers, and that depending on the user's * system, it may not be possible to parallelize the work. * @type {number|undefined} * @api diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js index 340bc37ff7..9ebd914e50 100644 --- a/src/ol/raster/operation.js +++ b/src/ol/raster/operation.js @@ -24,7 +24,7 @@ ol.raster.OperationType = { * an array of the same. The operations are called with a second "data" * argument, which can be used for storage. The data object is accessible * from raster events, where it can be initialized in "beforeoperations" and - * accessed again in "afteroperations." + * accessed again in "afteroperations". * * @typedef {function((Array.|Array.), Object): * (Array.|Array.)} diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 46b4d13502..c13a4c66d3 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -28,9 +28,9 @@ 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. + * A source that transforms data from any number of input sources using an array + * of {@link ol.raster.Operation} functions to transform input pixel values into + * output pixel values. * * @constructor * @extends {ol.source.Image} From 6f6698dd6af595938f6344d4c7c1c989093e92d9 Mon Sep 17 00:00:00 2001 From: Ian Schneider Date: Thu, 2 Jul 2015 13:30:11 -0600 Subject: [PATCH 24/28] Add a region growing example --- Makefile | 2 +- examples/region-growing.html | 43 +++++++++++ examples/region-growing.js | 134 +++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 examples/region-growing.html create mode 100644 examples/region-growing.js diff --git a/Makefile b/Makefile index ffe3604869..69b5e653a5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ BUILD_HOSTED := build/hosted/$(BRANCH) BUILD_HOSTED_EXAMPLES := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES)) BUILD_HOSTED_EXAMPLES_JS := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES_JS)) -UNPHANTOMABLE_EXAMPLES = examples/shaded-relief.html examples/raster.html +UNPHANTOMABLE_EXAMPLES = examples/shaded-relief.html examples/raster.html examples/region-growing.html CHECK_EXAMPLE_TIMESTAMPS = $(patsubst examples/%.html,build/timestamps/check-%-timestamp,$(filter-out $(UNPHANTOMABLE_EXAMPLES),$(EXAMPLES_HTML))) TASKS_JS := $(shell find tasks -name '*.js') diff --git a/examples/region-growing.html b/examples/region-growing.html new file mode 100644 index 0000000000..adcc843c27 --- /dev/null +++ b/examples/region-growing.html @@ -0,0 +1,43 @@ +--- +template: example.html +title: Region Growing +shortdesc: Grow a region from a seed pixel +docs: > +

Click a region on the map. The computed region will be red.

+

+ This example uses a ol.source.Raster to generate data + based on another source. The raster source accepts any number of + input sources (tile or image based) and runs a pipeline of + operations on the input data. The return from the final + operation is used as the data for the output source. +

+

+ In this case, a single tiled source of imagery data is used as input. + The region is calculated in a single "image" operation using the "seed" + pixel provided by the user clicking on the map. The "threshold" value + determines whether a given contiguous pixel belongs to the "region" - the + difference between a candidate pixel's RGB values and the seed values must + be below the threshold. +

+

+ This example also shows how an additional function can be made available + to the operation. +

+tags: "raster, region growing" +--- +
+
+
+
+
+
+
+ Threshold: +
+
+ +
+
+ +
+
diff --git a/examples/region-growing.js b/examples/region-growing.js new file mode 100644 index 0000000000..266c019751 --- /dev/null +++ b/examples/region-growing.js @@ -0,0 +1,134 @@ +// NOCOMPILE +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Image'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.BingMaps'); +goog.require('ol.source.Raster'); + +function growRegion(inputs, data) { + var image = inputs[0]; + var seed = data.pixel; + var delta = parseInt(data.delta); + if (!seed) { + return [image]; + } + + seed = seed.map(Math.round); + var width = image.width; + var height = image.height; + var inputData = image.data; + var outputData = new Uint8ClampedArray(inputData); + var seedIdx = (seed[1] * width + seed[0]) * 4; + var seedR = inputData[seedIdx]; + var seedG = inputData[seedIdx + 1]; + var seedB = inputData[seedIdx + 2]; + var edge = [seed]; + while (edge.length) { + var newedge = []; + for (var i = 0, ii = edge.length; i < ii; i++) { + // As noted in the Raster source constructor, this function is provided + // using the `lib` option. Other functions will NOT be visible unless + // provided using the `lib` option. + var next = nextEdges(edge[i]); + for (var j = 0, jj = next.length; j < jj; j++) { + var s = next[j][0], t = next[j][1]; + if (s >= 0 && s < width && t >= 0 && t < height) { + var ci = (t * width + s) * 4; + var cr = inputData[ci]; + var cg = inputData[ci + 1]; + var cb = inputData[ci + 2]; + var ca = inputData[ci + 3]; + // if alpha is zero, carry on + if (ca === 0) { + continue; + } + if (Math.abs(seedR - cr) < delta && Math.abs(seedG - cg) < delta && + Math.abs(seedB - cb) < delta) { + outputData[ci] = 255; + outputData[ci + 1] = 0; + outputData[ci + 2] = 0; + outputData[ci + 3] = 255; + newedge.push([s, t]); + } + // mark as visited + inputData[ci + 3] = 0; + } + } + } + edge = newedge; + } + return [new ImageData(outputData, width, height)]; +} + +function next4Edges(edge) { + var x = edge[0], y = edge[1]; + return [ + [x + 1, y], + [x - 1, y], + [x, y + 1], + [x, y - 1] + ]; +} + +var key = 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3'; + +var imagery = new ol.layer.Tile({ + source: new ol.source.BingMaps({key: key, imagerySet: 'Aerial'}) +}); + +var raster = new ol.source.Raster({ + sources: [imagery.getSource()], + operationType: 'image', + operations: [growRegion], + // the contents of `lib` option will be available to the operation(s) run in + // the web worker. + lib: { + nextEdges: next4Edges + } +}); + +var rasterImage = new ol.layer.Image({ + opacity: 0.8, + source: raster +}); + +var map = new ol.Map({ + layers: [imagery, rasterImage], + target: 'map', + view: new ol.View({ + center: ol.proj.fromLonLat([-120, 50]), + zoom: 6 + }) +}); + +var pixel; + +map.getView().on('change', function() { + pixel = null; +}); + +map.on('click', function(ev) { + pixel = map.getPixelFromCoordinate(ev.coordinate); + raster.changed(); +}); + +raster.on('beforeoperations', function(event) { + // the event.data object will be passed to operations + var data = event.data; + data.delta = thresholdControl.value; + data.pixel = pixel; +}); + +var thresholdControl = document.getElementById('threshold'); + +function updateControlValue() { + document.getElementById('threshold-value').innerText = thresholdControl.value; +} +updateControlValue(); + +thresholdControl.addEventListener('input', function() { + updateControlValue(); + raster.changed(); +}); From f2f5cd2630ddeb4b562f234de769dff8d990e62e Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 1 Aug 2015 16:57:35 -0600 Subject: [PATCH 25/28] Make seed coordinate sticky --- examples/region-growing.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/region-growing.js b/examples/region-growing.js index 266c019751..a393de64c3 100644 --- a/examples/region-growing.js +++ b/examples/region-growing.js @@ -103,14 +103,10 @@ var map = new ol.Map({ }) }); -var pixel; +var coordinate; -map.getView().on('change', function() { - pixel = null; -}); - -map.on('click', function(ev) { - pixel = map.getPixelFromCoordinate(ev.coordinate); +map.on('click', function(event) { + coordinate = event.coordinate; raster.changed(); }); @@ -118,7 +114,9 @@ raster.on('beforeoperations', function(event) { // the event.data object will be passed to operations var data = event.data; data.delta = thresholdControl.value; - data.pixel = pixel; + if (coordinate) { + data.pixel = map.getPixelFromCoordinate(coordinate); + } }); var thresholdControl = document.getElementById('threshold'); From 860fdabd7605876d0183f2554fe56d0b9bce7918 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 3 Aug 2015 09:31:17 -0600 Subject: [PATCH 26/28] Simplify raster sources by working with a single operation --- examples/raster.html | 4 +- examples/raster.js | 127 ++++++++++++++--------- examples/region-growing.html | 2 +- examples/region-growing.js | 14 +-- examples/shaded-relief.js | 6 +- externs/olx.js | 10 +- package.json | 2 +- src/ol/source/rastersource.js | 20 ++-- test/spec/ol/source/rastersource.test.js | 66 ++++++------ 9 files changed, 138 insertions(+), 113 deletions(-) diff --git a/examples/raster.html b/examples/raster.html index b16689882f..01126e9c9f 100644 --- a/examples/raster.html +++ b/examples/raster.html @@ -12,8 +12,8 @@ docs: >

In this case, a single tiled source of imagery is used as input. - For each pixel, the Triangular Greenness Index - (TGI) + For each pixel, the Vegetaion Greenness Index + (VGI) is calculated from the input pixels. A second operation colors those pixels based on a threshold value (values above the threshold are green and those below are transparent). diff --git a/examples/raster.js b/examples/raster.js index 33630dce2f..a400a63450 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -7,68 +7,96 @@ goog.require('ol.layer.Tile'); goog.require('ol.source.BingMaps'); goog.require('ol.source.Raster'); -var minTgi = 0; -var maxTgi = 25; +var minVgi = 0; +var maxVgi = 0.25; +var bins = 10; -function tgi(pixels, data) { - var pixel = pixels[0]; + +/** + * Calculate the Vegetation Greenness Index (VGI) from an input pixel. This + * is a rough estimate assuming that pixel values correspond to reflectance. + * @param {ol.raster.Pixel} pixel An array of [R, G, B, A] values. + * @return {number} The VGI value for the given pixel. + */ +function vgi(pixel) { var r = pixel[0] / 255; var g = pixel[1] / 255; var b = pixel[2] / 255; - var value = (120 * (r - b) - (190 * (r - g))) / 2; - pixel[0] = value; - return pixels; + return (2 * g - r - b) / (2 * g + r + b); } -function summarize(pixels, data) { - var value = Math.floor(pixels[0][0]); - var counts = data.counts; - if (value >= counts.min && value < counts.max) { - counts.values[value - counts.min] += 1; - } - return pixels; -} -function color(pixels, data) { - var pixel = pixels[0]; - var value = pixel[0]; - if (value > data.threshold) { - pixel[0] = 0; - pixel[1] = 255; - pixel[2] = 0; - pixel[3] = 128; +/** + * Summarize values for a histogram. + * @param {numver} value A VGI value. + * @param {Object} counts An object for keeping track of VGI counts. + */ +function summarize(value, counts) { + var min = counts.min; + var max = counts.max; + var num = counts.values.length; + if (value < min) { + // do nothing + } else if (value >= max) { + counts.values[num - 1] += 1; } else { - pixel[3] = 0; + var index = Math.floor((value - min) / counts.delta); + counts.values[index] += 1; } - return pixels; } + +/** + * Use aerial imagery as the input data for the raster source. + */ var bing = new ol.source.BingMaps({ key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3', imagerySet: 'Aerial' }); + +/** + * Create a raster source where pixels with VGI values above a threshold will + * be colored green. + */ var raster = new ol.source.Raster({ sources: [bing], - operations: [tgi, summarize, color] + operation: function(pixels, data) { + var pixel = pixels[0]; + var value = vgi(pixel); + summarize(value, data.counts); + if (value >= data.threshold) { + pixel[0] = 0; + pixel[1] = 255; + pixel[2] = 0; + pixel[3] = 128; + } else { + pixel[3] = 0; + } + return pixel; + }, + lib: { + vgi: vgi, + summarize: summarize + } }); -raster.set('threshold', 10); +raster.set('threshold', 0.1); -function createCounts(min, max) { - var len = max - min; - var values = new Array(len); - for (var i = 0; i < len; ++i) { +function createCounts(min, max, num) { + var values = new Array(num); + for (var i = 0; i < num; ++i) { values[i] = 0; } return { min: min, max: max, - values: values + values: values, + delta: (max - min) / num }; } raster.on('beforeoperations', function(event) { - event.data.counts = createCounts(minTgi, maxTgi); + event.data.counts = createCounts(minVgi, maxVgi, bins); event.data.threshold = raster.get('threshold'); }); @@ -107,7 +135,7 @@ function schedulePlot(resolution, counts, threshold) { var barWidth = 15; var plotHeight = 150; var chart = d3.select('#plot').append('svg') - .attr('width', barWidth * (maxTgi - minTgi)) + .attr('width', barWidth * bins) .attr('height', plotHeight); var chartRect = chart[0][0].getBoundingClientRect(); @@ -124,35 +152,32 @@ function plot(resolution, counts, threshold) { bar.enter().append('rect'); - bar.attr('class', function(value, index) { - return 'bar' + (index - counts.min >= threshold ? ' selected' : ''); + bar.attr('class', function(count, index) { + var value = counts.min + (index * counts.delta); + return 'bar' + (value >= threshold ? ' selected' : ''); }) .attr('width', barWidth - 2); - bar.transition() - .attr('transform', function(value, index) { - return 'translate(' + (index * barWidth) + ', ' + - (plotHeight - yScale(value)) + ')'; - }) - .attr('height', yScale); + bar.transition().attr('transform', function(value, index) { + return 'translate(' + (index * barWidth) + ', ' + + (plotHeight - yScale(value)) + ')'; + }) + .attr('height', yScale); - bar.on('mousemove', function() { - var old = threshold; - threshold = counts.min + - Math.floor((d3.event.pageX - chartRect.left) / barWidth); - if (old !== threshold) { + bar.on('mousemove', function(count, index) { + var threshold = counts.min + (index * counts.delta); + if (raster.get('threshold') !== threshold) { raster.set('threshold', threshold); raster.changed(); } }); - bar.on('mouseover', function() { - var index = Math.floor((d3.event.pageX - chartRect.left) / barWidth); + bar.on('mouseover', function(count, index) { var area = 0; for (var i = counts.values.length - 1; i >= index; --i) { area += resolution * resolution * counts.values[i]; } - tip.html(message(index + counts.min, area)); + tip.html(message(counts.min + (index * counts.delta), area)); tip.style('display', 'block'); tip.transition().style({ left: (chartRect.left + (index * barWidth) + (barWidth / 2)) + 'px', @@ -171,5 +196,5 @@ function plot(resolution, counts, threshold) { function message(value, area) { var acres = (area / 4046.86).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return acres + ' acres at
' + value + ' TGI or above'; + return acres + ' acres at
' + value.toFixed(2) + ' VGI or above'; } diff --git a/examples/region-growing.html b/examples/region-growing.html index adcc843c27..eff326d577 100644 --- a/examples/region-growing.html +++ b/examples/region-growing.html @@ -27,7 +27,7 @@ tags: "raster, region growing" ---

-
+
diff --git a/examples/region-growing.js b/examples/region-growing.js index a393de64c3..b5ce834c4d 100644 --- a/examples/region-growing.js +++ b/examples/region-growing.js @@ -12,7 +12,7 @@ function growRegion(inputs, data) { var seed = data.pixel; var delta = parseInt(data.delta); if (!seed) { - return [image]; + return image; } seed = seed.map(Math.round); @@ -59,7 +59,7 @@ function growRegion(inputs, data) { } edge = newedge; } - return [new ImageData(outputData, width, height)]; + return new ImageData(outputData, width, height); } function next4Edges(edge) { @@ -81,8 +81,8 @@ var imagery = new ol.layer.Tile({ var raster = new ol.source.Raster({ sources: [imagery.getSource()], operationType: 'image', - operations: [growRegion], - // the contents of `lib` option will be available to the operation(s) run in + operation: growRegion, + // Functions in the `lib` object will be available to the operation run in // the web worker. lib: { nextEdges: next4Edges @@ -90,7 +90,7 @@ var raster = new ol.source.Raster({ }); var rasterImage = new ol.layer.Image({ - opacity: 0.8, + opacity: 0.7, source: raster }); @@ -98,8 +98,8 @@ var map = new ol.Map({ layers: [imagery, rasterImage], target: 'map', view: new ol.View({ - center: ol.proj.fromLonLat([-120, 50]), - zoom: 6 + center: ol.proj.fromLonLat([-119.07, 47.65]), + zoom: 11 }) }); diff --git a/examples/shaded-relief.js b/examples/shaded-relief.js index 175102140b..b4e276741a 100644 --- a/examples/shaded-relief.js +++ b/examples/shaded-relief.js @@ -81,7 +81,7 @@ function shade(inputs, data) { aspect = Math.atan2(dzdy, -dzdx); if (aspect < 0) { aspect = halfPi - aspect; - } else if (aspect > Math.PI / 2) { + } else if (aspect > halfPi) { aspect = twoPi - aspect + halfPi; } else { aspect = halfPi - aspect; @@ -99,7 +99,7 @@ function shade(inputs, data) { } } - return [new ImageData(shadeData, width, height)]; + return new ImageData(shadeData, width, height); } var elevation = new ol.source.XYZ({ @@ -110,7 +110,7 @@ var elevation = new ol.source.XYZ({ var raster = new ol.source.Raster({ sources: [elevation], operationType: 'image', - operations: [shade] + operation: shade }); var map = new ol.Map({ diff --git a/externs/olx.js b/externs/olx.js index c65ba8247b..2b1ad5f597 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4500,7 +4500,7 @@ olx.source.ImageVectorOptions.prototype.style; /** * @typedef {{sources: Array., - * operations: (Array.|undefined), + * operation: (ol.raster.Operation|undefined), * lib: (Object|undefined), * threads: (number|undefined), * operationType: (ol.raster.OperationType|undefined)}} @@ -4518,12 +4518,12 @@ olx.source.RasterOptions.prototype.sources; /** - * Pixel operations. Operations will be called with data from input sources - * and the final output will be assigned to the raster source. - * @type {Array.|undefined} + * Raster operation. The operation will be called with data from input sources + * and the output will be assigned to the raster source. + * @type {ol.raster.Operation|undefined} * @api */ -olx.source.RasterOptions.prototype.operations; +olx.source.RasterOptions.prototype.operation; /** diff --git a/package.json b/package.json index a0dafaeda5..f038a0b987 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", - "pixelworks": "^0.11.0", + "pixelworks": "1.0.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index c13a4c66d3..7fff3d1b3a 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -46,8 +46,8 @@ ol.source.Raster = function(options) { this.operationType_ = goog.isDef(options.operationType) ? options.operationType : ol.raster.OperationType.PIXEL; - var operations = goog.isDef(options.operations) ? - options.operations : [ol.raster.IdentityOp]; + var operation = goog.isDef(options.operation) ? + options.operation : ol.raster.IdentityOp; /** * @private @@ -59,7 +59,7 @@ ol.source.Raster = function(options) { * @private * @type {*} */ - this.worker_ = this.createWorker_(operations, options.lib, this.threads_); + this.worker_ = this.createWorker_(operation, options.lib, this.threads_); /** * @private @@ -146,16 +146,16 @@ goog.inherits(ol.source.Raster, ol.source.Image); /** * Create a worker. - * @param {Array.} operations The operations. + * @param {ol.raster.Operation} operation The operation. * @param {Object=} opt_lib Optional lib functions. * @param {number=} opt_threads Number of threads. * @return {*} The worker. * @private */ ol.source.Raster.prototype.createWorker_ = - function(operations, opt_lib, opt_threads) { + function(operation, opt_lib, opt_threads) { return new ol.ext.pixelworks.Processor({ - operations: operations, + operation: operation, imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, queue: 1, lib: opt_lib, @@ -165,14 +165,14 @@ ol.source.Raster.prototype.createWorker_ = /** - * Reset the operations. - * @param {Array.} operations New operations. + * Reset the operation. + * @param {ol.raster.Operation} operation New operation. * @param {Object=} opt_lib Functions that will be available to operations run * in a worker. * @api */ -ol.source.Raster.prototype.setOperations = function(operations, opt_lib) { - this.worker_ = this.createWorker_(operations, opt_lib, this.threads_); +ol.source.Raster.prototype.setOperation = function(operation, opt_lib) { + this.worker_ = this.createWorker_(operation, opt_lib, this.threads_); this.changed(); }; diff --git a/test/spec/ol/source/rastersource.test.js b/test/spec/ol/source/rastersource.test.js index 69e8f71914..0aa680f75f 100644 --- a/test/spec/ol/source/rastersource.test.js +++ b/test/spec/ol/source/rastersource.test.js @@ -52,9 +52,9 @@ describe('ol.source.Raster', function() { raster = new ol.source.Raster({ threads: 0, sources: [redSource, greenSource, blueSource], - operations: [function(inputs) { - return inputs; - }] + operation: function(inputs) { + return inputs[0]; + } }); map = new ol.Map({ @@ -91,17 +91,17 @@ describe('ol.source.Raster', function() { expect(source).to.be.a(ol.source.Raster); }); - itNoPhantom('defaults to "pixel" operations', function(done) { + itNoPhantom('defaults to "pixel" operation', function(done) { var log = []; raster = new ol.source.Raster({ threads: 0, sources: [redSource, greenSource, blueSource], - operations: [function(inputs) { + operation: function(inputs) { log.push(inputs); - return inputs; - }] + return inputs[0]; + } }); raster.on('afteroperations', function() { @@ -127,10 +127,10 @@ describe('ol.source.Raster', function() { operationType: ol.raster.OperationType.IMAGE, threads: 0, sources: [redSource, greenSource, blueSource], - operations: [function(inputs) { + operation: function(inputs) { log.push(inputs); - return inputs; - }] + return inputs[0]; + } }); raster.on('afteroperations', function() { @@ -149,12 +149,12 @@ describe('ol.source.Raster', function() { }); - describe('#setOperations()', function() { + describe('#setOperation()', function() { - itNoPhantom('allows operations to be set', function(done) { + itNoPhantom('allows operation to be set', function(done) { var count = 0; - raster.setOperations([function(pixels) { + raster.setOperation(function(pixels) { ++count; var redPixel = pixels[0]; var greenPixel = pixels[1]; @@ -162,8 +162,8 @@ describe('ol.source.Raster', function() { expect(redPixel).to.eql([255, 0, 0, 255]); expect(greenPixel).to.eql([0, 255, 0, 255]); expect(bluePixel).to.eql([0, 0, 255, 255]); - return pixels; - }]); + return pixels[0]; + }); var view = map.getView(); view.setCenter([0, 0]); @@ -176,7 +176,7 @@ describe('ol.source.Raster', function() { }); - itNoPhantom('updates and re-runs the operations', function(done) { + itNoPhantom('updates and re-runs the operation', function(done) { var view = map.getView(); view.setCenter([0, 0]); @@ -186,9 +186,9 @@ describe('ol.source.Raster', function() { raster.on('afteroperations', function(event) { ++count; if (count === 1) { - raster.setOperations([function(inputs) { - return inputs; - }]); + raster.setOperation(function(inputs) { + return inputs[0]; + }); } else { done(); } @@ -203,10 +203,10 @@ describe('ol.source.Raster', function() { itNoPhantom('gets called before operations are run', function(done) { var count = 0; - raster.setOperations([function(inputs) { + raster.setOperation(function(inputs) { ++count; - return inputs; - }]); + return inputs[0]; + }); raster.on('beforeoperations', function(event) { expect(count).to.equal(0); @@ -224,12 +224,12 @@ describe('ol.source.Raster', function() { }); - itNoPhantom('allows data to be set for the operations', function(done) { + itNoPhantom('allows data to be set for the operation', function(done) { - raster.setOperations([function(inputs, data) { + raster.setOperation(function(inputs, data) { ++data.count; - return inputs; - }]); + return inputs[0]; + }); raster.on('beforeoperations', function(event) { event.data.count = 0; @@ -253,10 +253,10 @@ describe('ol.source.Raster', function() { itNoPhantom('gets called after operations are run', function(done) { var count = 0; - raster.setOperations([function(inputs) { + raster.setOperation(function(inputs) { ++count; - return inputs; - }]); + return inputs[0]; + }); raster.on('afteroperations', function(event) { expect(count).to.equal(4); @@ -273,12 +273,12 @@ describe('ol.source.Raster', function() { }); - itNoPhantom('receives data set by the operations', function(done) { + itNoPhantom('receives data set by the operation', function(done) { - raster.setOperations([function(inputs, data) { + raster.setOperation(function(inputs, data) { data.message = 'hello world'; - return inputs; - }]); + return inputs[0]; + }); raster.on('afteroperations', function(event) { expect(event.data.message).to.equal('hello world'); From 16aa548383b4585ccad002ccfd8e25d97b9aceca Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 3 Aug 2015 21:32:16 -0600 Subject: [PATCH 27/28] Only create a worker if an operation is provided --- src/ol/raster/operation.js | 11 -------- src/ol/source/rastersource.js | 53 ++++++++++++----------------------- 2 files changed, 18 insertions(+), 46 deletions(-) diff --git a/src/ol/raster/operation.js b/src/ol/raster/operation.js index 9ebd914e50..4dca308908 100644 --- a/src/ol/raster/operation.js +++ b/src/ol/raster/operation.js @@ -1,4 +1,3 @@ -goog.provide('ol.raster.IdentityOp'); goog.provide('ol.raster.Operation'); goog.provide('ol.raster.OperationType'); @@ -31,13 +30,3 @@ ol.raster.OperationType = { * @api */ ol.raster.Operation; - - -/** - * The identity operation. Returns the supplied input data as output. - * @param {(Array.|Array.)} inputs Input data. - * @return {(Array.|Array.)} The output data. - */ -ol.raster.IdentityOp = function(inputs) { - return inputs; -}; diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 7fff3d1b3a..3f177a5799 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -16,7 +16,6 @@ goog.require('ol.ext.pixelworks'); goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Tile'); -goog.require('ol.raster.IdentityOp'); goog.require('ol.raster.OperationType'); goog.require('ol.renderer.canvas.ImageLayer'); goog.require('ol.renderer.canvas.TileLayer'); @@ -39,6 +38,12 @@ goog.require('ol.source.Tile'); */ ol.source.Raster = function(options) { + /** + * @private + * @type {*} + */ + this.worker_ = null; + /** * @private * @type {ol.raster.OperationType} @@ -46,21 +51,12 @@ ol.source.Raster = function(options) { this.operationType_ = goog.isDef(options.operationType) ? options.operationType : ol.raster.OperationType.PIXEL; - var operation = goog.isDef(options.operation) ? - options.operation : ol.raster.IdentityOp; - /** * @private * @type {number} */ this.threads_ = goog.isDef(options.threads) ? options.threads : 1; - /** - * @private - * @type {*} - */ - this.worker_ = this.createWorker_(operation, options.lib, this.threads_); - /** * @private * @type {Array.} @@ -135,44 +131,31 @@ ol.source.Raster = function(options) { wantedTiles: {} }; - goog.base(this, { - // TODO: pass along any relevant options - }); + goog.base(this, {}); + if (goog.isDef(options.operation)) { + this.setOperation(options.operation, options.lib); + } }; goog.inherits(ol.source.Raster, ol.source.Image); /** - * Create a worker. - * @param {ol.raster.Operation} operation The operation. - * @param {Object=} opt_lib Optional lib functions. - * @param {number=} opt_threads Number of threads. - * @return {*} The worker. - * @private - */ -ol.source.Raster.prototype.createWorker_ = - function(operation, opt_lib, opt_threads) { - return new ol.ext.pixelworks.Processor({ - operation: operation, - imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, - queue: 1, - lib: opt_lib, - threads: opt_threads - }); -}; - - -/** - * Reset the operation. + * Set the operation. * @param {ol.raster.Operation} operation New operation. * @param {Object=} opt_lib Functions that will be available to operations run * in a worker. * @api */ ol.source.Raster.prototype.setOperation = function(operation, opt_lib) { - this.worker_ = this.createWorker_(operation, opt_lib, this.threads_); + this.worker_ = new ol.ext.pixelworks.Processor({ + operation: operation, + imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, + queue: 1, + lib: opt_lib, + threads: this.threads_ + }); this.changed(); }; From cee34fa51b2f2a54dff149b44345e6f21a37da2d Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 3 Aug 2015 21:33:46 -0600 Subject: [PATCH 28/28] Table for controls --- examples/region-growing.css | 4 ++++ examples/region-growing.html | 17 ++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 examples/region-growing.css diff --git a/examples/region-growing.css b/examples/region-growing.css new file mode 100644 index 0000000000..dd027637bb --- /dev/null +++ b/examples/region-growing.css @@ -0,0 +1,4 @@ +table.controls td { + min-width: 110px; + padding: 2px 5px; +} diff --git a/examples/region-growing.html b/examples/region-growing.html index eff326d577..66f3065314 100644 --- a/examples/region-growing.html +++ b/examples/region-growing.html @@ -28,16 +28,11 @@ tags: "raster, region growing"
-
-
-
-
- Threshold: -
-
- -
-
- + + + + + +
Threshold: