Rendering icons from external graphic urls

This commit is contained in:
ahocevar
2013-03-06 01:57:08 +01:00
parent 4f2523fcf2
commit ab19d255c7
4 changed files with 306 additions and 16 deletions

View File

@@ -2,6 +2,7 @@ goog.provide('ol.renderer.canvas.Renderer');
goog.provide('ol.renderer.canvas.SUPPORTED'); goog.provide('ol.renderer.canvas.SUPPORTED');
goog.require('goog.asserts'); goog.require('goog.asserts');
goog.require('goog.net.ImageLoader');
goog.require('goog.vec.Mat4'); goog.require('goog.vec.Mat4');
goog.require('ol.Feature'); goog.require('ol.Feature');
goog.require('ol.Pixel'); goog.require('ol.Pixel');
@@ -11,6 +12,7 @@ goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString'); goog.require('ol.geom.LineString');
goog.require('ol.geom.Point'); goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon'); goog.require('ol.geom.Polygon');
goog.require('ol.style.IconLiteral');
goog.require('ol.style.LineLiteral'); goog.require('ol.style.LineLiteral');
goog.require('ol.style.PointLiteral'); goog.require('ol.style.PointLiteral');
goog.require('ol.style.PolygonLiteral'); goog.require('ol.style.PolygonLiteral');
@@ -32,10 +34,13 @@ ol.renderer.canvas.SUPPORTED = ol.canvas.SUPPORTED;
* @param {HTMLCanvasElement} canvas Target canvas. * @param {HTMLCanvasElement} canvas Target canvas.
* @param {goog.vec.Mat4.Number} transform Transform. * @param {goog.vec.Mat4.Number} transform Transform.
* @param {ol.Pixel=} opt_offset Pixel offset for top-left corner. This is * @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 * provided as an optional argument as a convenience in cases where the
* transform applies to a separate canvas. * 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} */ var context = /** @type {CanvasRenderingContext2D} */
(canvas.getContext('2d')), (canvas.getContext('2d')),
@@ -70,6 +75,12 @@ ol.renderer.canvas.Renderer = function(canvas, transform, opt_offset) {
*/ */
this.context_ = context; 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 {ol.geom.GeometryType} type Geometry type.
* @param {Array.<ol.Feature>} features Array of features. * @param {Array.<ol.Feature>} features Array of features.
* @param {ol.style.SymbolizerLiteral} symbolizer Symbolizer. * @param {ol.style.SymbolizerLiteral} symbolizer Symbolizer.
* @return {boolean} true if deferred, false if rendered.
*/ */
ol.renderer.canvas.Renderer.prototype.renderFeaturesByGeometryType = ol.renderer.canvas.Renderer.prototype.renderFeaturesByGeometryType =
function(type, features, symbolizer) { function(type, features, symbolizer) {
var deferred = false;
switch (type) { switch (type) {
case ol.geom.GeometryType.POINT: case ol.geom.GeometryType.POINT:
goog.asserts.assert(symbolizer instanceof ol.style.PointLiteral); goog.asserts.assert(symbolizer instanceof ol.style.PointLiteral);
this.renderPointFeatures_( deferred = this.renderPointFeatures_(
features, /** @type {ol.style.PointLiteral} */ (symbolizer)); features, /** @type {ol.style.PointLiteral} */ (symbolizer));
break; break;
case ol.geom.GeometryType.LINESTRING: case ol.geom.GeometryType.LINESTRING:
@@ -99,6 +112,7 @@ ol.renderer.canvas.Renderer.prototype.renderFeaturesByGeometryType =
default: default:
throw new Error('Rendering not implemented for geometry type: ' + type); throw new Error('Rendering not implemented for geometry type: ' + type);
} }
return deferred;
}; };
@@ -138,31 +152,44 @@ ol.renderer.canvas.Renderer.prototype.renderLineStringFeatures_ =
/** /**
* @param {Array.<ol.Feature>} features Array of point features. * @param {Array.<ol.Feature>} features Array of point features.
* @param {ol.style.PointLiteral} symbolizer Point symbolizer. * @param {ol.style.PointLiteral} symbolizer Point symbolizer.
* @return {boolean} true if deferred, false if rendered.
* @private * @private
*/ */
ol.renderer.canvas.Renderer.prototype.renderPointFeatures_ = ol.renderer.canvas.Renderer.prototype.renderPointFeatures_ =
function(features, symbolizer) { function(features, symbolizer) {
var context = this.context_, var context = this.context_,
canvas, i, ii, point, vec; content, alpha, i, ii, point, vec;
if (symbolizer instanceof ol.style.ShapeLiteral) { 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 { } else {
throw new Error('Unsupported symbolizer: ' + symbolizer); 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.save();
context.setTransform(1, 0, 0, 1, -mid, -mid); context.setTransform(1, 0, 0, 1, -midWidth, -midHeight);
context.globalAlpha = 1; context.globalAlpha = alpha;
for (i = 0, ii = features.length; i < ii; ++i) { for (i = 0, ii = features.length; i < ii; ++i) {
point = /** @type {ol.geom.Point} */ features[i].getGeometry(); point = /** @type {ol.geom.Point} */ features[i].getGeometry();
vec = goog.vec.Mat4.multVec3( vec = goog.vec.Mat4.multVec3(
this.transform_, [point.get(0), point.get(1), 0], []); 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(); context.restore();
return false;
}; };
@@ -293,3 +320,84 @@ ol.renderer.canvas.Renderer.renderShape = function(shape) {
} }
return canvas; 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.<string, HTMLImageElement>}
* @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();
}
};

View File

@@ -126,6 +126,15 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) {
*/ */
this.tileGrid_ = null; 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); goog.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer);
@@ -252,7 +261,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
sketchCanvas.height = sketchSize.height; sketchCanvas.height = sketchSize.height;
var sketchCanvasRenderer = new ol.renderer.canvas.Renderer( var sketchCanvasRenderer = new ol.renderer.canvas.Renderer(
sketchCanvas, sketchTransform); sketchCanvas, sketchTransform, undefined, this.requestMapRenderFrame_);
// clear/resize final canvas // clear/resize final canvas
var finalCanvas = this.canvas_; var finalCanvas = this.canvas_;
@@ -267,15 +276,15 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
var filters = this.geometryFilters_, var filters = this.geometryFilters_,
numFilters = filters.length, numFilters = filters.length,
i, geomFilter, extentFilter, type, features, i, geomFilter, extentFilter, type, features,
groups, group, j, numGroups; groups, group, j, numGroups, deferred;
for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (y = tileRange.minY; y <= tileRange.maxY; ++y) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
deferred = false;
tileCoord = new ol.TileCoord(z, x, y); tileCoord = new ol.TileCoord(z, x, y);
key = tileCoord.toString(); key = tileCoord.toString();
if (this.tileCache_.containsKey(key)) { if (this.tileCache_.containsKey(key)) {
tilesToRender[key] = tileCoord; tilesToRender[key] = tileCoord;
} else if (!frameState.viewHints[ol.ViewHint.ANIMATING]) { } else if (!frameState.viewHints[ol.ViewHint.ANIMATING]) {
tilesToRender[key] = tileCoord;
extentFilter = new ol.filter.Extent( extentFilter = new ol.filter.Extent(
tileGrid.getTileCoordExtent(tileCoord)); tileGrid.getTileCoordExtent(tileCoord));
for (i = 0; i < numFilters; ++i) { for (i = 0; i < numFilters; ++i) {
@@ -288,11 +297,14 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
numGroups = groups.length; numGroups = groups.length;
for (j = 0; j < numGroups; ++j) { for (j = 0; j < numGroups; ++j) {
group = groups[j]; group = groups[j];
sketchCanvasRenderer.renderFeaturesByGeometryType(type, deferred = sketchCanvasRenderer.renderFeaturesByGeometryType(
group[0], group[1]); type, group[0], group[1]) || deferred;
} }
} }
} }
if (!deferred) {
tilesToRender[key] = tileCoord;
}
} }
} }
} }

170
src/ol/style/icon.js Normal file
View File

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

View File

@@ -185,7 +185,7 @@ describe('ol.parser.GeoJSON', function() {
var callback = function(feature, type) { var callback = function(feature, type) {
return lookup[type]; return lookup[type];
} };
var result = parser.readFeaturesFromString(text, {callback: callback}); var result = parser.readFeaturesFromString(text, {callback: callback});
expect(result.length).toBe(179); expect(result.length).toBe(179);