diff --git a/examples/heatmap-earthquakes.html b/examples/heatmap-earthquakes.html new file mode 100644 index 0000000000..f65000e383 --- /dev/null +++ b/examples/heatmap-earthquakes.html @@ -0,0 +1,52 @@ + + + + + + + + + + Earthquakes heatmap + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Earthquakes heatmap

+

Demonstrates the use of a heatmap layer.

+
+

+ This example parses a KML file and renders the features as a ol.layer.Heatmap layer. +

+

See the heatmap-earthquakes.js source to see how this is done.

+
+
heatmap, kml, vector, style
+
+
+ +
+ + + + + + + diff --git a/examples/heatmap-earthquakes.js b/examples/heatmap-earthquakes.js new file mode 100644 index 0000000000..3f5026461d --- /dev/null +++ b/examples/heatmap-earthquakes.js @@ -0,0 +1,40 @@ +goog.require('ol.Map'); +goog.require('ol.View2D'); +goog.require('ol.layer.Heatmap'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.KML'); +goog.require('ol.source.Stamen'); + + +var vector = new ol.layer.Heatmap({ + source: new ol.source.KML({ + projection: 'EPSG:3857', + url: 'data/kml/2012_Earthquakes_Mag5.kml' + }), + radius: 5 +}); + +vector.getSource().on('addfeature', function(event) { + // 2012_Earthquakes_Mag5.kml stores the magnitude of each earthquake in a + // standards-violating tag in each Placemark. We extract it from + // the Placemark's name instead. + var name = event.feature.get('name'); + var magnitude = parseFloat(name.substr(2)); + event.feature.set('weight', magnitude - 5); +}); + +var raster = new ol.layer.Tile({ + source: new ol.source.Stamen({ + layer: 'toner' + }) +}); + +var map = new ol.Map({ + layers: [raster, vector], + renderer: 'canvas', + target: 'map', + view: new ol.View2D({ + center: [0, 0], + zoom: 2 + }) +}); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 19ef7ded72..7f1d94d252 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -540,6 +540,26 @@ * @todo stability experimental */ +/** + * @typedef {Object} olx.layer.HeatmapOptions + * @property {number|undefined} brightness Brightness. + * @property {number|undefined} contrast Contrast. + * @property {number|undefined} hue Hue. + * @property {Array.|undefined} gradient The color gradient of the heatmap, + * specified as an array of CSS color strings. Default is `['#00f', '#0ff', '#0f0', '#ff0', '#f00']`. + * @property {number|undefined} minResolution The minimum resolution + * (inclusive) at which this layer will be visible. + * @property {number|undefined} maxResolution The maximum resolution + * (exclusive) below which this layer will be visible. + * @property {number|undefined} opacity Opacity. 0-1. Default is `1`. + * @property {number|undefined} saturation Saturation. + * @property {ol.source.Vector} source Source. + * @property {ol.style.Style|Array.|ol.feature.StyleFunction|undefined} style Layer style. + * @property {boolean|undefined} visible Visibility. Default is `true` (visible). + * @todo stability experimental + */ + + /** * @typedef {Object} olx.layer.TileOptions * @property {number|undefined} brightness Brightness. Default is `0`. diff --git a/src/ol/layer/heatmaplayer.exports b/src/ol/layer/heatmaplayer.exports new file mode 100644 index 0000000000..be2de928b7 --- /dev/null +++ b/src/ol/layer/heatmaplayer.exports @@ -0,0 +1 @@ +@exportSymbol ol.layer.Heatmap diff --git a/src/ol/layer/heatmaplayer.js b/src/ol/layer/heatmaplayer.js new file mode 100644 index 0000000000..babd45c0f5 --- /dev/null +++ b/src/ol/layer/heatmaplayer.js @@ -0,0 +1,197 @@ +// FIXME feature weight property +goog.provide('ol.layer.Heatmap'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.events'); +goog.require('ol.Object'); +goog.require('ol.layer.Vector'); +goog.require('ol.render.EventType'); +goog.require('ol.style.Icon'); +goog.require('ol.style.Style'); + + +/** + * @enum {string} + */ +ol.layer.HeatmapLayerProperty = { + GRADIENT: 'gradient' +}; + + + +/** + * @constructor + * @extends {ol.layer.Vector} + * @param {olx.layer.HeatmapOptions=} opt_options Options. + * @todo stability experimental + */ +ol.layer.Heatmap = function(opt_options) { + var options = goog.isDef(opt_options) ? opt_options : {}; + + goog.base(this, /** @type {olx.layer.VectorOptions} */ (options)); + + /** + * @private + * @type {Uint32Array} + */ + this.gradient_ = null; + + goog.events.listen(this, + ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.GRADIENT), + this.handleGradientChanged_, false, this); + + this.setGradient(goog.isDef(options.gradient) ? + options.gradient : ol.layer.Heatmap.DEFAULT_GRADIENT); + + var radius = goog.isDef(options.radius) ? options.radius : 8; + var blur = goog.isDef(options.blur) ? options.blur : 15; + var shadow = goog.isDef(options.shadow) ? options.shadow : 250; + + var style = new ol.style.Style({ + image: ol.layer.Heatmap.createIcon_(radius, blur, shadow) + }); + + // FIXME: styles are immutable + // /** + // * @param {ol.Feature} feature + // * @param {number} resolution + // * @return {number} weight + // */ + // var weightFunction = function(feature, resolution) { + // var weight = /** @type {number} */ (feature.get('weight')); + // return goog.isDef(weight) ? weight : 1; + // }; + // + // var styleArray = [style]; + // this.setStyle(function(feature, resolution) { + // var image = style.getImage(); + // image.setOpacity(weightFunction(feature, resolution)); + // return styleArray; + // }); + + this.setStyle(style); + + goog.events.listen(this, ol.render.EventType.RENDER, + this.handleRender_, false, this); + +}; +goog.inherits(ol.layer.Heatmap, ol.layer.Vector); + + +/** + * @const + * @type {Array.} + */ +ol.layer.Heatmap.DEFAULT_GRADIENT = ['#00f', '#0ff', '#0f0', '#ff0', '#f00']; + + +/** + * @param {Array.} colors + * @return {Uint32Array} + * @private + */ +ol.layer.Heatmap.createGradient_ = function(colors) { + var canvas = goog.dom.createElement(goog.dom.TagName.CANVAS); + var context = canvas.getContext('2d'); + var width = 1; + var height = 256; + canvas.width = width; + canvas.height = height; + + var gradient = context.createLinearGradient(0, 0, width, height); + var step = 1 / colors.length; + for (var i = 0, ii = colors.length; i < ii; ++i) { + gradient.addColorStop(i * step, colors[i]); + } + + context.fillStyle = gradient; + context.fillRect(0, 0, width, height); + var imageData = context.getImageData(0, 0, width, height).data; + for (var i = 0, ii = imageData.length; i < ii; i += 4) { + imageData[i + 3] = i / 4; + } + return new Uint32Array(imageData.buffer); +}; + + +/** + * @param {number} radius Radius size in pixel. + * @param {number} blur Blur size in pixel. + * @param {number} shadow Shadow offset size in pixel. + * @return {ol.style.Icon} icon + * @private + */ +ol.layer.Heatmap.createIcon_ = function(radius, blur, shadow) { + var canvas = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + var context = canvas.getContext('2d'); + var halfSize = radius + blur + 1; + canvas.width = canvas.height = halfSize * 2; + context.shadowOffsetX = context.shadowOffsetY = shadow; + context.shadowBlur = blur; + context.shadowColor = '#000'; + context.beginPath(); + var center = halfSize - shadow; + context.arc(center, center, radius, 0, Math.PI * 2, true); + context.fill(); + return new ol.style.Icon({ + src: canvas.toDataURL() + }); +}; + + +/** + * @return {Array.} Colors. + */ +ol.layer.Heatmap.prototype.getGradient = function() { + return /** @type {Array.} */ ( + this.get(ol.layer.HeatmapLayerProperty.GRADIENT)); +}; +goog.exportProperty( + ol.layer.Heatmap.prototype, + 'getGradient', + ol.layer.Heatmap.prototype.getGradient); + + +/** + * @private + */ +ol.layer.Heatmap.prototype.handleGradientChanged_ = function() { + this.gradient_ = ol.layer.Heatmap.createGradient_(this.getGradient()); +}; + + +/** + * @param {ol.render.Event} event Post compose event + * @private + */ +ol.layer.Heatmap.prototype.handleRender_ = function(event) { + goog.asserts.assert(event.type == ol.render.EventType.RENDER); + var context = event.context; + var canvas = context.canvas; + var image = context.getImageData(0, 0, canvas.width, canvas.height); + var view32 = new Uint32Array(image.data.buffer); + var view8 = image.data; + var i, ii, alpha; + for (i = 0, ii = view32.length; i < ii; ++i) { + alpha = view8[4 * i + 3]; + if (alpha) { + view32[i] = this.gradient_[alpha]; + } + } + context.putImageData(image, 0, 0); +}; + + +/** + * @param {Array.} colors Gradient. + */ +ol.layer.Heatmap.prototype.setGradient = function(colors) { + this.set(ol.layer.HeatmapLayerProperty.GRADIENT, colors); +}; +goog.exportProperty( + ol.layer.Heatmap.prototype, + 'setGradient', + ol.layer.Heatmap.prototype.setGradient); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 356603b6ed..00bb2db877 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -101,7 +101,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = replayContext.globalAlpha = layerState.opacity; replayGroup.replay( replayContext, frameState.extent, frameState.pixelRatio, transform, - renderGeometryFunction); + frameState.view2DState.rotation, renderGeometryFunction); if (replayContext != context) { this.dispatchRenderEvent(replayContext, frameState, transform);