Implement text rendering along paths

This commit also changes the TextReplay.drawText() signature, and moves
geometry calculation into drawText(). This improves performance where no
text needs to be rendered (TextStyle.getText() == ''), which is used often
in applications.
This commit is contained in:
Andreas Hocevar
2017-09-07 23:32:31 +02:00
parent 37dcd79a86
commit efc86d59b0
12 changed files with 640 additions and 189 deletions

View File

@@ -8,8 +8,7 @@
"dependencies": {
"pbf": "3.0.5",
"pixelworks": "1.1.0",
"rbush": "2.0.1",
"@mapbox/vector-tile": "1.3.0"
"rbush": "2.0.1"
},
"browserify": {
"transform": [

View File

@@ -0,0 +1,75 @@
goog.provide('ol.geom.flat.textpath');
goog.require('ol.math');
/**
* @param {Array.<number>} flatCoordinates Path to put text on.
* @param {number} offset Start offset of the `flatCoordinates`.
* @param {number} end End offset of the `flatCoordinates`.
* @param {number} stride Stride.
* @param {string} text Text to place on the path.
* @param {function(string):number} measure Measure function returning the
* width of the character passed as 1st argument.
* @param {number} startM m along the path where the text starts.
* @param {number} maxAngle Max angle between adjacent chars in radians.
* @param {Array.<Array.<number>>=} opt_result Array that will be populated with the
* result. Each entry consists of an array of x, y and z of the char to draw.
* If provided, this array will not be truncated to the number of characters of
* the `text`.
* @return {Array.<Array.<number>>} The result array of null if `maxAngle` was
* exceeded.
*/
ol.geom.flat.textpath.lineString = function(
flatCoordinates, offset, end, stride, text, measure, startM, maxAngle, opt_result) {
var result = opt_result ? opt_result : [];
// Keep text upright
var reverse = flatCoordinates[offset] > flatCoordinates[end - stride];
var numChars = text.length;
var x1 = flatCoordinates[offset];
var y1 = flatCoordinates[offset + 1];
offset += stride;
var x2 = flatCoordinates[offset];
var y2 = flatCoordinates[offset + 1];
var segmentM = 0;
var segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
var index, previousAngle;
for (var i = 0; i < numChars; ++i) {
index = reverse ? numChars - i - 1 : i;
var char = text[index];
var charLength = measure(char);
var charM = startM + charLength / 2;
while (offset < end - stride && segmentM + segmentLength < charM) {
x1 = x2;
y1 = y2;
offset += stride;
x2 = flatCoordinates[offset];
y2 = flatCoordinates[offset + 1];
segmentM += segmentLength;
segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
var segmentPos = charM - segmentM;
var angle = Math.atan2(y2 - y1, x2 - x1);
if (reverse) {
angle += angle > 0 ? -Math.PI : Math.PI;
}
if (previousAngle !== undefined) {
var delta = angle - previousAngle;
delta += (delta > Math.PI) ? -2 * Math.PI : (delta < -Math.PI) ? 2 * Math.PI : 0;
if (Math.abs(delta) > maxAngle) {
return null;
}
}
previousAngle = angle;
var interpolate = segmentPos / segmentLength;
var x = ol.math.lerp(x1, x2, interpolate);
var y = ol.math.lerp(y1, y2, interpolate);
result[index] = [x, y, angle];
startM += charLength;
}
return result;
};

View File

@@ -162,7 +162,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome
this.instructions.push([
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_,
this.anchorX_, this.anchorY_, this.height_, undefined, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.snapToPixel_, this.width_
]);
@@ -170,7 +170,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
this.hitDetectionImage_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorY_, this.height_, this.opacity_,
this.anchorX_, this.anchorY_, this.height_, undefined, this.opacity_,
this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.snapToPixel_, this.width_
]);

View File

@@ -9,11 +9,12 @@ ol.render.canvas.Instruction = {
CIRCLE: 2,
CLOSE_PATH: 3,
CUSTOM: 4,
DRAW_IMAGE: 5,
END_GEOMETRY: 6,
FILL: 7,
MOVE_TO_LINE_TO: 8,
SET_FILL_STYLE: 9,
SET_STROKE_STYLE: 10,
STROKE: 11
DRAW_CHARS: 5,
DRAW_IMAGE: 6,
END_GEOMETRY: 7,
FILL: 8,
MOVE_TO_LINE_TO: 9,
SET_FILL_STYLE: 10,
SET_STROKE_STYLE: 11,
STROKE: 12
};

View File

@@ -6,6 +6,8 @@ goog.require('ol.extent');
goog.require('ol.extent.Relationship');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.length');
goog.require('ol.geom.flat.textpath');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.obj');
@@ -130,10 +132,69 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio,
* @type {!ol.Transform}
*/
this.resetTransform_ = ol.transform.create();
/**
* @private
* @type {Array.<*>}
*/
this.chars_ = [];
};
ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {number} x X.
* @param {number} y Y.
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
* @param {number} anchorX Anchor X.
* @param {number} anchorY Anchor Y.
* @param {number} height Height.
* @param {number} opacity Opacity.
* @param {number} originX Origin X.
* @param {number} originY Origin Y.
* @param {number} rotation Rotation.
* @param {number} scale Scale.
* @param {boolean} snapToPixel Snap to pixel.
* @param {number} width Width.
*/
ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, anchorX, anchorY,
height, opacity, originX, originY, rotation, scale, snapToPixel, width) {
var localTransform = this.tmpLocalTransform_;
anchorX *= scale;
anchorY *= scale;
x -= anchorX;
y -= anchorY;
if (snapToPixel) {
x = Math.round(x);
y = Math.round(y);
}
if (rotation !== 0) {
var centerX = x + anchorX;
var centerY = y + anchorY;
ol.transform.compose(localTransform,
centerX, centerY, 1, 1, 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 * scale, h * scale);
if (opacity != 1) {
context.globalAlpha = alpha;
}
if (rotation !== 0) {
context.setTransform.apply(context, this.resetTransform_);
}
};
/**
* @protected
* @param {Array.<number>} dashArray Dash array.
@@ -340,9 +401,7 @@ ol.render.canvas.Replay.prototype.replay_ = function(
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 anchorX, anchorY, prevX, prevY, roundX, roundY;
var pendingFill = 0;
var pendingStroke = 0;
var coordinateCache = this.coordinateCache_;
@@ -435,53 +494,63 @@ ol.render.canvas.Replay.prototype.replay_ = function(
dd = /** @type {number} */ (instruction[2]);
var image = /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */
(instruction[3]);
var scale = /** @type {number} */ (instruction[12]);
// Remaining arguments in DRAW_IMAGE are in alphabetical order
var anchorX = /** @type {number} */ (instruction[4]) * scale;
var anchorY = /** @type {number} */ (instruction[5]) * scale;
anchorX = /** @type {number} */ (instruction[4]);
anchorY = /** @type {number} */ (instruction[5]);
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 (rotation !== 0) {
var centerX = x + anchorX;
var centerY = y + anchorY;
ol.transform.compose(localTransform,
centerX, centerY, 1, 1, rotation, -centerX, -centerY);
context.setTransform.apply(context, localTransform);
}
var alpha = context.globalAlpha;
if (opacity != 1) {
context.globalAlpha = alpha * opacity;
}
this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1],
image, anchorX, anchorY, height, opacity, originX, originY,
rotation, scale, snapToPixel, width);
}
++i;
break;
case ol.render.canvas.Instruction.DRAW_CHARS:
var begin = /** @type {number} */ (instruction[1]);
var end = /** @type {number} */ (instruction[2]);
var images = /** @type {Array.<HTMLCanvasElement>} */ (instruction[3]);
// Remaining arguments in DRAW_CHARS are in alphabetical order
var baseline = /** @type {number} */ (instruction[4]);
var exceedLength = /** @type {number} */ (instruction[5]);
var maxAngle = /** @type {number} */ (instruction[6]);
var measure = /** @type {function(string):number} */ (instruction[7]);
var offsetY = /** @type {number} */ (instruction[8]);
var strokeWidth = /** @type {number} */ (instruction[9]);
var text = /** @type {string} */ (instruction[10]);
var align = /** @type {number} */ (instruction[11]);
var textScale = /** @type {number} */ (instruction[12]);
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 * scale, h * scale);
if (opacity != 1) {
context.globalAlpha = alpha;
}
if (rotation !== 0) {
context.setTransform.apply(context, resetTransform);
var pathLength = ol.geom.flat.length.lineString(pixelCoordinates, begin, end, 2);
var textLength = measure(text);
if (exceedLength || textLength <= pathLength) {
var startM = (pathLength - textLength) * align;
var chars = ol.geom.flat.textpath.lineString(
pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle, this.chars_);
var numChars = text.length;
if (chars) {
for (var c = 0, cc = images.length; c < cc; ++c) {
var char = chars[c % numChars]; // x, y, rotation
var label = images[c];
anchorX = label.width / 2;
anchorY = baseline * label.height + 2 * (0.5 - baseline) * strokeWidth - offsetY;
this.replayImage_(context, char[0], char[1], label, anchorX, anchorY,
label.height, 1, 0, 0, char[2], textScale, false, label.width);
}
}
}
++i;
break;
case ol.render.canvas.Instruction.END_GEOMETRY:

View File

@@ -2,12 +2,15 @@ goog.provide('ol.render.canvas.TextReplay');
goog.require('ol');
goog.require('ol.colorlike');
goog.require('ol.dom');
goog.require('ol.geom.GeometryType');
goog.require('ol.has');
goog.require('ol.render.canvas');
goog.require('ol.render.canvas.Instruction');
goog.require('ol.render.canvas.Replay');
goog.require('ol.render.replay');
goog.require('ol.structs.LRUCache');
goog.require('ol.style.TextPlacement');
/**
@@ -24,6 +27,12 @@ ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution, pixelRa
ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps);
/**
* @private
* @type {Array.<HTMLCanvasElement>}
*/
this.labels_ = null;
/**
* @private
* @type {string}
@@ -78,6 +87,24 @@ ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution, pixelRa
*/
this.textState_ = null;
/**
* @private
* @type {string}
*/
this.textKey_ = '';
/**
* @private
* @type {string}
*/
this.fillKey_ = '';
/**
* @private
* @type {string}
*/
this.strokeKey_ = '';
while (ol.render.canvas.TextReplay.labelCache_.canExpireCache()) {
ol.render.canvas.TextReplay.labelCache_.pop();
}
@@ -95,33 +122,65 @@ ol.render.canvas.TextReplay.labelCache_ = new ol.structs.LRUCache();
/**
* @param {string} font Font to use for measuring.
* @param {Array.<string>} lines Lines to measure.
* @param {Array.<number>} widths Array will be populated with the widths of
* each line.
* @return {ol.Size} Measuremnt.
* @return {ol.Size} Measurement.
*/
ol.render.canvas.TextReplay.measureText = (function() {
ol.render.canvas.TextReplay.measureTextHeight = (function() {
var textContainer;
return function(font, lines, widths) {
if (!textContainer) {
textContainer = document.createElement('span');
textContainer.textContent = 'M';
textContainer.style.visibility = 'hidden';
textContainer.style.whiteSpace = 'nowrap';
}
textContainer.style.font = font;
document.body.appendChild(textContainer);
var height = textContainer.offsetHeight;
document.body.removeChild(textContainer);
return height;
};
})();
/**
* @this {Object}
* @param {CanvasRenderingContext2D} context Context.
* @param {number} pixelRatio Pixel ratio.
* @param {string} text Text.
* @return {number} Width.
*/
ol.render.canvas.TextReplay.getTextWidth = function(context, pixelRatio, text) {
var width = this[text];
if (!width) {
this[text] = width = context.measureText(text).width;
}
return width * pixelRatio;
};
/**
* @param {string} font Font to use for measuring.
* @param {Array.<string>} lines Lines to measure.
* @param {Array.<number>} widths Array will be populated with the widths of
* each line.
* @return {number} Width of the whole text.
*/
ol.render.canvas.TextReplay.measureTextWidths = (function() {
var context;
return function(font, lines, widths) {
if (!context) {
context = ol.dom.createCanvasContext2D(1, 1);
}
context.font = font;
var numLines = lines.length;
var width = 0;
var currentWidth, i;
for (i = 0; i < numLines; ++i) {
textContainer.textContent = lines[i];
currentWidth = textContainer.offsetWidth;
currentWidth = context.measureText(lines[i]).width;
width = Math.max(width, currentWidth);
widths.push(currentWidth);
}
var measurement = [width, textContainer.offsetHeight * numLines];
document.body.removeChild(textContainer);
return measurement;
return width;
};
})();
@@ -129,51 +188,113 @@ ol.render.canvas.TextReplay.measureText = (function() {
/**
* @inheritDoc
*/
ol.render.canvas.TextReplay.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, feature) {
ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) {
var fillState = this.textFillState_;
var strokeState = this.textStrokeState_;
var textState = this.textState_;
if (this.text_ === '' || !textState || (!fillState && !strokeState)) {
if (this.text_ === '' || !this.textState_ || (!fillState && !strokeState)) {
return;
}
this.beginGeometry(geometry, feature);
var begin = this.coordinates.length;
var myBegin = this.coordinates.length;
var myEnd =
this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false);
var fill = !!fillState;
var stroke = !!strokeState;
var pixelRatio = this.pixelRatio;
var textAlign = textState.textAlign;
var align = ol.render.replay.TEXT_ALIGN[textAlign];
var textBaseline = textState.textBaseline;
var baseline = ol.render.replay.TEXT_ALIGN[textBaseline];
var strokeWidth = stroke && strokeState.lineWidth ? strokeState.lineWidth : 0;
var geometryType = geometry.getType();
var flatCoordinates = null;
var end = 2;
var stride = 2;
if (this.textState_.placement === ol.style.TextPlacement.LINE) {
var ends;
flatCoordinates = geometry.getFlatCoordinates();
stride = geometry.getStride();
if (geometryType == ol.geom.GeometryType.LINE_STRING) {
ends = [flatCoordinates.length];
} else if (geometryType == ol.geom.GeometryType.MULTI_LINE_STRING) {
ends = geometry.getEnds();
} else if (geometryType == ol.geom.GeometryType.POLYGON) {
ends = geometry.getEnds().slice(0, 1);
} else if (geometryType == ol.geom.GeometryType.MULTI_POLYGON) {
var endss = geometry.getEndss();
ends = [];
for (var i = 0, ii = endss.length; i < ii; ++i) {
ends.push(endss[i][0]);
}
}
var flatOffset = 0;
var flatEnd;
for (var o = 0, oo = ends.length; o < oo; ++o) {
flatEnd = ends[o];
end = this.appendFlatCoordinates(flatCoordinates, flatOffset, flatEnd, stride, false, false);
flatOffset = flatEnd;
this.drawChars_(begin, end);
begin = end;
}
} else {
switch (geometryType) {
case ol.geom.GeometryType.POINT:
case ol.geom.GeometryType.MULTI_POINT:
flatCoordinates = geometry.getFlatCoordinates();
end = flatCoordinates.length;
break;
case ol.geom.GeometryType.LINE_STRING:
flatCoordinates = /** @type {ol.geom.LineString} */ (geometry).getFlatMidpoint();
break;
case ol.geom.GeometryType.CIRCLE:
flatCoordinates = /** @type {ol.geom.Circle} */ (geometry).getCenter();
break;
case ol.geom.GeometryType.MULTI_LINE_STRING:
flatCoordinates = /** @type {ol.geom.MultiLineString} */ (geometry).getFlatMidpoints();
end = flatCoordinates.length;
break;
case ol.geom.GeometryType.POLYGON:
flatCoordinates = /** @type {ol.geom.Polygon} */ (geometry).getFlatInteriorPoint();
break;
case ol.geom.GeometryType.MULTI_POLYGON:
flatCoordinates = /** @type {ol.geom.MultiPolygon} */ (geometry).getFlatInteriorPoints();
end = flatCoordinates.length;
break;
default:
}
end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false);
this.drawTextImage_(begin, end);
}
this.endGeometry(geometry, feature);
};
/**
* @private
* @param {string} text Text.
* @param {boolean} fill Fill.
* @param {boolean} stroke Stroke.
* @return {HTMLCanvasElement} Image.
*/
ol.render.canvas.TextReplay.prototype.getImage_ = function(text, fill, stroke) {
var label;
var text = this.text_;
var key =
(stroke ?
(typeof strokeState.strokeStyle == 'string' ? strokeState.strokeStyle : ol.getUid(strokeState.strokeStyle)) +
strokeState.lineCap + strokeState.lineDashOffset + '|' + strokeWidth +
strokeState.lineJoin + strokeState.miterLimit +
'[' + strokeState.lineDash.join() + ']' : '') +
textState.font + textAlign + text +
(fill ?
(typeof fillState.fillStyle == 'string' ? fillState.fillStyle : ('|' + ol.getUid(fillState.fillStyle))) : '');
var key = (stroke ? this.strokeKey_ : '') + this.textKey_ + text + (fill ? this.fillKey_ : '');
var lines = text.split('\n');
var numLines = lines.length;
if (!ol.render.canvas.TextReplay.labelCache_.containsKey(key)) {
label = /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
ol.render.canvas.TextReplay.labelCache_.set(key, label);
var context = label.getContext('2d');
var strokeState = this.textStrokeState_;
var fillState = this.textFillState_;
var textState = this.textState_;
var pixelRatio = this.pixelRatio;
var align = ol.render.replay.TEXT_ALIGN[textState.textAlign];
var strokeWidth = stroke && strokeState.lineWidth ? strokeState.lineWidth : 0;
var widths = [];
var metrics = ol.render.canvas.TextReplay.measureText(textState.font, lines, widths);
var lineHeight = metrics[1] / numLines;
label.width = Math.ceil(metrics[0] + 2 * strokeWidth) * pixelRatio;
label.height = Math.ceil(metrics[1] + 2 * strokeWidth) * pixelRatio;
var width = ol.render.canvas.TextReplay.measureTextWidths(textState.font, lines, widths);
var lineHeight = ol.render.canvas.TextReplay.measureTextHeight(textState.font);
var height = lineHeight * numLines;
var renderWidth = (width + 2 * strokeWidth);
var context = ol.dom.createCanvasContext2D(
Math.ceil(renderWidth * pixelRatio),
Math.ceil((height + 2 * strokeWidth) * pixelRatio));
label = context.canvas;
ol.render.canvas.TextReplay.labelCache_.set(key, label);
context.scale(pixelRatio, pixelRatio);
context.font = textState.font;
if (stroke) {
@@ -191,39 +312,101 @@ ol.render.canvas.TextReplay.prototype.drawText = function(flatCoordinates, offse
context.fillStyle = fillState.fillStyle;
}
context.textBaseline = 'top';
context.textAlign = 'left';
var x = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth;
context.textAlign = 'center';
var leftRight = (0.5 - align);
var x = align * label.width / pixelRatio + leftRight * 2 * strokeWidth;
var i;
if (stroke) {
for (i = 0; i < numLines; ++i) {
context.strokeText(lines[i], x - align * widths[i], strokeWidth + i * lineHeight);
context.strokeText(lines[i], x + leftRight * widths[i], strokeWidth + i * lineHeight);
}
}
if (fill) {
for (i = 0; i < numLines; ++i) {
context.fillText(lines[i], x - align * widths[i], strokeWidth + i * lineHeight);
context.fillText(lines[i], x + leftRight * widths[i], strokeWidth + i * lineHeight);
}
}
}
label = ol.render.canvas.TextReplay.labelCache_.get(key);
return ol.render.canvas.TextReplay.labelCache_.get(key);
};
/**
* @private
* @param {number} begin Begin.
* @param {number} end End.
*/
ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(begin, end) {
var textState = this.textState_;
var strokeState = this.textStrokeState_;
var pixelRatio = this.pixelRatio;
var align = ol.render.replay.TEXT_ALIGN[textState.textAlign];
var baseline = ol.render.replay.TEXT_ALIGN[textState.textBaseline];
var strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;
var label = this.getImage_(this.text_, !!this.textFillState_, !!this.textStrokeState_);
var anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth;
var anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
this.instructions.push([
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, label,
(anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
this.textScale_, true, label.width
]);
this.hitDetectionInstructions.push([
ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, label,
(anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
this.textScale_ / pixelRatio, true, label.width
]);
};
this.endGeometry(geometry, feature);
/**
* @private
* @param {number} begin Begin.
* @param {number} end End.
*/
ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) {
var pixelRatio = this.pixelRatio;
var strokeState = this.textStrokeState_;
var fill = !!this.textFillState_;
var stroke = !!strokeState;
var textState = this.textState_;
var baseline = ol.render.replay.TEXT_ALIGN[textState.textBaseline];
var strokeWidth = stroke && strokeState.lineWidth ? strokeState.lineWidth * pixelRatio : 0;
var labels = [];
var text = this.text_;
var numChars = this.text_.length;
var i;
if (stroke) {
for (i = 0; i < numChars; ++i) {
labels.push(this.getImage_(text.charAt(i), false, stroke));
}
}
if (fill) {
for (i = 0; i < numChars; ++i) {
labels.push(this.getImage_(text.charAt(i), fill, false));
}
}
var context = labels[0].getContext('2d');
var offsetY = this.textOffsetY_ * pixelRatio;
var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign];
var widths = {};
this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
begin, end, labels, baseline,
textState.exceedLength, textState.maxAngle,
ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, pixelRatio),
offsetY, strokeWidth, this.text_, align, this.textScale_
]);
this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
begin, end, labels, baseline,
textState.exceedLength, textState.maxAngle,
ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, 1),
offsetY, strokeWidth, this.text_, align, this.textScale_ / pixelRatio
]);
};
@@ -231,28 +414,26 @@ ol.render.canvas.TextReplay.prototype.drawText = function(flatCoordinates, offse
* @inheritDoc
*/
ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
var textState, fillState, strokeState;
if (!textStyle) {
this.text_ = '';
} else {
var textFillStyle = textStyle.getFill();
if (!textFillStyle) {
this.textFillState_ = null;
fillState = this.textFillState_ = null;
} else {
var textFillStyleColor = textFillStyle.getColor();
var fillStyle = ol.colorlike.asColorLike(textFillStyleColor ?
textFillStyleColor : ol.render.canvas.defaultFillStyle);
if (!this.textFillState_) {
this.textFillState_ = {
fillStyle: fillStyle
};
} else {
var textFillState = this.textFillState_;
textFillState.fillStyle = fillStyle;
fillState = this.textFillState_;
if (!fillState) {
fillState = this.textFillState_ = /** @type {ol.CanvasFillState} */ ({});
}
fillState.fillStyle = fillStyle;
}
var textStrokeStyle = textStyle.getStroke();
if (!textStrokeStyle) {
this.textStrokeState_ = null;
strokeState = this.textStrokeState_ = null;
} else {
var textStrokeStyleColor = textStrokeStyle.getColor();
var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
@@ -275,26 +456,17 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
textStrokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;
var strokeStyle = ol.colorlike.asColorLike(textStrokeStyleColor ?
textStrokeStyleColor : ol.render.canvas.defaultStrokeStyle);
if (!this.textStrokeState_) {
this.textStrokeState_ = {
lineCap: lineCap,
lineDash: lineDash,
lineDashOffset: lineDashOffset,
lineJoin: lineJoin,
lineWidth: lineWidth,
miterLimit: miterLimit,
strokeStyle: strokeStyle
};
} else {
var textStrokeState = this.textStrokeState_;
textStrokeState.lineCap = lineCap;
textStrokeState.lineDash = lineDash;
textStrokeState.lineDashOffset = lineDashOffset;
textStrokeState.lineJoin = lineJoin;
textStrokeState.lineWidth = lineWidth;
textStrokeState.miterLimit = miterLimit;
textStrokeState.strokeStyle = strokeStyle;
strokeState = this.textStrokeState_;
if (!strokeState) {
strokeState = this.textStrokeState_ = /** @type {ol.CanvasStrokeState} */ ({});
}
strokeState.lineCap = lineCap;
strokeState.lineDash = lineDash;
strokeState.lineDashOffset = lineDashOffset;
strokeState.lineJoin = lineJoin;
strokeState.lineWidth = lineWidth;
strokeState.miterLimit = miterLimit;
strokeState.strokeStyle = strokeStyle;
}
var textFont = textStyle.getFont();
var textOffsetX = textStyle.getOffsetX();
@@ -311,23 +483,32 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
textTextAlign : ol.render.canvas.defaultTextAlign;
var textBaseline = textTextBaseline !== undefined ?
textTextBaseline : ol.render.canvas.defaultTextBaseline;
if (!this.textState_) {
this.textState_ = {
font: font,
textAlign: textAlign,
textBaseline: textBaseline
};
} else {
var textState = this.textState_;
textState.font = font;
textState.textAlign = textAlign;
textState.textBaseline = textBaseline;
textState = this.textState_;
if (!textState) {
textState = this.textState_ = /** @type {ol.CanvasTextState} */ ({});
}
textState.exceedLength = textStyle.getExceedLength();
textState.font = font;
textState.maxAngle = textStyle.getMaxAngle();
textState.placement = textStyle.getPlacement();
textState.textAlign = textAlign;
textState.textBaseline = textBaseline;
this.text_ = textText !== undefined ? textText : '';
this.textOffsetX_ = textOffsetX !== undefined ? textOffsetX : 0;
this.textOffsetY_ = textOffsetY !== undefined ? textOffsetY : 0;
this.textRotateWithView_ = textRotateWithView !== undefined ? textRotateWithView : false;
this.textRotation_ = textRotation !== undefined ? textRotation : 0;
this.textScale_ = textScale !== undefined ? textScale : 1;
this.strokeKey_ = strokeState ?
(typeof strokeState.strokeStyle == 'string' ? strokeState.strokeStyle : ol.getUid(strokeState.strokeStyle)) +
strokeState.lineCap + strokeState.lineDashOffset + '|' + strokeState.lineWidth +
strokeState.lineJoin + strokeState.miterLimit + '[' + strokeState.lineDash.join() + ']' :
'';
this.textKey_ = textState.font + textState.textAlign;
this.fillKey_ = fillState ?
(typeof fillState.fillStyle == 'string' ? fillState.fillStyle : ('|' + ol.getUid(fillState.fillStyle))) :
'';
}
};

View File

@@ -108,14 +108,10 @@ ol.render.VectorContext.prototype.drawPolygon = function(polygonGeometry, featur
/**
* @param {Array.<number>} flatCoordinates Flat coordinates.
* @param {number} offset Offset.
* @param {number} end End.
* @param {number} stride Stride.
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
* @param {ol.Feature|ol.render.Feature} feature Feature.
*/
ol.render.VectorContext.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, feature) {};
ol.render.VectorContext.prototype.drawText = function(geometry, feature) {};
/**

View File

@@ -88,19 +88,15 @@ ol.inherits(ol.render.webgl.Immediate, ol.render.VectorContext);
/**
* @param {ol.render.webgl.ReplayGroup} replayGroup Replay group.
* @param {Array.<number>} flatCoordinates Flat coordinates.
* @param {number} offset Offset.
* @param {number} end End.
* @param {number} stride Stride.
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
* @private
*/
ol.render.webgl.Immediate.prototype.drawText_ = function(replayGroup,
flatCoordinates, offset, end, stride) {
ol.render.webgl.Immediate.prototype.drawText_ = function(replayGroup, geometry) {
var context = this.context_;
var replay = /** @type {ol.render.webgl.TextReplay} */ (
replayGroup.getReplay(0, ol.render.ReplayType.TEXT));
replay.setTextStyle(this.textStyle_);
replay.drawText(flatCoordinates, offset, end, stride, null, null);
replay.drawText(geometry, null);
replay.finish(context);
// default colors
var opacity = 1;
@@ -219,9 +215,7 @@ ol.render.webgl.Immediate.prototype.drawPoint = function(geometry, data) {
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatCoordinates = geometry.getFlatCoordinates();
var stride = geometry.getStride();
this.drawText_(replayGroup, flatCoordinates, 0, flatCoordinates.length, stride);
this.drawText_(replayGroup, geometry);
}
};
@@ -247,9 +241,7 @@ ol.render.webgl.Immediate.prototype.drawMultiPoint = function(geometry, data) {
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatCoordinates = geometry.getFlatCoordinates();
var stride = geometry.getStride();
this.drawText_(replayGroup, flatCoordinates, 0, flatCoordinates.length, stride);
this.drawText_(replayGroup, geometry);
}
};
@@ -275,8 +267,7 @@ ol.render.webgl.Immediate.prototype.drawLineString = function(geometry, data) {
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatMidpoint = geometry.getFlatMidpoint();
this.drawText_(replayGroup, flatMidpoint, 0, 2, 2);
this.drawText_(replayGroup, geometry);
}
};
@@ -302,8 +293,7 @@ ol.render.webgl.Immediate.prototype.drawMultiLineString = function(geometry, dat
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatMidpoints = geometry.getFlatMidpoints();
this.drawText_(replayGroup, flatMidpoints, 0, flatMidpoints.length, 2);
this.drawText_(replayGroup, geometry);
}
};
@@ -329,8 +319,7 @@ ol.render.webgl.Immediate.prototype.drawPolygon = function(geometry, data) {
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatInteriorPoint = geometry.getFlatInteriorPoint();
this.drawText_(replayGroup, flatInteriorPoint, 0, 2, 2);
this.drawText_(replayGroup, geometry);
}
};
@@ -356,8 +345,7 @@ ol.render.webgl.Immediate.prototype.drawMultiPolygon = function(geometry, data)
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
var flatInteriorPoints = geometry.getFlatInteriorPoints();
this.drawText_(replayGroup, flatInteriorPoints, 0, flatInteriorPoints.length, 2);
this.drawText_(replayGroup, geometry);
}
};
@@ -383,7 +371,7 @@ ol.render.webgl.Immediate.prototype.drawCircle = function(geometry, data) {
replay.getDeleteResourcesFunction(context)();
if (this.textStyle_) {
this.drawText_(replayGroup, geometry.getCenter(), 0, 2, 2);
this.drawText_(replayGroup, geometry);
}
};

