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);