goog.provide('ol.renderer.canvas.Vector'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.vec.Mat4'); goog.require('ol.Feature'); goog.require('ol.geom.AbstractCollection'); goog.require('ol.geom.Geometry'); goog.require('ol.geom.GeometryType'); goog.require('ol.geom.LineString'); goog.require('ol.geom.MultiLineString'); goog.require('ol.geom.MultiPoint'); goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.style.IconLiteral'); goog.require('ol.style.LineLiteral'); goog.require('ol.style.Literal'); goog.require('ol.style.PointLiteral'); goog.require('ol.style.PolygonLiteral'); goog.require('ol.style.ShapeLiteral'); goog.require('ol.style.ShapeType'); goog.require('ol.style.TextLiteral'); /** * @constructor * @param {HTMLCanvasElement} canvas Target canvas. * @param {goog.vec.Mat4.Number} transform Transform. * @param {function()=} opt_iconLoadedCallback Callback for deferred rendering * when images need to be loaded before rendering. */ ol.renderer.canvas.Vector = function(canvas, transform, opt_iconLoadedCallback) { var context = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); /** * @type {goog.vec.Mat4.Number} * @private */ this.transform_ = transform; var vec = [1, 0, 0]; goog.vec.Mat4.multVec3NoTranslate(transform, vec, vec); /** * @type {number} * @private */ this.inverseScale_ = 1 / Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); /** * @type {CanvasRenderingContext2D} * @private */ this.context_ = context; /** * @type {function()|undefined} * @private */ this.iconLoadedCallback_ = opt_iconLoadedCallback; /** * @type {Object.>} * @private */ this.symbolSizes_ = {}; /** * @type {Object.>} * @private */ this.symbolOffsets_ = {}; /** * @type {Array.} * @private */ this.maxSymbolSize_ = [0, 0]; }; /** * @return {Object.>} Symbolizer sizes. */ ol.renderer.canvas.Vector.prototype.getSymbolSizes = function() { return this.symbolSizes_; }; /** * @return {Object.>} Symbolizer offsets. */ ol.renderer.canvas.Vector.prototype.getSymbolOffsets = function() { return this.symbolOffsets_; }; /** * @return {Array.} Maximum symbolizer size. */ ol.renderer.canvas.Vector.prototype.getMaxSymbolSize = function() { return this.maxSymbolSize_; }; /** * @param {Array.} features Array of features. * @param {ol.style.Literal} symbolizer Symbolizer. * @param {Array} data Additional data. * @return {boolean} true if deferred, false if rendered. */ ol.renderer.canvas.Vector.prototype.renderFeatures = function(features, symbolizer, data) { var deferred = false; if (symbolizer instanceof ol.style.PointLiteral) { deferred = this.renderPointFeatures_(features, symbolizer); } else if (symbolizer instanceof ol.style.LineLiteral) { this.renderLineStringFeatures_(features, symbolizer); } else if (symbolizer instanceof ol.style.PolygonLiteral) { this.renderPolygonFeatures_(features, symbolizer); } else if (symbolizer instanceof ol.style.TextLiteral) { this.renderText_(features, symbolizer, data); } return deferred; }; /** * @param {Array.} features Array of line features. * @param {ol.style.LineLiteral} symbolizer Line symbolizer. * @private */ ol.renderer.canvas.Vector.prototype.renderLineStringFeatures_ = function(features, symbolizer) { var context = this.context_, i, ii, feature, id, currentSize, geometry, components, j, jj, line, k, kk, vec, strokeSize; context.globalAlpha = symbolizer.opacity; context.strokeStyle = symbolizer.color; context.lineWidth = symbolizer.width; context.lineCap = 'round'; // TODO: accept this as a symbolizer property context.lineJoin = 'round'; // TODO: accept this as a symbolizer property strokeSize = context.lineWidth * this.inverseScale_; context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { continue; } id = goog.getUid(feature); currentSize = goog.isDef(this.symbolSizes_[id]) ? this.symbolSizes_[id] : [0]; currentSize[0] = Math.max(currentSize[0], strokeSize); this.symbolSizes_[id] = currentSize; this.maxSymbolSize_ = [Math.max(currentSize[0], this.maxSymbolSize_[0]), Math.max(currentSize[0], this.maxSymbolSize_[1])]; geometry = feature.getGeometry(); if (geometry instanceof ol.geom.LineString) { components = [geometry]; } else { goog.asserts.assert(geometry instanceof ol.geom.MultiLineString, 'Expected MultiLineString'); components = geometry.getComponents(); } for (j = 0, jj = components.length; j < jj; ++j) { line = components[j]; for (k = 0, kk = line.getCount(); k < kk; ++k) { vec = [line.get(k, 0), line.get(k, 1), 0]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); if (k === 0) { context.moveTo(vec[0], vec[1]); } else { context.lineTo(vec[0], vec[1]); } } } } context.stroke(); }; /** * @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.Vector.prototype.renderPointFeatures_ = function(features, symbolizer) { var context = this.context_, content, alpha, i, ii, feature, id, size, geometry, components, j, jj, point, vec; var xOffset = 0; var yOffset = 0; if (symbolizer instanceof ol.style.ShapeLiteral) { content = ol.renderer.canvas.Vector.renderShape(symbolizer); alpha = 1; } else if (symbolizer instanceof ol.style.IconLiteral) { content = ol.renderer.canvas.Vector.renderIcon( symbolizer, this.iconLoadedCallback_); alpha = symbolizer.opacity; xOffset = symbolizer.xOffset; yOffset = symbolizer.yOffset; } else { throw new Error('Unsupported symbolizer: ' + symbolizer); } if (goog.isNull(content)) { return true; } var midWidth = content.width / 2; var midHeight = content.height / 2; var contentWidth = content.width * this.inverseScale_; var contentHeight = content.height * this.inverseScale_; var contentXOffset = xOffset * this.inverseScale_; var contentYOffset = yOffset * this.inverseScale_; context.save(); context.setTransform(1, 0, 0, 1, -midWidth, -midHeight); context.globalAlpha = alpha; for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { continue; } id = goog.getUid(feature); size = this.symbolSizes_[id]; this.symbolSizes_[id] = goog.isDef(size) ? [Math.max(size[0], contentWidth), Math.max(size[1], contentHeight)] : [contentWidth, contentHeight]; this.symbolOffsets_[id] = [xOffset * this.inverseScale_, yOffset * this.inverseScale_]; this.maxSymbolSize_ = [Math.max(this.maxSymbolSize_[0], this.symbolSizes_[id][0] + 2 * Math.abs(contentXOffset)), Math.max(this.maxSymbolSize_[1], this.symbolSizes_[id][1] + 2 * Math.abs(contentYOffset))]; geometry = feature.getGeometry(); if (geometry instanceof ol.geom.Point) { components = [geometry]; } else { goog.asserts.assert(geometry instanceof ol.geom.MultiPoint, 'Expected MultiPoint'); components = geometry.getComponents(); } for (j = 0, jj = components.length; j < jj; ++j) { point = components[j]; vec = [point.get(0), point.get(1), 0]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); context.drawImage(content, vec[0] + xOffset, vec[1] + yOffset, content.width, content.height); } } context.restore(); return false; }; /** * @param {Array.} features Array of features. * @param {ol.style.TextLiteral} text Text symbolizer. * @param {Array} texts Label text for each feature. * @private */ ol.renderer.canvas.Vector.prototype.renderText_ = function(features, text, texts) { var context = this.context_, feature, vecs, vec; if (context.fillStyle !== text.color) { context.fillStyle = text.color; } context.font = text.fontSize + 'px ' + text.fontFamily; context.globalAlpha = text.opacity; // TODO: make alignments configurable context.textAlign = 'center'; context.textBaseline = 'middle'; for (var i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { continue; } vecs = ol.renderer.canvas.Vector.getLabelVectors( feature.getGeometry()); for (var j = 0, jj = vecs.length; j < jj; ++j) { vec = vecs[j]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); context.fillText(texts[i], vec[0], vec[1]); } } }; /** * @param {Array.} features Array of polygon features. * @param {ol.style.PolygonLiteral} symbolizer Polygon symbolizer. * @private */ ol.renderer.canvas.Vector.prototype.renderPolygonFeatures_ = function(features, symbolizer) { var context = this.context_, strokeColor = symbolizer.strokeColor, strokeWidth = symbolizer.strokeWidth, strokeOpacity = symbolizer.strokeOpacity, fillColor = symbolizer.fillColor, fillOpacity = symbolizer.fillOpacity, globalAlpha, i, ii, geometry, components, j, jj, poly, rings, numRings, ring, k, kk, vec, feature; if (strokeColor) { context.strokeStyle = strokeColor; if (strokeWidth) { context.lineWidth = strokeWidth; } context.lineCap = 'round'; // TODO: accept this as a symbolizer property context.lineJoin = 'round'; // TODO: accept this as a symbolizer property } if (fillColor) { context.fillStyle = fillColor; } /** * Four scenarios covered here: * 1) stroke only, no holes - only need to have a single path * 2) fill only, no holes - only need to have a single path * 3) fill and stroke, no holes * 4) holes - render polygon to sketch canvas first */ context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { continue; } geometry = feature.getGeometry(); if (geometry instanceof ol.geom.Polygon) { components = [geometry]; } else { goog.asserts.assert(geometry instanceof ol.geom.MultiPolygon, 'Expected MultiPolygon'); components = geometry.getComponents(); } for (j = 0, jj = components.length; j < jj; ++j) { poly = components[j]; rings = poly.getRings(); numRings = rings.length; if (numRings > 0) { // TODO: scenario 4 ring = rings[0]; for (k = 0, kk = ring.getCount(); k < kk; ++k) { vec = [ring.get(k, 0), ring.get(k, 1), 0]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); if (k === 0) { context.moveTo(vec[0], vec[1]); } else { context.lineTo(vec[0], vec[1]); } } if (fillColor && strokeColor) { // scenario 3 - fill and stroke each time if (fillOpacity !== globalAlpha) { goog.asserts.assertNumber(fillOpacity); context.globalAlpha = fillOpacity; globalAlpha = fillOpacity; } context.fill(); if (strokeOpacity !== globalAlpha) { goog.asserts.assertNumber(strokeOpacity); context.globalAlpha = strokeOpacity; globalAlpha = strokeOpacity; } context.stroke(); if (i < ii - 1 || j < jj - 1) { context.beginPath(); } } } } } if (!(fillColor && strokeColor)) { if (fillColor) { // scenario 2 - fill all at once if (fillOpacity !== globalAlpha) { goog.asserts.assertNumber(fillOpacity); context.globalAlpha = fillOpacity; globalAlpha = fillOpacity; } context.fill(); } else { // scenario 1 - stroke all at once if (strokeOpacity !== globalAlpha) { goog.asserts.assertNumber(strokeOpacity); context.globalAlpha = strokeOpacity; globalAlpha = strokeOpacity; } context.stroke(); } } }; /** * @param {ol.style.ShapeLiteral} circle Shape symbolizer. * @return {!HTMLCanvasElement} Canvas element. * @private */ ol.renderer.canvas.Vector.renderCircle_ = function(circle) { var strokeWidth = circle.strokeWidth || 0, size = circle.size + (2 * strokeWidth) + 1, mid = size / 2, canvas = /** @type {HTMLCanvasElement} */ (goog.dom.createElement(goog.dom.TagName.CANVAS)), context = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')), fillColor = circle.fillColor, strokeColor = circle.strokeColor, twoPi = Math.PI * 2; canvas.height = size; canvas.width = size; if (fillColor) { context.fillStyle = fillColor; } if (strokeColor) { context.lineWidth = strokeWidth; context.strokeStyle = strokeColor; context.lineCap = 'round'; // TODO: accept this as a symbolizer property context.lineJoin = 'round'; // TODO: accept this as a symbolizer property } context.beginPath(); context.arc(mid, mid, circle.size / 2, 0, twoPi, true); if (fillColor) { goog.asserts.assertNumber(circle.fillOpacity); context.globalAlpha = circle.fillOpacity; context.fill(); } if (strokeColor) { goog.asserts.assertNumber(circle.strokeOpacity); context.globalAlpha = circle.strokeOpacity; context.stroke(); } return canvas; }; /** * @param {ol.geom.Geometry} geometry Geometry. * @return {Array.} Renderable geometry vectors. */ ol.renderer.canvas.Vector.getLabelVectors = function(geometry) { if (geometry instanceof ol.geom.AbstractCollection) { var components = geometry.getComponents(); var numComponents = components.length; var result = []; for (var i = 0; i < numComponents; ++i) { result.push.apply(result, ol.renderer.canvas.Vector.getLabelVectors(components[i])); } return result; } var type = geometry.getType(); if (type == ol.geom.GeometryType.POINT) { return [[geometry.get(0), geometry.get(1), 0]]; } if (type == ol.geom.GeometryType.POLYGON) { var coordinates = geometry.getInteriorPoint(); return [[coordinates[0], coordinates[1], 0]]; } throw new Error('Label rendering not implemented for geometry type: ' + type); }; /** * @param {ol.style.ShapeLiteral} shape Shape symbolizer. * @return {!HTMLCanvasElement} Canvas element. */ ol.renderer.canvas.Vector.renderShape = function(shape) { var canvas; if (shape.type === ol.style.ShapeType.CIRCLE) { canvas = ol.renderer.canvas.Vector.renderCircle_(shape); } else { throw new Error('Unsupported shape type: ' + 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.Vector.renderIcon = function(icon, opt_callback) { var url = icon.url; var image = ol.renderer.canvas.Vector.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.Vector.handleIconError_, null, opt_callback), false, ol.renderer.canvas.Vector.renderIcon); goog.events.listenOnce(image, goog.events.EventType.LOAD, goog.bind(ol.renderer.canvas.Vector.handleIconLoad_, null, opt_callback), false, ol.renderer.canvas.Vector.renderIcon); image.setAttribute('src', url); } 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; image.width = width; } else if (goog.isDef(height)) { image.width = height / image.height * image.width; image.height = height; } } return deferred ? null : image; }; /** * @type {Object.} * @private */ ol.renderer.canvas.Vector.icons_ = {}; /** * @param {function()=} opt_callback Callback. * @param {Event=} opt_event Event. * @private */ ol.renderer.canvas.Vector.handleIconError_ = function(opt_callback, opt_event) { if (goog.isDef(opt_event)) { var url = opt_event.target.getAttribute('src'); ol.renderer.canvas.Vector.icons_[url] = null; ol.renderer.canvas.Vector.handleIconLoad_(opt_callback, opt_event); } }; /** * @param {function()=} opt_callback Callback. * @param {Event=} opt_event Event. * @private */ ol.renderer.canvas.Vector.handleIconLoad_ = function(opt_callback, opt_event) { if (goog.isDef(opt_event)) { var url = opt_event.target.getAttribute('src'); ol.renderer.canvas.Vector.icons_[url] = /** @type {HTMLImageElement} */ (opt_event.target); } if (goog.isDef(opt_callback)) { opt_callback(); } };