View File

@@ -3,6 +3,7 @@ goog.provide('ol.render.webgl.TextReplay');
goog.require('ol');
goog.require('ol.colorlike');
goog.require('ol.dom');
goog.require('ol.geom.GeometryType');
goog.require('ol.has');
goog.require('ol.render.replay');
goog.require('ol.render.webgl');
@@ -118,9 +119,38 @@ ol.inherits(ol.render.webgl.TextReplay, ol.render.webgl.TextureReplay);
/**
* @inheritDoc
*/
ol.render.webgl.TextReplay.prototype.drawText = function(flatCoordinates, offset,
end, stride, geometry, feature) {
ol.render.webgl.TextReplay.prototype.drawText = function(geometry, feature) {
if (this.text_) {
var flatCoordinates = null;
var offset = 0;
var end = 2;
var stride = 2;
switch (geometry.getType()) {
case ol.geom.GeometryType.POINT:
case ol.geom.GeometryType.MULTI_POINT:
flatCoordinates = geometry.getFlatCoordinates();
end = flatCoordinates.length;
stride = geometry.getStride();
break;
case ol.geom.GeometryType.CIRCLE:
flatCoordinates = /** @type {ol.geom.Circle} */ (geometry).getCenter();
break;
case ol.geom.GeometryType.LINE_STRING:
flatCoordinates = /** @type {ol.geom.LineString} */ (geometry).getFlatMidpoint();
break;
case ol.geom.GeometryType.MULTI_LINE_STRING:
flatCoordinates = /** @type {ol.geom.MultiLineString} */ (geometry).getFlatMidpoints();
end = flatCoordinates.length;
break;
case ol.geom.GeometryType.POLYGON:
flatCoordinates = /** @type {ol.geom.Polygon} */ (geometry).getFlatInteriorPoint();
break;
case ol.geom.GeometryType.MULTI_POLYGON:
flatCoordinates = /** @type {ol.geom.MultiPolygon} */ (geometry).getFlatInteriorPoints();
end = flatCoordinates.length;
break;
default:
}
this.startIndices.push(this.indices.length);
this.startIndicesFeature.push(feature);

View File

@@ -58,7 +58,7 @@ ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry.getCenter(), 0, 2, 2, geometry, feature);
textReplay.drawText(geometry, feature);
}
};
@@ -181,7 +181,7 @@ ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, s
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry.getFlatMidpoint(), 0, 2, 2, geometry, feature);
textReplay.drawText(geometry, feature);
}
};
@@ -206,9 +206,7 @@ ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geomet
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
var flatMidpointCoordinates = geometry.getFlatMidpoints();
textReplay.drawText(flatMidpointCoordinates, 0,
flatMidpointCoordinates.length, 2, geometry, feature);
textReplay.drawText(geometry, feature);
}
};
@@ -234,9 +232,7 @@ ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry,
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
var flatInteriorPointCoordinates = geometry.getFlatInteriorPoints();
textReplay.drawText(flatInteriorPointCoordinates, 0,
flatInteriorPointCoordinates.length, 2, geometry, feature);
textReplay.drawText(geometry, feature);
}
};
@@ -264,8 +260,7 @@ ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style,
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry.getFlatCoordinates(), 0, 2, 2, geometry,
feature);
textReplay.drawText(geometry, feature);
}
};
@@ -293,9 +288,7 @@ ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, s
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
var flatCoordinates = geometry.getFlatCoordinates();
textReplay.drawText(flatCoordinates, 0, flatCoordinates.length,
geometry.getStride(), geometry, feature);
textReplay.drawText(geometry, feature);
}
};
@@ -321,8 +314,7 @@ ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, styl
var textReplay = replayGroup.getReplay(
style.getZIndex(), ol.render.ReplayType.TEXT);
textReplay.setTextStyle(textStyle);
textReplay.drawText(
geometry.getFlatInteriorPoint(), 0, 2, 2, geometry, feature);
textReplay.drawText(geometry, feature);
}
};

