diff --git a/src/ol/renderer/canvas/canvasrenderer.js b/src/ol/renderer/canvas/canvasrenderer.js index 4f0d0b7fb48..3c2197ec858 100644 --- a/src/ol/renderer/canvas/canvasrenderer.js +++ b/src/ol/renderer/canvas/canvasrenderer.js @@ -2,6 +2,7 @@ goog.provide('ol.renderer.canvas.Renderer'); goog.provide('ol.renderer.canvas.SUPPORTED'); goog.require('goog.asserts'); +goog.require('goog.net.ImageLoader'); goog.require('goog.vec.Mat4'); goog.require('ol.Feature'); goog.require('ol.Pixel'); @@ -11,6 +12,7 @@ goog.require('ol.geom.GeometryType'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); +goog.require('ol.style.IconLiteral'); goog.require('ol.style.LineLiteral'); goog.require('ol.style.PointLiteral'); goog.require('ol.style.PolygonLiteral'); @@ -32,10 +34,13 @@ ol.renderer.canvas.SUPPORTED = ol.canvas.SUPPORTED; * @param {HTMLCanvasElement} canvas Target canvas. * @param {goog.vec.Mat4.Number} transform Transform. * @param {ol.Pixel=} opt_offset Pixel offset for top-left corner. This is - * provided as an optional argument as a convenience in cases where the - * transform applies to a separate canvas. + * provided as an optional argument as a convenience in cases where the + * transform applies to a separate canvas. + * @param {function()=} opt_iconLoadedCallback Callback for deferred rendering + * when images need to be loaded before rendering. */ -ol.renderer.canvas.Renderer = function(canvas, transform, opt_offset) { +ol.renderer.canvas.Renderer = + function(canvas, transform, opt_offset, opt_iconLoadedCallback) { var context = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')), @@ -70,6 +75,12 @@ ol.renderer.canvas.Renderer = function(canvas, transform, opt_offset) { */ this.context_ = context; + /** + * @type {function()|undefined} + * @private + */ + this.iconLoadedCallback_ = opt_iconLoadedCallback; + }; @@ -77,13 +88,15 @@ ol.renderer.canvas.Renderer = function(canvas, transform, opt_offset) { * @param {ol.geom.GeometryType} type Geometry type. * @param {Array.} features Array of features. * @param {ol.style.SymbolizerLiteral} symbolizer Symbolizer. + * @return {boolean} true if deferred, false if rendered. */ ol.renderer.canvas.Renderer.prototype.renderFeaturesByGeometryType = function(type, features, symbolizer) { + var deferred = false; switch (type) { case ol.geom.GeometryType.POINT: goog.asserts.assert(symbolizer instanceof ol.style.PointLiteral); - this.renderPointFeatures_( + deferred = this.renderPointFeatures_( features, /** @type {ol.style.PointLiteral} */ (symbolizer)); break; case ol.geom.GeometryType.LINESTRING: @@ -99,6 +112,7 @@ ol.renderer.canvas.Renderer.prototype.renderFeaturesByGeometryType = default: throw new Error('Rendering not implemented for geometry type: ' + type); } + return deferred; }; @@ -138,31 +152,44 @@ ol.renderer.canvas.Renderer.prototype.renderLineStringFeatures_ = /** * @param {Array.} features Array of point features. * @param {ol.style.PointLiteral} symbolizer Point symbolizer. + * @return {boolean} true if deferred, false if rendered. * @private */ ol.renderer.canvas.Renderer.prototype.renderPointFeatures_ = function(features, symbolizer) { var context = this.context_, - canvas, i, ii, point, vec; + content, alpha, i, ii, point, vec; if (symbolizer instanceof ol.style.ShapeLiteral) { - canvas = ol.renderer.canvas.Renderer.renderShape(symbolizer); + content = ol.renderer.canvas.Renderer.renderShape(symbolizer); + alpha = 1; + } else if (symbolizer instanceof ol.style.IconLiteral) { + content = ol.renderer.canvas.Renderer.renderIcon( + symbolizer, this.iconLoadedCallback_); + alpha = symbolizer.opacity; } else { throw new Error('Unsupported symbolizer: ' + symbolizer); } - var mid = canvas.width / 2; + if (goog.isNull(content)) { + return true; + } + + var midWidth = content.width / 2; + var midHeight = content.height / 2; context.save(); - context.setTransform(1, 0, 0, 1, -mid, -mid); - context.globalAlpha = 1; + context.setTransform(1, 0, 0, 1, -midWidth, -midHeight); + context.globalAlpha = alpha; for (i = 0, ii = features.length; i < ii; ++i) { point = /** @type {ol.geom.Point} */ features[i].getGeometry(); vec = goog.vec.Mat4.multVec3( this.transform_, [point.get(0), point.get(1), 0], []); - context.drawImage(canvas, vec[0], vec[1]); + context.drawImage(content, vec[0], vec[1]); } context.restore(); + + return false; }; @@ -293,3 +320,84 @@ ol.renderer.canvas.Renderer.renderShape = function(shape) { } return canvas; }; + + +/** + * @param {ol.style.IconLiteral} icon Icon literal. + * @param {function()=} opt_callback Callback which will be called when + * the icon is loaded and rendering will work without deferring. + * @return {HTMLImageElement} image element of null if deferred. + */ +ol.renderer.canvas.Renderer.renderIcon = function(icon, opt_callback) { + var url = icon.url; + var image = ol.renderer.canvas.Renderer.icons_[url]; + var deferred = false; + if (!goog.isDef(image)) { + deferred = true; + image = /** @type {HTMLImageElement} */ + (goog.dom.createElement(goog.dom.TagName.IMG)); + goog.events.listenOnce(image, goog.events.EventType.ERROR, + goog.bind(ol.renderer.canvas.Renderer.handleIconError_, null, + opt_callback), + false, ol.renderer.canvas.Renderer.renderIcon); + goog.events.listenOnce(image, goog.events.EventType.LOAD, + goog.bind(ol.renderer.canvas.Renderer.handleIconLoad_, null, + opt_callback), + false, ol.renderer.canvas.Renderer.renderIcon); + image.setAttribute('src', url); + ol.renderer.canvas.Renderer.icons_[url] = image; + } else if (!goog.isNull(image)) { + var width = icon.width, + height = icon.height; + if (goog.isDef(width) && goog.isDef(height)) { + image.width = width; + image.height = height; + } else if (goog.isDef(width)) { + image.height = width / image.width * image.height; + } else if (goog.isDef(height)) { + image.width = height / image.height * image.width; + } + } + return deferred ? null : image; +}; + + +/** + * @type {Object.} + * @private + */ +ol.renderer.canvas.Renderer.icons_ = {}; + + +/** + * @param {function()=} opt_callback Callback. + * @param {Event=} opt_event Event. + * @private + */ +ol.renderer.canvas.Renderer.handleIconError_ = + function(opt_callback, opt_event) { + if (goog.isDef(opt_event)) { + var url = opt_event.target.getAttribute('src'); + ol.renderer.canvas.Renderer.icons_[url] = null; + ol.renderer.canvas.Renderer.handleIconLoad_(opt_callback, opt_event); + } +}; + + +/** + * @param {function()=} opt_callback Callback. + * @param {Event=} opt_event Event. + * @private + */ +ol.renderer.canvas.Renderer.handleIconLoad_ = + function(opt_callback, opt_event) { + if (goog.isDef(opt_event)) { + var url = opt_event.target.getAttribute('src'); + ol.renderer.canvas.Renderer.icons_[url] = + /** @type {HTMLImageElement} */ (opt_event.target); + } + if (goog.isDef(opt_callback)) { + opt_callback(); + } +}; + diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index b3a0a191b65..a81f18d9515 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -126,6 +126,15 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) { */ this.tileGrid_ = null; + /** + * @private + * @type {function()} + */ + this.requestMapRenderFrame_ = goog.bind(function() { + this.dirty_ = true; + mapRenderer.getMap().requestRenderFrame(); + }, this); + }; goog.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer); @@ -252,7 +261,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = sketchCanvas.height = sketchSize.height; var sketchCanvasRenderer = new ol.renderer.canvas.Renderer( - sketchCanvas, sketchTransform); + sketchCanvas, sketchTransform, undefined, this.requestMapRenderFrame_); // clear/resize final canvas var finalCanvas = this.canvas_; @@ -267,15 +276,15 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var filters = this.geometryFilters_, numFilters = filters.length, i, geomFilter, extentFilter, type, features, - groups, group, j, numGroups; + groups, group, j, numGroups, deferred; for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + deferred = false; tileCoord = new ol.TileCoord(z, x, y); key = tileCoord.toString(); if (this.tileCache_.containsKey(key)) { tilesToRender[key] = tileCoord; } else if (!frameState.viewHints[ol.ViewHint.ANIMATING]) { - tilesToRender[key] = tileCoord; extentFilter = new ol.filter.Extent( tileGrid.getTileCoordExtent(tileCoord)); for (i = 0; i < numFilters; ++i) { @@ -288,11 +297,14 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = numGroups = groups.length; for (j = 0; j < numGroups; ++j) { group = groups[j]; - sketchCanvasRenderer.renderFeaturesByGeometryType(type, - group[0], group[1]); + deferred = sketchCanvasRenderer.renderFeaturesByGeometryType( + type, group[0], group[1]) || deferred; } } } + if (!deferred) { + tilesToRender[key] = tileCoord; + } } } } diff --git a/src/ol/style/icon.js b/src/ol/style/icon.js new file mode 100644 index 00000000000..251981b6d3c --- /dev/null +++ b/src/ol/style/icon.js @@ -0,0 +1,170 @@ +goog.provide('ol.style.Icon'); +goog.provide('ol.style.IconLiteral'); +goog.provide('ol.style.IconType'); + +goog.require('ol.Expression'); +goog.require('ol.ExpressionLiteral'); +goog.require('ol.style.Point'); +goog.require('ol.style.PointLiteral'); + + +/** + * @typedef {{url: (string), + * width: (number|undefined), + * height: (number|undefined), + * opacity: (number), + * rotation: (number)}} + */ +ol.style.IconLiteralOptions; + + + +/** + * @constructor + * @extends {ol.style.PointLiteral} + * @param {ol.style.IconLiteralOptions} config Symbolizer properties. + */ +ol.style.IconLiteral = function(config) { + + /** @type {string} */ + this.url = config.url; + + /** @type {number|undefined} */ + this.width = config.width; + + /** @type {number|undefined} */ + this.height = config.height; + + /** @type {number} */ + this.opacity = config.opacity; + + /** @type {number} */ + this.rotation = config.rotation; + +}; +goog.inherits(ol.style.IconLiteral, ol.style.PointLiteral); + + +/** + * @inheritDoc + */ +ol.style.IconLiteral.prototype.equals = function(iconLiteral) { + return this.url == iconLiteral.type && + this.width == iconLiteral.width && + this.height == iconLiteral.height && + this.opacity == iconLiteral.opacity && + this.rotation == iconLiteral.rotation; +}; + + +/** + * @typedef {{url: (string|ol.Expression), + * width: (number|ol.Expression|undefined), + * height: (number|ol.Expression|undefined), + * opacity: (number|ol.Expression|undefined), + * rotation: (number|ol.Expression|undefined)}} + */ +ol.style.IconOptions; + + + +/** + * @constructor + * @extends {ol.style.Point} + * @param {ol.style.IconOptions} options Symbolizer properties. + */ +ol.style.Icon = function(options) { + + goog.asserts.assert(options.url, 'url must be set'); + + /** + * @type {ol.Expression} + * @private + */ + this.url_ = (options.url instanceof ol.Expression) ? + options.url : new ol.ExpressionLiteral(options.url); + + /** + * @type {ol.Expression} + * @private + */ + this.width_ = !goog.isDef(options.width) ? + null : + (options.width instanceof ol.Expression) ? + options.width : new ol.ExpressionLiteral(options.width); + + /** + * @type {ol.Expression} + * @private + */ + this.height_ = !goog.isDef(options.height) ? + null : + (options.height instanceof ol.Expression) ? + options.height : new ol.ExpressionLiteral(options.height); + + /** + * @type {ol.Expression} + * @private + */ + this.opacity_ = !goog.isDef(options.opacity) ? + new ol.ExpressionLiteral(ol.style.IconDefaults.opacity) : + (options.opacity instanceof ol.Expression) ? + options.opacity : new ol.ExpressionLiteral(options.opacity); + + /** + * @type {ol.Expression} + * @private + */ + this.rotation_ = !goog.isDef(options.rotation) ? + new ol.ExpressionLiteral(ol.style.IconDefaults.rotation) : + (options.rotation instanceof ol.Expression) ? + options.rotation : new ol.ExpressionLiteral(options.rotation); + +}; + + +/** + * @inheritDoc + * @return {ol.style.IconLiteral} Literal shape symbolizer. + */ +ol.style.Icon.prototype.createLiteral = function(feature) { + var attrs = feature.getAttributes(); + + var url = /** @type {string} */ (this.url_.evaluate(feature, attrs)); + goog.asserts.assert(goog.isString(url) && url != '#', 'url must be a string'); + + var width = /** @type {number|undefined} */ (goog.isNull(this.width_) ? + undefined : this.width_.evaluate(feature, attrs)); + goog.asserts.assert(!goog.isDef(width) || goog.isNumber(width), + 'width must be undefined or a number'); + + var height = /** @type {number|undefined} */ (goog.isNull(this.height_) ? + undefined : this.height_.evaluate(feature, attrs)); + goog.asserts.assert(!goog.isDef(height) || goog.isNumber(height), + 'height must be undefined or a number'); + + var opacity = /** {@type {number} */ (this.opacity_.evaluate(feature, attrs)); + goog.asserts.assertNumber(opacity, 'opacity must be a number'); + + var rotation = + /** {@type {number} */ (this.opacity_.evaluate(feature, attrs)); + goog.asserts.assertNumber(rotation, 'rotation must be a number'); + + return new ol.style.IconLiteral({ + url: url, + width: width, + height: height, + opacity: opacity, + rotation: rotation + }); +}; + + +/** + * @type {ol.style.IconLiteral} + */ +ol.style.IconDefaults = new ol.style.IconLiteral({ + url: '#', + opacity: 1, + rotation: 0 +}); diff --git a/test/spec/ol/parser/geojson.test.js b/test/spec/ol/parser/geojson.test.js index f5e750aa4de..0153e4ee2fc 100644 --- a/test/spec/ol/parser/geojson.test.js +++ b/test/spec/ol/parser/geojson.test.js @@ -185,7 +185,7 @@ describe('ol.parser.GeoJSON', function() { var callback = function(feature, type) { return lookup[type]; - } + }; var result = parser.readFeaturesFromString(text, {callback: callback}); expect(result.length).toBe(179);