diff --git a/src/ol/geom/flat/textpath.js b/src/ol/geom/flat/textpath.js index 89b4a04add..0f04401acf 100644 --- a/src/ol/geom/flat/textpath.js +++ b/src/ol/geom/flat/textpath.js @@ -34,74 +34,114 @@ export function drawTextOnPath( cache, rotation ) { - const result = []; + let x2 = flatCoordinates[offset]; + let y2 = flatCoordinates[offset + 1]; + let x1 = 0; + let y1 = 0; + let segmentLength = 0; + let segmentM = 0; + + function advance() { + x1 = x2; + y1 = y2; + offset += stride; + x2 = flatCoordinates[offset]; + y2 = flatCoordinates[offset + 1]; + segmentM += segmentLength; + segmentLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + do { + advance(); + } while (offset < end - stride && segmentM + segmentLength < startM); + + let interpolate = (startM - segmentM) / segmentLength; + const beginX = lerp(x1, x2, interpolate); + const beginY = lerp(y1, y2, interpolate); + + const startOffset = offset - stride; + const startLength = segmentM; + const endM = startM + scale * measureAndCacheTextWidth(font, text, cache); + while (offset < end - stride && segmentM + segmentLength < endM) { + advance(); + } + interpolate = (endM - segmentM) / segmentLength; + const endX = lerp(x1, x2, interpolate); + const endY = lerp(y1, y2, interpolate); // Keep text upright let reverse; if (rotation) { - const rotatedCoordinates = rotate( - flatCoordinates, - offset, - end, - stride, - rotation, - [flatCoordinates[offset], flatCoordinates[offset + 1]] - ); - reverse = - rotatedCoordinates[0] > - rotatedCoordinates[rotatedCoordinates.length - stride]; + const flat = [beginX, beginY, endX, endY]; + rotate(flat, 0, 4, 2, rotation, flat, flat); + reverse = flat[0] > flat[2]; } else { - reverse = flatCoordinates[offset] > flatCoordinates[end - stride]; + reverse = beginX > endX; } - const numChars = text.length; + const PI = Math.PI; + const result = []; + const singleSegment = startOffset + stride === offset; - let x1 = flatCoordinates[offset]; - let y1 = flatCoordinates[offset + 1]; - offset += stride; - let x2 = flatCoordinates[offset]; - let y2 = flatCoordinates[offset + 1]; - let segmentM = 0; - let segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); - let angleChanged = false; + offset = startOffset; + segmentLength = 0; + segmentM = startLength; + x2 = flatCoordinates[offset]; + y2 = flatCoordinates[offset + 1]; - let index, previousAngle; - for (let i = 0; i < numChars; ++i) { - index = reverse ? numChars - i - 1 : i; - const char = text[index]; - const charLength = scale * measureAndCacheTextWidth(font, char, cache); - const 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)); + // All on the same segment + if (singleSegment) { + advance(); + + let previousAngle = Math.atan2(y2 - y1, x2 - x1); + if (reverse) { + previousAngle += previousAngle > 0 ? -PI : PI; } - const segmentPos = charM - segmentM; + const x = (endX + beginX) / 2; + const y = (endY + beginY) / 2; + result[0] = [x, y, (endM - startM) / 2, previousAngle, text]; + return result; + } + + let previousAngle; + for (let i = 0, ii = text.length; i < ii; ) { + advance(); let angle = Math.atan2(y2 - y1, x2 - x1); if (reverse) { - angle += angle > 0 ? -Math.PI : Math.PI; + angle += angle > 0 ? -PI : PI; } if (previousAngle !== undefined) { let delta = angle - previousAngle; - angleChanged = angleChanged || delta !== 0; - delta += - delta > Math.PI ? -2 * Math.PI : delta < -Math.PI ? 2 * Math.PI : 0; + delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0; if (Math.abs(delta) > maxAngle) { return null; } } previousAngle = angle; - const interpolate = segmentPos / segmentLength; + + const iStart = i; + let charLength = 0; + for (; i < ii; ++i) { + const index = reverse ? ii - i - 1 : i; + const len = scale * measureAndCacheTextWidth(font, text[index], cache); + if ( + offset + stride < end && + segmentM + segmentLength < startM + charLength + len / 2 + ) { + break; + } + charLength += len; + } + if (i === iStart) { + continue; + } + const chars = reverse + ? text.substring(ii - iStart, ii - i) + : text.substring(iStart, i); + interpolate = (startM + charLength / 2 - segmentM) / segmentLength; const x = lerp(x1, x2, interpolate); const y = lerp(y1, y2, interpolate); - result[index] = [x, y, charLength / 2, angle, char]; + result.push([x, y, charLength / 2, angle, chars]); startM += charLength; } - return angleChanged - ? result - : [[result[0][0], result[0][1], result[0][2], result[0][3], text]]; + return result; } diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index d7a049c00b..587633de9c 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -918,7 +918,9 @@ class Executor { part = parts[c]; // x, y, anchorX, rotation, chunk chars = /** @type {string} */ (part[4]); label = this.createLabel(chars, textKey, '', strokeKey); - anchorX = /** @type {number} */ (part[2]) + strokeWidth; + anchorX = + /** @type {number} */ (part[2]) + + (textScale[0] < 0 ? -strokeWidth : strokeWidth); anchorY = baseline * label.height + ((0.5 - baseline) * 2 * strokeWidth * textScale[1]) / diff --git a/test/spec/ol/geom/flat/textpath.test.js b/test/spec/ol/geom/flat/textpath.test.js index bc43b43c62..15a11a534f 100644 --- a/test/spec/ol/geom/flat/textpath.test.js +++ b/test/spec/ol/geom/flat/textpath.test.js @@ -28,7 +28,7 @@ describe('ol.geom.flat.drawTextOnPath', function () { '', {} ); - expect(instructions).to.eql([[40, 0, 5, 0, 'foo']]); + expect(instructions).to.eql([[50, 0, 15, 0, 'foo']]); }); it('left-aligns text on a horizontal line', function () { @@ -45,7 +45,7 @@ describe('ol.geom.flat.drawTextOnPath', function () { '', {} ); - expect(instructions).to.eql([[5, 0, 5, 0, 'foo']]); + expect(instructions).to.eql([[15, 0, 15, 0, 'foo']]); }); it('right-aligns text on a horizontal line', function () { @@ -63,7 +63,7 @@ describe('ol.geom.flat.drawTextOnPath', function () { '', {} ); - expect(instructions).to.eql([[75, 0, 5, 0, 'foo']]); + expect(instructions).to.eql([[85, 0, 15, 0, 'foo']]); }); it('draws text on a vertical line', function () { @@ -82,7 +82,7 @@ describe('ol.geom.flat.drawTextOnPath', function () { {} ); const a = (90 * Math.PI) / 180; - expect(instructions).to.eql([[0, 40, 5, a, 'foo']]); + expect(instructions).to.eql([[0, 50, 15, a, 'foo']]); }); it('draws text on a diagonal line', function () { @@ -138,19 +138,19 @@ describe('ol.geom.flat.drawTextOnPath', function () { '', {} ); - expect(instructions[0]).to.eql([-20, 0, 5, 0, 'foo-foo-foo-foo']); + expect(instructions[0]).to.eql([50, 0, 75, 0, 'foo-foo-foo-foo']); expect(instructions.length).to.be(1); }); it('renders angled text', function () { const length = lineStringLength(angled, 0, angled.length, 2); - const startM = length / 2 - 15; + const startM = length / 2 - 20; const instructions = drawTextOnPath( angled, 0, angled.length, 2, - 'foo', + 'fooo', startM, Infinity, 1, @@ -159,11 +159,9 @@ describe('ol.geom.flat.drawTextOnPath', function () { {} ); expect(instructions[0][3]).to.eql((45 * Math.PI) / 180); - expect(instructions[0][4]).to.be('f'); - expect(instructions[1][3]).to.eql((45 * Math.PI) / 180); - expect(instructions[1][4]).to.be('o'); - expect(instructions[2][3]).to.eql((-45 * Math.PI) / 180); - expect(instructions[2][4]).to.be('o'); + expect(instructions[0][4]).to.be('fo'); + expect(instructions[1][3]).to.eql((-45 * Math.PI) / 180); + expect(instructions[1][4]).to.be('oo'); }); it('respects maxAngle', function () {