896 lines
31 KiB
JavaScript
896 lines
31 KiB
JavaScript
/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
|
|
* full list of contributors). Published under the Clear BSD license.
|
|
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
|
|
* full text of the license. */
|
|
|
|
/**
|
|
* @requires OpenLayers/Renderer.js
|
|
*/
|
|
|
|
/**
|
|
* Class: OpenLayers.Renderer.Canvas
|
|
* A renderer based on the 2D 'canvas' drawing element.
|
|
*
|
|
* Inherits:
|
|
* - <OpenLayers.Renderer>
|
|
*/
|
|
OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
|
|
|
|
/**
|
|
* APIProperty: hitDetection
|
|
* {Boolean} Allow for hit detection of features. Default is true.
|
|
*/
|
|
hitDetection: true,
|
|
|
|
/**
|
|
* Property: hitOverflow
|
|
* {Number} The method for converting feature identifiers to color values
|
|
* supports 16777215 sequential values. Two features cannot be
|
|
* predictably detected if their identifiers differ by more than this
|
|
* value. The hitOverflow allows for bigger numbers (but the
|
|
* difference in values is still limited).
|
|
*/
|
|
hitOverflow: 0,
|
|
|
|
/**
|
|
* Property: canvas
|
|
* {Canvas} The canvas context object.
|
|
*/
|
|
canvas: null,
|
|
|
|
/**
|
|
* Property: features
|
|
* {Object} Internal object of feature/style pairs for use in redrawing the layer.
|
|
*/
|
|
features: null,
|
|
|
|
/**
|
|
* Property: pendingRedraw
|
|
* {Boolean} The renderer needs a redraw call to render features added while
|
|
* the renderer was locked.
|
|
*/
|
|
pendingRedraw: false,
|
|
|
|
/**
|
|
* Property: cachedSymbolBounds
|
|
* {Object} Internal cache of calculated symbol extents.
|
|
*/
|
|
cachedSymbolBounds: {},
|
|
|
|
/**
|
|
* Constructor: OpenLayers.Renderer.Canvas
|
|
*
|
|
* Parameters:
|
|
* containerID - {<String>}
|
|
* options - {Object} Optional properties to be set on the renderer.
|
|
*/
|
|
initialize: function(containerID, options) {
|
|
OpenLayers.Renderer.prototype.initialize.apply(this, arguments);
|
|
this.root = document.createElement("canvas");
|
|
this.container.appendChild(this.root);
|
|
this.canvas = this.root.getContext("2d");
|
|
this.features = {};
|
|
if (this.hitDetection) {
|
|
this.hitCanvas = document.createElement("canvas");
|
|
this.hitContext = this.hitCanvas.getContext("2d");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: setExtent
|
|
* Set the visible part of the layer.
|
|
*
|
|
* Parameters:
|
|
* extent - {<OpenLayers.Bounds>}
|
|
* resolutionChanged - {Boolean}
|
|
*
|
|
* Returns:
|
|
* {Boolean} true to notify the layer that the new extent does not exceed
|
|
* the coordinate range, and the features will not need to be redrawn.
|
|
* False otherwise.
|
|
*/
|
|
setExtent: function() {
|
|
OpenLayers.Renderer.prototype.setExtent.apply(this, arguments);
|
|
// always redraw features
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Method: eraseGeometry
|
|
* Erase a geometry from the renderer. Because the Canvas renderer has
|
|
* 'memory' of the features that it has drawn, we have to remove the
|
|
* feature so it doesn't redraw.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* featureId - {String}
|
|
*/
|
|
eraseGeometry: function(geometry, featureId) {
|
|
this.eraseFeatures(this.features[featureId][0]);
|
|
},
|
|
|
|
/**
|
|
* APIMethod: supported
|
|
*
|
|
* Returns:
|
|
* {Boolean} Whether or not the browser supports the renderer class
|
|
*/
|
|
supported: function() {
|
|
return OpenLayers.CANVAS_SUPPORTED;
|
|
},
|
|
|
|
/**
|
|
* Method: setSize
|
|
* Sets the size of the drawing surface.
|
|
*
|
|
* Once the size is updated, redraw the canvas.
|
|
*
|
|
* Parameters:
|
|
* size - {<OpenLayers.Size>}
|
|
*/
|
|
setSize: function(size) {
|
|
this.size = size.clone();
|
|
var root = this.root;
|
|
root.style.width = size.w + "px";
|
|
root.style.height = size.h + "px";
|
|
root.width = size.w;
|
|
root.height = size.h;
|
|
this.resolution = null;
|
|
if (this.hitDetection) {
|
|
var hitCanvas = this.hitCanvas;
|
|
hitCanvas.style.width = size.w + "px";
|
|
hitCanvas.style.height = size.h + "px";
|
|
hitCanvas.width = size.w;
|
|
hitCanvas.height = size.h;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawFeature
|
|
* Draw the feature. Stores the feature in the features list,
|
|
* then redraws the layer.
|
|
*
|
|
* Parameters:
|
|
* feature - {<OpenLayers.Feature.Vector>}
|
|
* style - {<Object>}
|
|
*
|
|
* Returns:
|
|
* {Boolean} The feature has been drawn completely. If the feature has no
|
|
* geometry, undefined will be returned. If the feature is not rendered
|
|
* for other reasons, false will be returned.
|
|
*/
|
|
drawFeature: function(feature, style) {
|
|
var rendered;
|
|
if (feature.geometry) {
|
|
style = this.applyDefaultSymbolizer(style || feature.style);
|
|
// don't render if display none or feature outside extent
|
|
var bounds = feature.geometry.getBounds();
|
|
|
|
var worldBounds;
|
|
if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) {
|
|
worldBounds = this.map.getMaxExtent();
|
|
}
|
|
|
|
var intersects = bounds && bounds.intersectsBounds(this.extent, {worldBounds: worldBounds});
|
|
|
|
rendered = (style.display !== "none") && !!bounds && intersects;
|
|
if (rendered) {
|
|
// keep track of what we have rendered for redraw
|
|
this.features[feature.id] = [feature, style];
|
|
}
|
|
else {
|
|
// remove from features tracked for redraw
|
|
delete(this.features[feature.id]);
|
|
}
|
|
this.pendingRedraw = true;
|
|
}
|
|
if (this.pendingRedraw && !this.locked) {
|
|
this.redraw();
|
|
this.pendingRedraw = false;
|
|
}
|
|
return rendered;
|
|
},
|
|
|
|
/**
|
|
* Method: drawGeometry
|
|
* Used when looping (in redraw) over the features; draws
|
|
* the canvas.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
*/
|
|
drawGeometry: function(geometry, style, featureId) {
|
|
var className = geometry.CLASS_NAME;
|
|
if ((className == "OpenLayers.Geometry.Collection") ||
|
|
(className == "OpenLayers.Geometry.MultiPoint") ||
|
|
(className == "OpenLayers.Geometry.MultiLineString") ||
|
|
(className == "OpenLayers.Geometry.MultiPolygon")) {
|
|
for (var i = 0; i < geometry.components.length; i++) {
|
|
this.drawGeometry(geometry.components[i], style, featureId);
|
|
}
|
|
return;
|
|
}
|
|
switch (geometry.CLASS_NAME) {
|
|
case "OpenLayers.Geometry.Point":
|
|
this.drawPoint(geometry, style, featureId);
|
|
break;
|
|
case "OpenLayers.Geometry.LineString":
|
|
this.drawLineString(geometry, style, featureId);
|
|
break;
|
|
case "OpenLayers.Geometry.LinearRing":
|
|
this.drawLinearRing(geometry, style, featureId);
|
|
break;
|
|
case "OpenLayers.Geometry.Polygon":
|
|
this.drawPolygon(geometry, style, featureId);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawExternalGraphic
|
|
* Called to draw External graphics.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawExternalGraphic: function(geometry, style, featureId) {
|
|
var img = new Image();
|
|
|
|
if (style.graphicTitle) {
|
|
img.title = style.graphicTitle;
|
|
}
|
|
|
|
var width = style.graphicWidth || style.graphicHeight;
|
|
var height = style.graphicHeight || style.graphicWidth;
|
|
width = width ? width : style.pointRadius * 2;
|
|
height = height ? height : style.pointRadius * 2;
|
|
var xOffset = (style.graphicXOffset != undefined) ?
|
|
style.graphicXOffset : -(0.5 * width);
|
|
var yOffset = (style.graphicYOffset != undefined) ?
|
|
style.graphicYOffset : -(0.5 * height);
|
|
|
|
var opacity = style.graphicOpacity || style.fillOpacity;
|
|
|
|
var onLoad = function() {
|
|
if(!this.features[featureId]) {
|
|
return;
|
|
}
|
|
var pt = this.getLocalXY(geometry);
|
|
var p0 = pt[0];
|
|
var p1 = pt[1];
|
|
if(!isNaN(p0) && !isNaN(p1)) {
|
|
var x = (p0 + xOffset) | 0;
|
|
var y = (p1 + yOffset) | 0;
|
|
var canvas = this.canvas;
|
|
canvas.globalAlpha = opacity;
|
|
var factor = OpenLayers.Renderer.Canvas.drawImageScaleFactor ||
|
|
(OpenLayers.Renderer.Canvas.drawImageScaleFactor =
|
|
/android 2.1/.test(navigator.userAgent.toLowerCase()) ?
|
|
// 320 is the screen width of the G1 phone, for
|
|
// which drawImage works out of the box.
|
|
320 / window.screen.width : 1
|
|
);
|
|
canvas.drawImage(
|
|
img, x*factor, y*factor, width*factor, height*factor
|
|
);
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("fill", featureId);
|
|
this.hitContext.fillRect(x, y, width, height);
|
|
}
|
|
}
|
|
};
|
|
|
|
img.onload = OpenLayers.Function.bind(onLoad, this);
|
|
img.src = style.externalGraphic;
|
|
},
|
|
|
|
/**
|
|
* Method: drawNamedSymbol
|
|
* Called to draw Well Known Graphic Symbol Name.
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawNamedSymbol: function(geometry, style, featureId) {
|
|
var x, y, cx, cy, i, symbolBounds, scaling, angle;
|
|
var unscaledStrokeWidth;
|
|
var deg2rad = Math.PI / 180.0;
|
|
|
|
var symbol = OpenLayers.Renderer.symbol[style.graphicName];
|
|
|
|
if (!symbol) {
|
|
throw new Error(style.graphicName + ' is not a valid symbol name');
|
|
}
|
|
|
|
if (!symbol.length || symbol.length < 2) return;
|
|
|
|
var pt = this.getLocalXY(geometry);
|
|
var p0 = pt[0];
|
|
var p1 = pt[1];
|
|
|
|
if (isNaN(p0) || isNaN(p1)) return;
|
|
|
|
// Use rounded line caps
|
|
this.canvas.lineCap = "round";
|
|
this.canvas.lineJoin = "round";
|
|
|
|
// Scale and rotate symbols, using precalculated bounds whenever possible.
|
|
if (style.graphicName in this.cachedSymbolBounds) {
|
|
symbolBounds = this.cachedSymbolBounds[style.graphicName];
|
|
} else {
|
|
symbolBounds = new OpenLayers.Bounds();
|
|
for(i = 0; i < symbol.length; i+=2) {
|
|
symbolBounds.extend(new OpenLayers.LonLat(symbol[i], symbol[i+1]));
|
|
}
|
|
this.cachedSymbolBounds[style.graphicName] = symbolBounds;
|
|
}
|
|
|
|
// Push symbol scaling, translation and rotation onto the transformation stack in reverse order.
|
|
// Don't forget to apply all canvas transformations to the hitContext canvas as well(!)
|
|
this.canvas.save();
|
|
if (this.hitDetection) this.hitContext.save();
|
|
|
|
// Step 3: place symbol at the desired location
|
|
this.canvas.translate(p0,p1);
|
|
if (this.hitDetection) this.hitContext.translate(p0,p1);
|
|
|
|
// Step 2a. rotate the symbol if necessary
|
|
angle = deg2rad * style.rotation; // will be NaN when style.rotation is undefined.
|
|
if (!isNaN(angle)) {
|
|
this.canvas.rotate(angle);
|
|
if (this.hitDetection) this.hitContext.rotate(angle);
|
|
}
|
|
|
|
// // Step 2: scale symbol such that pointRadius equals half the maximum symbol dimension.
|
|
scaling = 2.0 * style.pointRadius / Math.max(symbolBounds.getWidth(), symbolBounds.getHeight());
|
|
this.canvas.scale(scaling,scaling);
|
|
if (this.hitDetection) this.hitContext.scale(scaling,scaling);
|
|
|
|
// Step 1: center the symbol at the origin
|
|
cx = symbolBounds.getCenterLonLat().lon;
|
|
cy = symbolBounds.getCenterLonLat().lat;
|
|
this.canvas.translate(-cx,-cy);
|
|
if (this.hitDetection) this.hitContext.translate(-cx,-cy);
|
|
|
|
// Don't forget to scale stroke widths, because they are affected by canvas scale transformations as well(!)
|
|
// Alternative: scale symbol coordinates manually, so stroke width scaling is not needed anymore.
|
|
unscaledStrokeWidth = style.strokeWidth;
|
|
style.strokeWidth = unscaledStrokeWidth / scaling;
|
|
|
|
if (style.fill !== false) {
|
|
this.setCanvasStyle("fill", style);
|
|
this.canvas.beginPath();
|
|
for (i=0; i<symbol.length; i=i+2) {
|
|
x = symbol[i];
|
|
y = symbol[i+1];
|
|
if (i == 0) this.canvas.moveTo(x,y);
|
|
this.canvas.lineTo(x,y);
|
|
}
|
|
this.canvas.closePath();
|
|
this.canvas.fill();
|
|
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("fill", featureId, style);
|
|
this.hitContext.beginPath();
|
|
for (i=0; i<symbol.length; i=i+2) {
|
|
x = symbol[i];
|
|
y = symbol[i+1];
|
|
if (i == 0) this.canvas.moveTo(x,y);
|
|
this.hitContext.lineTo(x,y);
|
|
}
|
|
this.hitContext.closePath();
|
|
this.hitContext.fill();
|
|
}
|
|
}
|
|
|
|
if (style.stroke !== false) {
|
|
this.setCanvasStyle("stroke", style);
|
|
|
|
this.canvas.beginPath();
|
|
|
|
for (i=0; i<symbol.length; i=i+2) {
|
|
x = symbol[i];
|
|
y = symbol[i+1];
|
|
if (i == 0) this.canvas.moveTo(x,y);
|
|
this.canvas.lineTo(x,y);
|
|
}
|
|
this.canvas.closePath();
|
|
this.canvas.stroke();
|
|
}
|
|
|
|
style.strokeWidth = unscaledStrokeWidth;
|
|
this.canvas.restore();
|
|
if (this.hitDetection) this.hitContext.restore();
|
|
this.setCanvasStyle("reset");
|
|
},
|
|
|
|
/**
|
|
* Method: setCanvasStyle
|
|
* Prepare the canvas for drawing by setting various global settings.
|
|
*
|
|
* Parameters:
|
|
* type - {String} one of 'stroke', 'fill', or 'reset'
|
|
* style - {Object} Symbolizer hash
|
|
*/
|
|
setCanvasStyle: function(type, style) {
|
|
if (type === "fill") {
|
|
this.canvas.globalAlpha = style['fillOpacity'];
|
|
this.canvas.fillStyle = style['fillColor'];
|
|
} else if (type === "stroke") {
|
|
this.canvas.globalAlpha = style['strokeOpacity'];
|
|
this.canvas.strokeStyle = style['strokeColor'];
|
|
this.canvas.lineWidth = style['strokeWidth'];
|
|
} else {
|
|
this.canvas.globalAlpha = 0;
|
|
this.canvas.lineWidth = 1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: setCanvasStyle
|
|
* Prepare the canvas for drawing by setting various global settings.
|
|
*
|
|
* Parameters:
|
|
* type - {String} one of 'stroke', 'fill', or 'reset'
|
|
* style - {Object} Symbolizer hash
|
|
*/
|
|
setCanvasStyle: function(type, style) {
|
|
if (type === "fill") {
|
|
this.canvas.globalAlpha = style['fillOpacity'];
|
|
this.canvas.fillStyle = style['fillColor'];
|
|
} else if (type === "stroke") {
|
|
this.canvas.globalAlpha = style['strokeOpacity'];
|
|
this.canvas.strokeStyle = style['strokeColor'];
|
|
this.canvas.lineWidth = style['strokeWidth'];
|
|
} else {
|
|
this.canvas.globalAlpha = 0;
|
|
this.canvas.lineWidth = 1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: featureIdToHex
|
|
* Convert a feature ID string into an RGB hex string.
|
|
*
|
|
* Parameters:
|
|
* featureId - {String} Feature id
|
|
*
|
|
* Returns:
|
|
* {String} RGB hex string.
|
|
*/
|
|
featureIdToHex: function(featureId) {
|
|
var id = Number(featureId.split("_").pop()) + 1; // zero for no feature
|
|
if (id >= 16777216) {
|
|
this.hitOverflow = id - 16777215;
|
|
id = id % 16777216 + 1;
|
|
}
|
|
var hex = "000000" + id.toString(16);
|
|
var len = hex.length;
|
|
hex = "#" + hex.substring(len-6, len);
|
|
return hex;
|
|
},
|
|
|
|
/**
|
|
* Method: setHitContextStyle
|
|
* Prepare the hit canvas for drawing by setting various global settings.
|
|
*
|
|
* Parameters:
|
|
* type - {String} one of 'stroke', 'fill', or 'reset'
|
|
* featureId - {String} The feature id.
|
|
* symbolizer - {<OpenLayers.Symbolizer>} The symbolizer.
|
|
*/
|
|
setHitContextStyle: function(type, featureId, symbolizer) {
|
|
var hex = this.featureIdToHex(featureId);
|
|
if (type == "fill") {
|
|
this.hitContext.globalAlpha = 1.0;
|
|
this.hitContext.fillStyle = hex;
|
|
} else if (type == "stroke") {
|
|
this.hitContext.globalAlpha = 1.0;
|
|
this.hitContext.strokeStyle = hex;
|
|
// bump up stroke width to deal with antialiasing
|
|
this.hitContext.lineWidth = symbolizer.strokeWidth + 2;
|
|
} else {
|
|
this.hitContext.globalAlpha = 0;
|
|
this.hitContext.lineWidth = 1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawPoint
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawPoint: function(geometry, style, featureId) {
|
|
if(style.graphic !== false) {
|
|
if(style.externalGraphic) {
|
|
this.drawExternalGraphic(geometry, style, featureId);
|
|
} else if (style.graphicName && (style.graphicName != "circle")) {
|
|
this.drawNamedSymbol(geometry, style, featureId);
|
|
} else {
|
|
var pt = this.getLocalXY(geometry);
|
|
var p0 = pt[0];
|
|
var p1 = pt[1];
|
|
if(!isNaN(p0) && !isNaN(p1)) {
|
|
var twoPi = Math.PI*2;
|
|
var radius = style.pointRadius;
|
|
if(style.fill !== false) {
|
|
this.setCanvasStyle("fill", style);
|
|
this.canvas.beginPath();
|
|
this.canvas.arc(p0, p1, radius, 0, twoPi, true);
|
|
this.canvas.fill();
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("fill", featureId, style);
|
|
this.hitContext.beginPath();
|
|
this.hitContext.arc(p0, p1, radius, 0, twoPi, true);
|
|
this.hitContext.fill();
|
|
}
|
|
}
|
|
|
|
if(style.stroke !== false) {
|
|
this.setCanvasStyle("stroke", style);
|
|
this.canvas.beginPath();
|
|
this.canvas.arc(p0, p1, radius, 0, twoPi, true);
|
|
this.canvas.stroke();
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("stroke", featureId, style);
|
|
this.hitContext.beginPath();
|
|
this.hitContext.arc(p0, p1, radius, 0, twoPi, true);
|
|
this.hitContext.stroke();
|
|
}
|
|
this.setCanvasStyle("reset");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawLineString
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawLineString: function(geometry, style, featureId) {
|
|
style = OpenLayers.Util.applyDefaults({fill: false}, style);
|
|
this.drawLinearRing(geometry, style, featureId);
|
|
},
|
|
|
|
/**
|
|
* Method: drawLinearRing
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawLinearRing: function(geometry, style, featureId) {
|
|
if (style.fill !== false) {
|
|
this.setCanvasStyle("fill", style);
|
|
this.renderPath(this.canvas, geometry, style, featureId, "fill");
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("fill", featureId, style);
|
|
this.renderPath(this.hitContext, geometry, style, featureId, "fill");
|
|
}
|
|
}
|
|
if (style.stroke !== false) {
|
|
this.setCanvasStyle("stroke", style);
|
|
this.renderPath(this.canvas, geometry, style, featureId, "stroke");
|
|
if (this.hitDetection) {
|
|
this.setHitContextStyle("stroke", featureId, style);
|
|
this.renderPath(this.hitContext, geometry, style, featureId, "stroke");
|
|
}
|
|
}
|
|
this.setCanvasStyle("reset");
|
|
},
|
|
|
|
/**
|
|
* Method: renderPath
|
|
* Render a path with stroke and optional fill.
|
|
*/
|
|
renderPath: function(context, geometry, style, featureId, type) {
|
|
var components = geometry.components;
|
|
var len = components.length;
|
|
context.beginPath();
|
|
var start = this.getLocalXY(components[0]);
|
|
var x = start[0];
|
|
var y = start[1];
|
|
if (!isNaN(x) && !isNaN(y)) {
|
|
context.moveTo(start[0], start[1]);
|
|
for (var i=1; i<len; ++i) {
|
|
var pt = this.getLocalXY(components[i]);
|
|
context.lineTo(pt[0], pt[1]);
|
|
}
|
|
if (type === "fill") {
|
|
context.fill();
|
|
} else {
|
|
context.stroke();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawPolygon
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
* style - {Object}
|
|
* featureId - {String}
|
|
*/
|
|
drawPolygon: function(geometry, style, featureId) {
|
|
var components = geometry.components;
|
|
var len = components.length;
|
|
this.drawLinearRing(components[0], style, featureId);
|
|
// erase inner rings
|
|
for (var i=1; i<len; ++i) {
|
|
/**
|
|
* Note that this is overly agressive. Here we punch holes through
|
|
* all previously rendered features on the same canvas. A better
|
|
* solution for polygons with interior rings would be to draw the
|
|
* polygon on a sketch canvas first. We could erase all holes
|
|
* there and then copy the drawing to the layer canvas.
|
|
* TODO: http://trac.osgeo.org/openlayers/ticket/3130
|
|
*/
|
|
this.canvas.globalCompositeOperation = "destination-out";
|
|
if (this.hitDetection) {
|
|
this.hitContext.globalCompositeOperation = "destination-out";
|
|
}
|
|
this.drawLinearRing(
|
|
components[i],
|
|
OpenLayers.Util.applyDefaults({stroke: false, fillOpacity: 1.0}, style),
|
|
featureId
|
|
);
|
|
this.canvas.globalCompositeOperation = "source-over";
|
|
if (this.hitDetection) {
|
|
this.hitContext.globalCompositeOperation = "source-over";
|
|
}
|
|
this.drawLinearRing(
|
|
components[i],
|
|
OpenLayers.Util.applyDefaults({fill: false}, style),
|
|
featureId
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawText
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* location - {<OpenLayers.Point>}
|
|
* style - {Object}
|
|
*/
|
|
drawText: function(location, style) {
|
|
var pt = this.getLocalXY(location);
|
|
|
|
this.setCanvasStyle("reset");
|
|
this.canvas.fillStyle = style.fontColor;
|
|
this.canvas.globalAlpha = style.fontOpacity || 1.0;
|
|
var fontStyle = [style.fontStyle ? style.fontStyle : "normal",
|
|
"normal", // "font-variant" not supported
|
|
style.fontWeight ? style.fontWeight : "normal",
|
|
style.fontSize ? style.fontSize : "1em",
|
|
style.fontFamily ? style.fontFamily : "sans-serif"].join(" ");
|
|
var labelRows = style.label.split('\n');
|
|
var numRows = labelRows.length;
|
|
if (this.canvas.fillText) {
|
|
// HTML5
|
|
this.canvas.font = fontStyle;
|
|
this.canvas.textAlign =
|
|
OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[0]] ||
|
|
"center";
|
|
this.canvas.textBaseline =
|
|
OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[1]] ||
|
|
"middle";
|
|
var vfactor =
|
|
OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]];
|
|
if (vfactor == null) {
|
|
vfactor = -.5;
|
|
}
|
|
var lineHeight =
|
|
this.canvas.measureText('Mg').height ||
|
|
this.canvas.measureText('xx').width;
|
|
pt[1] += lineHeight*vfactor*(numRows-1);
|
|
for (var i = 0; i < numRows; i++) {
|
|
if (style.labelOutlineWidth) {
|
|
this.canvas.save();
|
|
this.canvas.strokeStyle = style.labelOutlineColor;
|
|
this.canvas.lineWidth = style.labelOutlineWidth;
|
|
this.canvas.strokeText(labelRows[i], pt[0], pt[1] + (lineHeight*i) + 1);
|
|
this.canvas.restore();
|
|
}
|
|
this.canvas.fillText(labelRows[i], pt[0], pt[1] + (lineHeight*i));
|
|
}
|
|
} else if (this.canvas.mozDrawText) {
|
|
// Mozilla pre-Gecko1.9.1 (<FF3.1)
|
|
this.canvas.mozTextStyle = fontStyle;
|
|
// No built-in text alignment, so we measure and adjust the position
|
|
var hfactor =
|
|
OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[0]];
|
|
if (hfactor == null) {
|
|
hfactor = -.5;
|
|
}
|
|
var vfactor =
|
|
OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]];
|
|
if (vfactor == null) {
|
|
vfactor = -.5;
|
|
}
|
|
var lineHeight = this.canvas.mozMeasureText('xx');
|
|
pt[1] += lineHeight*(1 + (vfactor*numRows));
|
|
for (var i = 0; i < numRows; i++) {
|
|
var x = pt[0] + (hfactor*this.canvas.mozMeasureText(labelRows[i]));
|
|
var y = pt[1] + (i*lineHeight);
|
|
this.canvas.translate(x, y);
|
|
this.canvas.mozDrawText(labelRows[i]);
|
|
this.canvas.translate(-x, -y);
|
|
}
|
|
}
|
|
this.setCanvasStyle("reset");
|
|
},
|
|
|
|
/**
|
|
* Method: getLocalXY
|
|
* transform geographic xy into pixel xy
|
|
*
|
|
* Parameters:
|
|
* point - {<OpenLayers.Geometry.Point>}
|
|
*/
|
|
getLocalXY: function(point) {
|
|
var resolution = this.getResolution();
|
|
var extent = this.extent;
|
|
var x = ((point.x - this.featureDx) / resolution + (-extent.left / resolution));
|
|
var y = ((extent.top / resolution) - point.y / resolution);
|
|
return [x, y];
|
|
},
|
|
|
|
/**
|
|
* Method: clear
|
|
* Clear all vectors from the renderer.
|
|
*/
|
|
clear: function() {
|
|
var height = this.root.height;
|
|
var width = this.root.width;
|
|
this.canvas.clearRect(0, 0, width, height);
|
|
this.features = {};
|
|
if (this.hitDetection) {
|
|
this.hitContext.clearRect(0, 0, width, height);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: getFeatureIdFromEvent
|
|
* Returns a feature id from an event on the renderer.
|
|
*
|
|
* Parameters:
|
|
* evt - {<OpenLayers.Event>}
|
|
*
|
|
* Returns:
|
|
* {<OpenLayers.Feature.Vector} A feature or undefined. This method returns a
|
|
* feature instead of a feature id to avoid an unnecessary lookup on the
|
|
* layer.
|
|
*/
|
|
getFeatureIdFromEvent: function(evt) {
|
|
var feature;
|
|
if (this.hitDetection) {
|
|
// this dragging check should go in the feature handler
|
|
if (!this.map.dragging) {
|
|
var xy = evt.xy;
|
|
var x = xy.x | 0;
|
|
var y = xy.y | 0;
|
|
var data = this.hitContext.getImageData(x, y, 1, 1).data;
|
|
if (data[3] === 255) { // antialiased
|
|
var id = data[2] + (256 * (data[1] + (256 * data[0])));
|
|
if (id) {
|
|
feature = this.features["OpenLayers.Feature.Vector_" + (id - 1 + this.hitOverflow)][0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return feature;
|
|
},
|
|
|
|
/**
|
|
* Method: eraseFeatures
|
|
* This is called by the layer to erase features; removes the feature from
|
|
* the list, then redraws the layer.
|
|
*
|
|
* Parameters:
|
|
* features - {Array(<OpenLayers.Feature.Vector>)}
|
|
*/
|
|
eraseFeatures: function(features) {
|
|
if(!(OpenLayers.Util.isArray(features))) {
|
|
features = [features];
|
|
}
|
|
for(var i=0; i<features.length; ++i) {
|
|
delete this.features[features[i].id];
|
|
}
|
|
this.redraw();
|
|
},
|
|
|
|
/**
|
|
* Method: redraw
|
|
* The real 'meat' of the function: any time things have changed,
|
|
* redraw() can be called to loop over all the data and (you guessed
|
|
* it) redraw it. Unlike Elements-based Renderers, we can't interact
|
|
* with things once they're drawn, to remove them, for example, so
|
|
* instead we have to just clear everything and draw from scratch.
|
|
*/
|
|
redraw: function() {
|
|
if (!this.locked) {
|
|
var height = this.root.height;
|
|
var width = this.root.width;
|
|
this.canvas.clearRect(0, 0, width, height);
|
|
if (this.hitDetection) {
|
|
this.hitContext.clearRect(0, 0, width, height);
|
|
}
|
|
var labelMap = [];
|
|
var feature, geometry, style;
|
|
var worldBounds = (this.map.baseLayer && this.map.baseLayer.wrapDateLine) && this.map.getMaxExtent();
|
|
for (var id in this.features) {
|
|
if (!this.features.hasOwnProperty(id)) { continue; }
|
|
feature = this.features[id][0];
|
|
geometry = feature.geometry;
|
|
this.calculateFeatureDx(geometry.getBounds(), worldBounds);
|
|
style = this.features[id][1];
|
|
this.drawGeometry(geometry, style, feature.id);
|
|
if(style.label) {
|
|
labelMap.push([feature, style]);
|
|
}
|
|
}
|
|
var item;
|
|
for (var i=0, len=labelMap.length; i<len; ++i) {
|
|
item = labelMap[i];
|
|
this.drawText(item[0].geometry.getCentroid(), item[1]);
|
|
}
|
|
}
|
|
},
|
|
|
|
CLASS_NAME: "OpenLayers.Renderer.Canvas"
|
|
});
|
|
|
|
/**
|
|
* Constant: OpenLayers.Renderer.Canvas.LABEL_ALIGN
|
|
* {Object}
|
|
*/
|
|
OpenLayers.Renderer.Canvas.LABEL_ALIGN = {
|
|
"l": "left",
|
|
"r": "right",
|
|
"t": "top",
|
|
"b": "bottom"
|
|
};
|
|
|
|
/**
|
|
* Constant: OpenLayers.Renderer.Canvas.LABEL_FACTOR
|
|
* {Object}
|
|
*/
|
|
OpenLayers.Renderer.Canvas.LABEL_FACTOR = {
|
|
"l": 0,
|
|
"r": -1,
|
|
"t": 0,
|
|
"b": -1
|
|
};
|
|
|
|
/**
|
|
* Constant: OpenLayers.Renderer.Canvas.drawImageScaleFactor
|
|
* {Number} Scale factor to apply to the canvas drawImage arguments. This
|
|
* is always 1 except for Android 2.1 devices, to work around
|
|
* http://code.google.com/p/android/issues/detail?id=5141.
|
|
*/
|
|
OpenLayers.Renderer.Canvas.drawImageScaleFactor = null;
|