diff --git a/examples/vector-labels.js b/examples/vector-labels.js index ef80956195..c11bf40825 100644 --- a/examples/vector-labels.js +++ b/examples/vector-labels.js @@ -77,7 +77,7 @@ var getText = function(feature, resolution, dom) { text = ''; } else if (type == 'shorten') { text = text.trunc(12); - } else if (type == 'wrap') { + } else if (type == 'wrap' && dom.placement.value != 'line') { text = stringDivider(text, 16, '\n'); } diff --git a/src/ol/geom/flat/textpath.js b/src/ol/geom/flat/textpath.js index bd84defe32..87a20d607c 100644 --- a/src/ol/geom/flat/textpath.js +++ b/src/ol/geom/flat/textpath.js @@ -13,16 +13,12 @@ goog.require('ol.math'); * 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.>=} 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.>} The result array of null if `maxAngle` was - * exceeded. + * @return {Array.>} The result array of null if `maxAngle` was + * exceeded. Entries of the array are x, y, anchorX, angle, chunk. */ ol.geom.flat.textpath.lineString = function( - flatCoordinates, offset, end, stride, text, measure, startM, maxAngle, opt_result) { - var result = opt_result ? opt_result : []; + flatCoordinates, offset, end, stride, text, measure, startM, maxAngle) { + var result = []; // Keep text upright var reverse = flatCoordinates[offset] > flatCoordinates[end - stride]; @@ -37,11 +33,15 @@ ol.geom.flat.textpath.lineString = function( var segmentM = 0; var segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); - var index, previousAngle; + var chunk = ''; + var chunkLength = 0; + var data, index, previousAngle; for (var i = 0; i < numChars; ++i) { index = reverse ? numChars - i - 1 : i; - var char = text[index]; - var charLength = measure(char); + var char = text.charAt(index); + chunk = reverse ? char + chunk : chunk + char; + var charLength = measure(chunk) - chunkLength; + chunkLength += charLength; var charM = startM + charLength / 2; while (offset < end - stride && segmentM + segmentLength < charM) { x1 = x2; @@ -64,11 +64,27 @@ ol.geom.flat.textpath.lineString = function( 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]; + if (previousAngle == angle) { + if (reverse) { + data[0] = x; + data[1] = y; + data[2] = charLength / 2; + } + data[4] = chunk; + } else { + chunk = char; + chunkLength = charLength; + data = [x, y, charLength / 2, angle, chunk]; + if (reverse) { + result.unshift(data); + } else { + result.push(data); + } + previousAngle = angle; + } startM += charLength; } return result; diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 4550547397..6d214409b0 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -142,9 +142,9 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, /** * @private - * @type {Array.>} + * @type {!ol.Transform} */ - this.chars_ = []; + this.resetTransform_ = ol.transform.create(); }; ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext); @@ -569,34 +569,52 @@ ol.render.canvas.Replay.prototype.replay_ = function( case ol.render.canvas.Instruction.DRAW_CHARS: var begin = /** @type {number} */ (instruction[1]); var end = /** @type {number} */ (instruction[2]); - var images = /** @type {Array.} */ (instruction[3]); - // Remaining arguments in DRAW_CHARS are in alphabetical order - var baseline = /** @type {number} */ (instruction[4]); - declutterGroup = /** @type {ol.DeclutterGroup} */ (instruction[5]); - var exceedLength = /** @type {number} */ (instruction[6]); + var baseline = /** @type {number} */ (instruction[3]); + declutterGroup = /** @type {ol.DeclutterGroup} */ (instruction[4]); + var exceedLength = /** @type {number} */ (instruction[5]); + var fill = /** @type {boolean} */ (instruction[6]); var maxAngle = /** @type {number} */ (instruction[7]); var measure = /** @type {function(string):number} */ (instruction[8]); var offsetY = /** @type {number} */ (instruction[9]); - var text = /** @type {string} */ (instruction[10]); - var align = /** @type {number} */ (instruction[11]); - var textScale = /** @type {number} */ (instruction[12]); + var stroke = /** @type {boolean} */ (instruction[10]); + var strokeWidth = /** @type {number} */ (instruction[11]); + var text = /** @type {string} */ (instruction[12]); + var textAlign = /** @type {number} */ (instruction[13]); + var textScale = /** @type {number} */ (instruction[14]); 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) { - var fillHeight = images[images.length - 1].height; - 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 + (0.5 - baseline) * (label.height - fillHeight) - offsetY; - this.replayImage_(context, char[0], char[1], label, - anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, char[2], textScale, false, label.width); + var startM = (pathLength - textLength) * textAlign; + var parts = ol.geom.flat.textpath.lineString( + pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle); + if (parts) { + var c, cc, chars, label, part; + if (stroke) { + for (c = 0, cc = parts.length; c < cc; ++c) { + part = parts[c]; // x, y, anchorX, rotation, chunk + chars = /** @type {string} */ (part[4]); + label = /** @type {ol.render.canvas.TextReplay} */ (this).getImage(chars, false, true); + anchorX = /** @type {number} */ (part[2]) + strokeWidth; + anchorY = baseline * label.height + (0.5 - baseline) * strokeWidth - offsetY; + this.replayImage_(context, + /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, + anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, + /** @type {number} */ (part[3]), textScale, false, label.width); + } + } + if (fill) { + for (c = 0, cc = parts.length; c < cc; ++c) { + part = parts[c]; // x, y, anchorX, rotation, chunk + chars = /** @type {string} */ (part[4]); + label = /** @type {ol.render.canvas.TextReplay} */ (this).getImage(chars, true, false); + anchorX = /** @type {number} */ (part[2]); + anchorY = baseline * label.height - offsetY; + this.replayImage_(context, + /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, + anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, + /** @type {number} */ (part[3]), textScale, false, label.width); + } } } } diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index 5220b4a3d5..349e01152c 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -115,6 +115,12 @@ ol.render.canvas.TextReplay = function( */ this.strokeKey_ = ''; + /** + * @private + * @type {Object.} + */ + this.widths_ = {}; + while (ol.render.canvas.TextReplay.labelCache_.canExpireCache()) { ol.render.canvas.TextReplay.labelCache_.pop(); } @@ -158,19 +164,23 @@ ol.render.canvas.TextReplay.measureTextHeight = (function() { /** - * @this {Object} - * @param {CanvasRenderingContext2D} context Context. - * @param {number} pixelRatio Pixel ratio. + * @param {string} font Font. * @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; -}; +ol.render.canvas.TextReplay.measureTextWidth = (function() { + var measureContext; + var currentFont; + return function(font, text) { + if (!measureContext) { + measureContext = ol.dom.createCanvasContext2D(1, 1); + } + if (font != currentFont) { + currentFont = measureContext.font = font; + } + return measureContext.measureText(text).width; + }; +})(); /** @@ -180,24 +190,17 @@ ol.render.canvas.TextReplay.getTextWidth = function(context, pixelRatio, text) { * 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) { - currentWidth = context.measureText(lines[i]).width; - width = Math.max(width, currentWidth); - widths.push(currentWidth); - } - return width; - }; -})(); +ol.render.canvas.TextReplay.measureTextWidths = function(font, lines, widths) { + var numLines = lines.length; + var width = 0; + var currentWidth, i; + for (i = 0; i < numLines; ++i) { + currentWidth = ol.render.canvas.TextReplay.measureTextWidth(font, lines[i]); + width = Math.max(width, currentWidth); + widths.push(currentWidth); + } + return width; +}; /** @@ -263,7 +266,7 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) { this.endGeometry(geometry, feature); } else { - var label = this.getImage_(this.text_, !!this.textFillState_, !!this.textStrokeState_); + var label = this.getImage(this.text_, !!this.textFillState_, !!this.textStrokeState_); var width = label.width / this.pixelRatio; switch (geometryType) { case ol.geom.GeometryType.POINT: @@ -312,13 +315,12 @@ ol.render.canvas.TextReplay.prototype.drawText = function(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) { +ol.render.canvas.TextReplay.prototype.getImage = function(text, fill, stroke) { var label; var key = (stroke ? this.strokeKey_ : '') + this.textKey_ + text + (fill ? this.fillKey_ : ''); @@ -343,7 +345,9 @@ ol.render.canvas.TextReplay.prototype.getImage_ = function(text, fill, stroke) { Math.ceil((height + strokeWidth) * scale)); label = context.canvas; ol.render.canvas.TextReplay.labelCache_.set(key, label); - context.scale(scale, scale); + if (scale != 1) { + context.scale(scale, scale); + } context.font = textState.font; if (stroke) { context.strokeStyle = strokeState.strokeStyle; @@ -422,37 +426,36 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutte var textState = this.textState_; var baseline = ol.render.replay.TEXT_ALIGN[textState.textBaseline]; - 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 = {}; + var textAlign = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign]; + var text = this.text_; + var font = textState.font; + var textScale = this.textScale_; + var strokeWidth = strokeState ? strokeState.lineWidth * textScale / 2 : 0; + var widths = this.widths_; this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS, - begin, end, labels, baseline, declutterGroup, - textState.exceedLength, textState.maxAngle, - ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, pixelRatio * this.textScale_), - offsetY, this.text_, align, 1 + begin, end, baseline, declutterGroup, + textState.exceedLength, fill, textState.maxAngle, + function(text) { + var width = widths[text]; + if (!width) { + width = widths[text] = ol.render.canvas.TextReplay.measureTextWidth(font, text); + } + return width * textScale * pixelRatio; + }, + offsetY, stroke, strokeWidth * pixelRatio, text, textAlign, 1 ]); this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS, - begin, end, labels, baseline, declutterGroup, - textState.exceedLength, textState.maxAngle, - ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, this.textScale_), - offsetY, this.text_, align, 1 / pixelRatio + begin, end, baseline, declutterGroup, + textState.exceedLength, fill, textState.maxAngle, + function(text) { + var width = widths[text]; + if (!width) { + width = widths[text] = ol.render.canvas.TextReplay.measureTextWidth(font, text); + } + return width * textScale; + }, + offsetY, stroke, strokeWidth, text, textAlign, 1 / pixelRatio ]); }; diff --git a/test/rendering/ol/style/expected/text-multilinestring.png b/test/rendering/ol/style/expected/text-multilinestring.png index 7d4a567ca9..117aec74f8 100644 Binary files a/test/rendering/ol/style/expected/text-multilinestring.png and b/test/rendering/ol/style/expected/text-multilinestring.png differ diff --git a/test/rendering/ol/style/text.test.js b/test/rendering/ol/style/text.test.js index 3d130150c4..6e7c19a54b 100644 --- a/test/rendering/ol/style/text.test.js +++ b/test/rendering/ol/style/text.test.js @@ -358,7 +358,7 @@ describe('ol.rendering.style.Text', function() { })); vectorSource.addFeature(feature); map.getView().fit(vectorSource.getExtent()); - expectResemble(map, 'rendering/ol/style/expected/text-multilinestring.png', 6.9, done); + expectResemble(map, 'rendering/ol/style/expected/text-multilinestring.png', 7, done); }); it('renders text along a Polygon', function(done) { diff --git a/test/spec/ol/geom/flat/textpath.test.js b/test/spec/ol/geom/flat/textpath.test.js index d85932607f..64fca500ee 100644 --- a/test/spec/ol/geom/flat/textpath.test.js +++ b/test/spec/ol/geom/flat/textpath.test.js @@ -18,20 +18,20 @@ describe('textpath', 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]]); + expect(instructions).to.eql([[40, 0, 5, 0, 'foo']]); }); 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]]); + expect(instructions).to.eql([[5, 0, 5, 0, 'foo']]); }); 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]]); + expect(instructions).to.eql([[75, 0, 5, 0, 'foo']]); }); it('draws text on a vertical line', function() { @@ -39,33 +39,31 @@ describe('textpath', function() { 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]]); + expect(instructions).to.eql([[0, 40, 5, a, 'foo']]); }); 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]); + expect(instructions[0][3]).to.be(45 * Math.PI / 180); + expect(instructions.length).to.be(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]); + expect(instructions[0][3]).to.be(-45 * Math.PI / 180); + expect(instructions.length).to.be(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]); + expect(instructions[0]).to.eql([-20, 0, 5, 0, 'foo-foo-foo-foo']); + expect(instructions.length).to.be(1); }); it('renders angled text', function() { @@ -73,8 +71,10 @@ describe('textpath', function() { 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); + expect(instructions[0][3]).to.eql(45 * Math.PI / 180); + expect(instructions[0][4]).to.be('fo'); + expect(instructions[1][3]).to.eql(-45 * Math.PI / 180); + expect(instructions[1][4]).to.be('o'); }); it('respects maxAngle', function() { @@ -93,12 +93,13 @@ describe('textpath', function() { expect(instructions).to.not.be(undefined); }); - it('respects the begin option', function() { + it('respects the offset 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); + expect(instructions[0][3]).to.be(-45 * Math.PI / 180); + expect(instructions.length).to.be(1); }); it('respects the end option', function() { @@ -106,16 +107,8 @@ describe('textpath', function() { 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]); + expect(instructions[0][3]).to.be(45 * Math.PI / 180); + expect(instructions.length).to.be(1); }); });