Files
openlayers/src/ol/renderer/canvas/canvasvectorrenderer.js
2013-10-04 23:36:16 -06:00

591 lines
18 KiB
JavaScript

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.<number, Array.<number>>}
* @private
*/
this.symbolSizes_ = {};
/**
* @type {Object.<number, Array.<number>>}
* @private
*/
this.symbolOffsets_ = {};
/**
* @type {Array.<number>}
* @private
*/
this.maxSymbolSize_ = [0, 0];
};
/**
* @return {Object.<number, Array.<number>>} Symbolizer sizes.
*/
ol.renderer.canvas.Vector.prototype.getSymbolSizes = function() {
return this.symbolSizes_;
};
/**
* @return {Object.<number, Array.<number>>} Symbolizer offsets.
*/
ol.renderer.canvas.Vector.prototype.getSymbolOffsets = function() {
return this.symbolOffsets_;
};
/**
* @return {Array.<number>} Maximum symbolizer size.
*/
ol.renderer.canvas.Vector.prototype.getMaxSymbolSize = function() {
return this.maxSymbolSize_;
};
/**
* @param {Array.<ol.Feature>} 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.<ol.Feature>} 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.<ol.Feature>} 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.<ol.Feature>} 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.<ol.Feature>} 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.<goog.vec.Vec3.AnyType>} 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.<string, HTMLImageElement>}
* @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();
}
};