View File

@@ -0,0 +1,121 @@
goog.require('ol.geom.flat.textpath');
goog.require('ol.geom.flat.length');
describe('textpath', function() {
var horizontal = [0, 0, 100, 0];
var vertical = [0, 0, 0, 100];
var diagonal = [0, 0, 100, 100];
var reverse = [100, 0, 0, 100];
var angled = [0, 0, 100, 100, 200, 0];
var reverseangled = [151, 17, 163, 22, 159, 30, 150, 30, 143, 24, 151, 17];
function measure(text) {
return 10 * text.length;
}
it('center-aligns text on a horizontal line', function() {
var startM = 50 - 15;
var instructions = ol.geom.flat.textpath.lineString(
horizontal, 0, horizontal.length, 2, 'foo', measure, startM, Infinity);
expect(instructions).to.eql([[40, 0, 0], [50, 0, 0], [60, 0, 0]]);
});
it('left-aligns text on a horizontal line', function() {
var instructions = ol.geom.flat.textpath.lineString(
horizontal, 0, horizontal.length, 2, 'foo', measure, 0, Infinity);
expect(instructions).to.eql([[5, 0, 0], [15, 0, 0], [25, 0, 0]]);
});
it('right-aligns text on a horizontal line', function() {
var startM = 100 - 30;
var instructions = ol.geom.flat.textpath.lineString(
horizontal, 0, horizontal.length, 2, 'foo', measure, startM, Infinity);
expect(instructions).to.eql([[75, 0, 0], [85, 0, 0], [95, 0, 0]]);
});
it('draws text on a vertical line', function() {
var startM = 50 - 15;
var instructions = ol.geom.flat.textpath.lineString(
vertical, 0, vertical.length, 2, 'foo', measure, startM, Infinity);
var a = 90 * Math.PI / 180;
expect(instructions).to.eql([[0, 40, a], [0, 50, a], [0, 60, a]]);
});
it('draws text on a diagonal line', function() {
var startM = Math.sqrt(2) * 50 - 15;
var instructions = ol.geom.flat.textpath.lineString(
diagonal, 0, diagonal.length, 2, 'foo', measure, startM, Infinity);
expect(instructions[0][2]).to.be(45 * Math.PI / 180);
expect(instructions[0][0]).to.be.lessThan(instructions[2][0]);
expect(instructions[0][1]).to.be.lessThan(instructions[2][1]);
});
it('draws reverse text on a diagonal line', function() {
var startM = Math.sqrt(2) * 50 - 15;
var instructions = ol.geom.flat.textpath.lineString(
reverse, 0, reverse.length, 2, 'foo', measure, startM, Infinity);
expect(instructions[0][2]).to.be(-45 * Math.PI / 180);
expect(instructions[0][0]).to.be.lessThan(instructions[2][0]);
expect(instructions[0][1]).to.be.greaterThan(instructions[2][1]);
});
it('renders long text with extrapolation', function() {
var startM = 50 - 75;
var instructions = ol.geom.flat.textpath.lineString(
horizontal, 0, horizontal.length, 2, 'foo-foo-foo-foo', measure, startM, Infinity);
expect(instructions[0]).to.eql([-20, 0, 0]);
expect(instructions[14]).to.eql([120, 0, 0]);
});
it('renders angled text', function() {
var length = ol.geom.flat.length.lineString(angled, 0, angled.length, 2);
var startM = length / 2 - 15;
var instructions = ol.geom.flat.textpath.lineString(
angled, 0, angled.length, 2, 'foo', measure, startM, Infinity);
expect(instructions[0][2]).to.be(45 * Math.PI / 180);
expect(instructions[2][2]).to.be(-45 * Math.PI / 180);
});
it('respects maxAngle', function() {
var length = ol.geom.flat.length.lineString(angled, 0, angled.length, 2);
var startM = length / 2 - 15;
var instructions = ol.geom.flat.textpath.lineString(
angled, 0, angled.length, 2, 'foo', measure, startM, Math.PI / 4);
expect(instructions).to.be(null);
});
it('uses the smallest angle for maxAngleDelta', function() {
var length = ol.geom.flat.length.lineString(reverseangled, 0, reverseangled.length, 2);
var startM = length / 2 - 15;
var instructions = ol.geom.flat.textpath.lineString(
reverseangled, 0, reverseangled.length, 2, 'foo', measure, startM, Math.PI);
expect(instructions).to.not.be(undefined);
});
it('respects the begin option', function() {
var length = ol.geom.flat.length.lineString(angled, 2, angled.length, 2);
var startM = length / 2 - 15;
var instructions = ol.geom.flat.textpath.lineString(
angled, 2, angled.length, 2, 'foo', measure, startM, Infinity);
expect(instructions[1][0]).to.be(150);
});
it('respects the end option', function() {
var length = ol.geom.flat.length.lineString(angled, 0, 4, 2);
var startM = length / 2 - 15;
var instructions = ol.geom.flat.textpath.lineString(
angled, 0, 4, 2, 'foo', measure, startM, Infinity);
expect(instructions[1][0]).to.be(50);
});
it('uses the provided result array', function() {
var result = [];
result[3] = undefined;
var startM = 50 - 15;
ol.geom.flat.textpath.lineString(
horizontal, 0, horizontal.length, 2, 'foo', measure, startM, Infinity, result);
expect(result).to.eql([[40, 0, 0], [50, 0, 0], [60, 0, 0], undefined]);
});
});

