openlayers
Version:
Build tools and sources for developing OpenLayers based mapping applications
689 lines (637 loc) • 24.2 kB
JavaScript
goog.provide('ol.render.canvas.Replay');
goog.require('ol');
goog.require('ol.array');
goog.require('ol.colorlike');
goog.require('ol.extent');
goog.require('ol.extent.Relationship');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.obj');
goog.require('ol.render.VectorContext');
goog.require('ol.render.canvas');
goog.require('ol.render.canvas.Instruction');
goog.require('ol.transform');
/**
* @constructor
* @extends {ol.render.VectorContext}
* @param {number} tolerance Tolerance.
* @param {ol.Extent} maxExtent Maximum extent.
* @param {number} resolution Resolution.
* @param {boolean} overlaps The replay can have overlapping geometries.
* @struct
*/
ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, overlaps) {
ol.render.VectorContext.call(this);
/**
* @protected
* @type {number}
*/
this.tolerance = tolerance;
/**
* @protected
* @const
* @type {ol.Extent}
*/
this.maxExtent = maxExtent;
/**
* @protected
* @type {boolean}
*/
this.overlaps = overlaps;
/**
* @protected
* @type {number}
*/
this.maxLineWidth = 0;
/**
* @protected
* @const
* @type {number}
*/
this.resolution = resolution;
/**
* @private
* @type {ol.Coordinate}
*/
this.fillOrigin_;
/**
* @private
* @type {Array.<*>}
*/
this.beginGeometryInstruction1_ = null;
/**
* @private
* @type {Array.<*>}
*/
this.beginGeometryInstruction2_ = null;
/**
* @protected
* @type {Array.<*>}
*/
this.instructions = [];
/**
* @protected
* @type {Array.<number>}
*/
this.coordinates = [];
/**
* @private
* @type {ol.Transform}
*/
this.renderedTransform_ = ol.transform.create();
/**
* @protected
* @type {Array.<*>}
*/
this.hitDetectionInstructions = [];
/**
* @private
* @type {Array.<number>}
*/
this.pixelCoordinates_ = [];
/**
* @private
* @type {ol.Transform}
*/
this.tmpLocalTransform_ = ol.transform.create();
/**
* @private
* @type {ol.Transform}
*/
this.resetTransform_ = ol.transform.create();
};
ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);
/**
* @param {Array.<number>} flatCoordinates Flat coordinates.
* @param {number} offset Offset.
* @param {number} end End.
* @param {number} stride Stride.
* @param {boolean} closed Last input coordinate equals first.
* @param {boolean} skipFirst Skip first coordinate.
* @protected
* @return {number} My end.
*/
ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, closed, skipFirst) {
var myEnd = this.coordinates.length;
var extent = this.getBufferedMaxExtent();
if (skipFirst) {
offset += stride;
}
var lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]];
var nextCoord = [NaN, NaN];
var skipped = true;
var i, lastRel, nextRel;
for (i = offset + stride; i < end; i += stride) {
nextCoord[0] = flatCoordinates[i];
nextCoord[1] = flatCoordinates[i + 1];
nextRel = ol.extent.coordinateRelationship(extent, nextCoord);
if (nextRel !== lastRel) {
if (skipped) {
this.coordinates[myEnd++] = lastCoord[0];
this.coordinates[myEnd++] = lastCoord[1];
}
this.coordinates[myEnd++] = nextCoord[0];
this.coordinates[myEnd++] = nextCoord[1];
skipped = false;
} else if (nextRel === ol.extent.Relationship.INTERSECTING) {
this.coordinates[myEnd++] = nextCoord[0];
this.coordinates[myEnd++] = nextCoord[1];
skipped = false;
} else {
skipped = true;
}
lastCoord[0] = nextCoord[0];
lastCoord[1] = nextCoord[1];
lastRel = nextRel;
}
// Last coordinate equals first or only one point to append:
if ((closed && skipped) || i === offset + stride) {
this.coordinates[myEnd++] = lastCoord[0];
this.coordinates[myEnd++] = lastCoord[1];
}
return myEnd;
};
/**
* @protected
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
* @param {ol.Feature|ol.render.Feature} feature Feature.
*/
ol.render.canvas.Replay.prototype.beginGeometry = function(geometry, feature) {
this.beginGeometryInstruction1_ =
[ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
this.instructions.push(this.beginGeometryInstruction1_);
this.beginGeometryInstruction2_ =
[ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
};
/**
* @private
* @param {CanvasRenderingContext2D} context Context.
* @param {number} rotation Rotation.
*/
ol.render.canvas.Replay.prototype.fill_ = function(context, rotation) {
if (this.fillOrigin_) {
var origin = ol.transform.apply(this.renderedTransform_, this.fillOrigin_.slice());
context.translate(origin[0], origin[1]);
context.rotate(rotation);
}
context.fill();
if (this.fillOrigin_) {
context.setTransform.apply(context, this.resetTransform_);
}
};
/**
* @private
* @param {CanvasRenderingContext2D} context Context.
* @param {number} pixelRatio Pixel ratio.
* @param {ol.Transform} transform Transform.
* @param {number} viewRotation View rotation.
* @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
* to skip.
* @param {Array.<*>} instructions Instructions array.
* @param {function((ol.Feature|ol.render.Feature)): T|undefined}
* featureCallback Feature callback.
* @param {ol.Extent=} opt_hitExtent Only check features that intersect this
* extent.
* @return {T|undefined} Callback result.
* @template T
*/
ol.render.canvas.Replay.prototype.replay_ = function(
context, pixelRatio, transform, viewRotation, skippedFeaturesHash,
instructions, featureCallback, opt_hitExtent) {
/** @type {Array.<number>} */
var pixelCoordinates;
if (ol.array.equals(transform, this.renderedTransform_)) {
pixelCoordinates = this.pixelCoordinates_;
} else {
pixelCoordinates = ol.geom.flat.transform.transform2D(
this.coordinates, 0, this.coordinates.length, 2,
transform, this.pixelCoordinates_);
ol.transform.setFromArray(this.renderedTransform_, transform);
ol.DEBUG && console.assert(pixelCoordinates === this.pixelCoordinates_,
'pixelCoordinates should be the same as this.pixelCoordinates_');
}
var skipFeatures = !ol.obj.isEmpty(skippedFeaturesHash);
var i = 0; // instruction index
var ii = instructions.length; // end of instructions
var d = 0; // data index
var dd; // end of per-instruction data
var localTransform = this.tmpLocalTransform_;
var resetTransform = this.resetTransform_;
var prevX, prevY, roundX, roundY;
var pendingFill = 0;
var pendingStroke = 0;
// When the batch size gets too big, performance decreases. 200 is a good
// balance between batch size and number of fill/stroke instructions.
var batchSize =
this.instructions != instructions || this.overlaps ? 0 : 200;
while (i < ii) {
var instruction = instructions[i];
var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
var feature, fill, stroke, text, x, y;
switch (type) {
case ol.render.canvas.Instruction.BEGIN_GEOMETRY:
feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
if ((skipFeatures &&
skippedFeaturesHash[ol.getUid(feature).toString()]) ||
!feature.getGeometry()) {
i = /** @type {number} */ (instruction[2]);
} else if (opt_hitExtent !== undefined && !ol.extent.intersects(
opt_hitExtent, feature.getGeometry().getExtent())) {
i = /** @type {number} */ (instruction[2]) + 1;
} else {
++i;
}
break;
case ol.render.canvas.Instruction.BEGIN_PATH:
if (pendingFill > batchSize) {
this.fill_(context, viewRotation);
pendingFill = 0;
}
if (pendingStroke > batchSize) {
context.stroke();
pendingStroke = 0;
}
if (!pendingFill && !pendingStroke) {
context.beginPath();
}
++i;
break;
case ol.render.canvas.Instruction.CIRCLE:
ol.DEBUG && console.assert(typeof instruction[1] === 'number',
'second instruction should be a number');
d = /** @type {number} */ (instruction[1]);
var x1 = pixelCoordinates[d];
var y1 = pixelCoordinates[d + 1];
var x2 = pixelCoordinates[d + 2];
var y2 = pixelCoordinates[d + 3];
var dx = x2 - x1;
var dy = y2 - y1;
var r = Math.sqrt(dx * dx + dy * dy);
context.moveTo(x1 + r, y1);
context.arc(x1, y1, r, 0, 2 * Math.PI, true);
++i;
break;
case ol.render.canvas.Instruction.CLOSE_PATH:
context.closePath();
++i;
break;
case ol.render.canvas.Instruction.DRAW_IMAGE:
ol.DEBUG && console.assert(typeof instruction[1] === 'number',
'second instruction should be a number');
d = /** @type {number} */ (instruction[1]);
ol.DEBUG && console.assert(typeof instruction[2] === 'number',
'third instruction should be a number');
dd = /** @type {number} */ (instruction[2]);
var image = /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */
(instruction[3]);
// Remaining arguments in DRAW_IMAGE are in alphabetical order
var anchorX = /** @type {number} */ (instruction[4]) * pixelRatio;
var anchorY = /** @type {number} */ (instruction[5]) * pixelRatio;
var height = /** @type {number} */ (instruction[6]);
var opacity = /** @type {number} */ (instruction[7]);
var originX = /** @type {number} */ (instruction[8]);
var originY = /** @type {number} */ (instruction[9]);
var rotateWithView = /** @type {boolean} */ (instruction[10]);
var rotation = /** @type {number} */ (instruction[11]);
var scale = /** @type {number} */ (instruction[12]);
var snapToPixel = /** @type {boolean} */ (instruction[13]);
var width = /** @type {number} */ (instruction[14]);
if (rotateWithView) {
rotation += viewRotation;
}
for (; d < dd; d += 2) {
x = pixelCoordinates[d] - anchorX;
y = pixelCoordinates[d + 1] - anchorY;
if (snapToPixel) {
x = Math.round(x);
y = Math.round(y);
}
if (scale != 1 || rotation !== 0) {
var centerX = x + anchorX;
var centerY = y + anchorY;
ol.transform.compose(localTransform,
centerX, centerY, scale, scale, rotation, -centerX, -centerY);
context.setTransform.apply(context, localTransform);
}
var alpha = context.globalAlpha;
if (opacity != 1) {
context.globalAlpha = alpha * opacity;
}
var w = (width + originX > image.width) ? image.width - originX : width;
var h = (height + originY > image.height) ? image.height - originY : height;
context.drawImage(image, originX, originY, w, h,
x, y, w * pixelRatio, h * pixelRatio);
if (opacity != 1) {
context.globalAlpha = alpha;
}
if (scale != 1 || rotation !== 0) {
context.setTransform.apply(context, resetTransform);
}
}
++i;
break;
case ol.render.canvas.Instruction.DRAW_TEXT:
ol.DEBUG && console.assert(typeof instruction[1] === 'number',
'2nd instruction should be a number');
d = /** @type {number} */ (instruction[1]);
ol.DEBUG && console.assert(typeof instruction[2] === 'number',
'3rd instruction should be a number');
dd = /** @type {number} */ (instruction[2]);
ol.DEBUG && console.assert(typeof instruction[3] === 'string',
'4th instruction should be a string');
text = /** @type {string} */ (instruction[3]);
ol.DEBUG && console.assert(typeof instruction[4] === 'number',
'5th instruction should be a number');
var offsetX = /** @type {number} */ (instruction[4]) * pixelRatio;
ol.DEBUG && console.assert(typeof instruction[5] === 'number',
'6th instruction should be a number');
var offsetY = /** @type {number} */ (instruction[5]) * pixelRatio;
ol.DEBUG && console.assert(typeof instruction[6] === 'number',
'7th instruction should be a number');
rotation = /** @type {number} */ (instruction[6]);
ol.DEBUG && console.assert(typeof instruction[7] === 'number',
'8th instruction should be a number');
scale = /** @type {number} */ (instruction[7]) * pixelRatio;
ol.DEBUG && console.assert(typeof instruction[8] === 'boolean',
'9th instruction should be a boolean');
fill = /** @type {boolean} */ (instruction[8]);
ol.DEBUG && console.assert(typeof instruction[9] === 'boolean',
'10th instruction should be a boolean');
stroke = /** @type {boolean} */ (instruction[9]);
rotateWithView = /** @type {boolean} */ (instruction[10]);
if (rotateWithView) {
rotation += viewRotation;
}
for (; d < dd; d += 2) {
x = pixelCoordinates[d] + offsetX;
y = pixelCoordinates[d + 1] + offsetY;
if (scale != 1 || rotation !== 0) {
ol.transform.compose(localTransform, x, y, scale, scale, rotation, -x, -y);
context.setTransform.apply(context, localTransform);
}
// Support multiple lines separated by \n
var lines = text.split('\n');
var numLines = lines.length;
var fontSize, lineY;
if (numLines > 1) {
// Estimate line height using width of capital M, and add padding
fontSize = Math.round(context.measureText('M').width * 1.5);
lineY = y - (((numLines - 1) / 2) * fontSize);
} else {
// No need to calculate line height/offset for a single line
fontSize = 0;
lineY = y;
}
for (var lineIndex = 0; lineIndex < numLines; lineIndex++) {
var line = lines[lineIndex];
if (stroke) {
context.strokeText(line, x, lineY);
}
if (fill) {
context.fillText(line, x, lineY);
}
// Move next line down by fontSize px
lineY = lineY + fontSize;
}
if (scale != 1 || rotation !== 0) {
context.setTransform.apply(context, resetTransform);
}
}
++i;
break;
case ol.render.canvas.Instruction.END_GEOMETRY:
if (featureCallback !== undefined) {
feature =
/** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
var result = featureCallback(feature);
if (result) {
return result;
}
}
++i;
break;
case ol.render.canvas.Instruction.FILL:
if (batchSize) {
pendingFill++;
} else {
this.fill_(context, viewRotation);
}
++i;
break;
case ol.render.canvas.Instruction.MOVE_TO_LINE_TO:
ol.DEBUG && console.assert(typeof instruction[1] === 'number',
'2nd instruction should be a number');
d = /** @type {number} */ (instruction[1]);
ol.DEBUG && console.assert(typeof instruction[2] === 'number',
'3rd instruction should be a number');
dd = /** @type {number} */ (instruction[2]);
x = pixelCoordinates[d];
y = pixelCoordinates[d + 1];
roundX = (x + 0.5) | 0;
roundY = (y + 0.5) | 0;
if (roundX !== prevX || roundY !== prevY) {
context.moveTo(x, y);
prevX = roundX;
prevY = roundY;
}
for (d += 2; d < dd; d += 2) {
x = pixelCoordinates[d];
y = pixelCoordinates[d + 1];
roundX = (x + 0.5) | 0;
roundY = (y + 0.5) | 0;
if (d == dd - 2 || roundX !== prevX || roundY !== prevY) {
context.lineTo(x, y);
prevX = roundX;
prevY = roundY;
}
}
++i;
break;
case ol.render.canvas.Instruction.SET_FILL_STYLE:
ol.DEBUG && console.assert(
ol.colorlike.isColorLike(instruction[1]),
'2nd instruction should be a string, ' +
'CanvasPattern, or CanvasGradient');
this.fillOrigin_ = instruction[2];
if (pendingFill) {
this.fill_(context, viewRotation);
pendingFill = 0;
}
context.fillStyle = /** @type {ol.ColorLike} */ (instruction[1]);
++i;
break;
case ol.render.canvas.Instruction.SET_STROKE_STYLE:
ol.DEBUG && console.assert(ol.colorlike.isColorLike(instruction[1]),
'2nd instruction should be a string, CanvasPattern, or CanvasGradient');
ol.DEBUG && console.assert(typeof instruction[2] === 'number',
'3rd instruction should be a number');
ol.DEBUG && console.assert(typeof instruction[3] === 'string',
'4rd instruction should be a string');
ol.DEBUG && console.assert(typeof instruction[4] === 'string',
'5th instruction should be a string');
ol.DEBUG && console.assert(typeof instruction[5] === 'number',
'6th instruction should be a number');
ol.DEBUG && console.assert(instruction[6],
'7th instruction should not be null');
ol.DEBUG && console.assert(typeof instruction[8] === 'number',
'9th instruction should be a number');
var usePixelRatio = instruction[7] !== undefined ?
instruction[7] : true;
var renderedPixelRatio = instruction[8];
var lineWidth = /** @type {number} */ (instruction[2]);
if (pendingStroke) {
context.stroke();
pendingStroke = 0;
}
context.strokeStyle = /** @type {ol.ColorLike} */ (instruction[1]);
context.lineWidth = usePixelRatio ? lineWidth * pixelRatio : lineWidth;
context.lineCap = /** @type {string} */ (instruction[3]);
context.lineJoin = /** @type {string} */ (instruction[4]);
context.miterLimit = /** @type {number} */ (instruction[5]);
if (ol.has.CANVAS_LINE_DASH) {
var lineDash = /** @type {Array.<number>} */ (instruction[6]);
if (usePixelRatio && pixelRatio !== renderedPixelRatio) {
lineDash = lineDash.map(function(dash) {
return dash * pixelRatio / renderedPixelRatio;
});
instruction[6] = lineDash;
instruction[8] = pixelRatio;
}
context.setLineDash(lineDash);
}
prevX = NaN;
prevY = NaN;
++i;
break;
case ol.render.canvas.Instruction.SET_TEXT_STYLE:
ol.DEBUG && console.assert(typeof instruction[1] === 'string',
'2nd instruction should be a string');
ol.DEBUG && console.assert(typeof instruction[2] === 'string',
'3rd instruction should be a string');
ol.DEBUG && console.assert(typeof instruction[3] === 'string',
'4th instruction should be a string');
context.font = /** @type {string} */ (instruction[1]);
context.textAlign = /** @type {string} */ (instruction[2]);
context.textBaseline = /** @type {string} */ (instruction[3]);
++i;
break;
case ol.render.canvas.Instruction.STROKE:
if (batchSize) {
pendingStroke++;
} else {
context.stroke();
}
++i;
break;
default:
ol.DEBUG && console.assert(false, 'Unknown canvas render instruction');
++i; // consume the instruction anyway, to avoid an infinite loop
break;
}
}
if (pendingFill) {
this.fill_(context, viewRotation);
}
if (pendingStroke) {
context.stroke();
}
// assert that all instructions were consumed
ol.DEBUG && console.assert(i == instructions.length,
'all instructions should be consumed');
return undefined;
};
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {number} pixelRatio Pixel ratio.
* @param {ol.Transform} transform Transform.
* @param {number} viewRotation View rotation.
* @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
* to skip.
*/
ol.render.canvas.Replay.prototype.replay = function(
context, pixelRatio, transform, viewRotation, skippedFeaturesHash) {
var instructions = this.instructions;
this.replay_(context, pixelRatio, transform, viewRotation,
skippedFeaturesHash, instructions, undefined, undefined);
};
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {ol.Transform} transform Transform.
* @param {number} viewRotation View rotation.
* @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
* to skip.
* @param {function((ol.Feature|ol.render.Feature)): T=} opt_featureCallback
* Feature callback.
* @param {ol.Extent=} opt_hitExtent Only check features that intersect this
* extent.
* @return {T|undefined} Callback result.
* @template T
*/
ol.render.canvas.Replay.prototype.replayHitDetection = function(
context, transform, viewRotation, skippedFeaturesHash,
opt_featureCallback, opt_hitExtent) {
var instructions = this.hitDetectionInstructions;
return this.replay_(context, 1, transform, viewRotation,
skippedFeaturesHash, instructions, opt_featureCallback, opt_hitExtent);
};
/**
* Reverse the hit detection instructions.
*/
ol.render.canvas.Replay.prototype.reverseHitDetectionInstructions = function() {
var hitDetectionInstructions = this.hitDetectionInstructions;
// step 1 - reverse array
hitDetectionInstructions.reverse();
// step 2 - reverse instructions within geometry blocks
var i;
var n = hitDetectionInstructions.length;
var instruction;
var type;
var begin = -1;
for (i = 0; i < n; ++i) {
instruction = hitDetectionInstructions[i];
type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
if (type == ol.render.canvas.Instruction.END_GEOMETRY) {
ol.DEBUG && console.assert(begin == -1, 'begin should be -1');
begin = i;
} else if (type == ol.render.canvas.Instruction.BEGIN_GEOMETRY) {
instruction[2] = i;
ol.DEBUG && console.assert(begin >= 0,
'begin should be larger than or equal to 0');
ol.array.reverseSubArray(this.hitDetectionInstructions, begin, i);
begin = -1;
}
}
};
/**
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
* @param {ol.Feature|ol.render.Feature} feature Feature.
*/
ol.render.canvas.Replay.prototype.endGeometry = function(geometry, feature) {
ol.DEBUG && console.assert(this.beginGeometryInstruction1_,
'this.beginGeometryInstruction1_ should not be null');
this.beginGeometryInstruction1_[2] = this.instructions.length;
this.beginGeometryInstruction1_ = null;
ol.DEBUG && console.assert(this.beginGeometryInstruction2_,
'this.beginGeometryInstruction2_ should not be null');
this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
this.beginGeometryInstruction2_ = null;
var endGeometryInstruction =
[ol.render.canvas.Instruction.END_GEOMETRY, feature];
this.instructions.push(endGeometryInstruction);
this.hitDetectionInstructions.push(endGeometryInstruction);
};
/**
* FIXME empty description for jsdoc
*/
ol.render.canvas.Replay.prototype.finish = ol.nullFunction;
/**
* Get the buffered rendering extent. Rendering will be clipped to the extent
* provided to the constructor. To account for symbolizers that may intersect
* this extent, we calculate a buffered extent (e.g. based on stroke width).
* @return {ol.Extent} The buffered rendering extent.
* @protected
*/
ol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() {
return this.maxExtent;
};