View File

@@ -1,6 +1,5 @@
goog.require('ol.dom');
goog.require('ol.geom.Point');
goog.require('ol.render.webgl.TextReplay');
goog.require('ol.style.Fill');
goog.require('ol.style.Stroke');
@@ -127,19 +126,19 @@ describe('ol.render.webgl.TextReplay', function() {
var point;
point = [1000, 2000];
replay.drawText(point, 0, 2, 2, null, null);
replay.drawText(new ol.geom.Point(point), null);
expect(replay.vertices).to.have.length(256);
expect(replay.indices).to.have.length(48);
point = [2000, 3000];
replay.drawText(point, 0, 2, 2, null, null);
replay.drawText(new ol.geom.Point(point), null);
expect(replay.vertices).to.have.length(512);
expect(replay.indices).to.have.length(96);
});
it('sets part of its state during drawing', function() {
var point = [1000, 2000];
replay.drawText(point, 0, 2, 2, null, null);
replay.drawText(new ol.geom.Point(point), null);
var height = replay.currAtlas_.height;
var widths = replay.currAtlas_.width;
@@ -163,7 +162,7 @@ describe('ol.render.webgl.TextReplay', function() {
var point;
point = [1000, 2000];
replay.drawText(point, 0, 2, 2, null, null);
replay.drawText(new ol.geom.Point(point), null);
expect(replay.vertices).to.have.length(0);
expect(replay.indices).to.have.length(0);
});