From 7660f3c522078dae9844d2c03a7a54ac153793b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Fri, 25 Sep 2020 23:28:54 +0200 Subject: [PATCH 1/5] Determine orientation by actual text start and end x --- src/ol/geom/flat/textpath.js | 114 ++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/src/ol/geom/flat/textpath.js b/src/ol/geom/flat/textpath.js index 89b4a04add..659ed19627 100644 --- a/src/ol/geom/flat/textpath.js +++ b/src/ol/geom/flat/textpath.js @@ -34,71 +34,89 @@ 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 + 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; - - 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)); + offset = startOffset; + segmentLength = 0; + segmentM = startLength; + x2 = flatCoordinates[offset]; + y2 = flatCoordinates[offset + 1]; + advance(); let angleChanged = false; - let index, previousAngle; - for (let i = 0; i < numChars; ++i) { - index = reverse ? numChars - i - 1 : i; + const PI = Math.PI; + const result = []; + let previousAngle = Math.atan2(y2 - y1, x2 - x1); + if (reverse) { + previousAngle += previousAngle > 0 ? -PI : PI; + } + for (let i = 0, ii = text.length; i < ii; ++i) { + const index = reverse ? ii - 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)); - } - const segmentPos = charM - segmentM; - let angle = Math.atan2(y2 - y1, x2 - x1); - if (reverse) { - angle += angle > 0 ? -Math.PI : Math.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; - if (Math.abs(delta) > maxAngle) { - return null; + advance(); + let angle = Math.atan2(y2 - y1, x2 - x1); + if (reverse) { + angle += angle > 0 ? -PI : PI; } + if (previousAngle !== undefined && angle !== previousAngle) { + let delta = angle - previousAngle; + delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0; + if (Math.abs(delta) > maxAngle) { + return null; + } + angleChanged = true; + } + previousAngle = angle; } - previousAngle = angle; - const interpolate = segmentPos / segmentLength; + interpolate = (charM - segmentM) / segmentLength; const x = lerp(x1, x2, interpolate); const y = lerp(y1, y2, interpolate); - result[index] = [x, y, charLength / 2, angle, char]; + result[index] = [x, y, charLength / 2, previousAngle, char]; startM += charLength; } return angleChanged From 528833b05b43d390047a93577861acab1a85c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Fri, 25 Sep 2020 21:13:05 +0200 Subject: [PATCH 2/5] Don't iterate each chracter if entire string fits in single segment --- src/ol/geom/flat/textpath.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/ol/geom/flat/textpath.js b/src/ol/geom/flat/textpath.js index 659ed19627..2f10739157 100644 --- a/src/ol/geom/flat/textpath.js +++ b/src/ol/geom/flat/textpath.js @@ -60,7 +60,7 @@ export function drawTextOnPath( const startOffset = offset - stride; const startLength = segmentM; - const endM = startM + measureAndCacheTextWidth(font, text, cache); + const endM = startM + scale * measureAndCacheTextWidth(font, text, cache); while (offset < end - stride && segmentM + segmentLength < endM) { advance(); } @@ -78,13 +78,14 @@ export function drawTextOnPath( reverse = beginX > endX; } + const singleSegment = startOffset + stride === offset; + offset = startOffset; segmentLength = 0; segmentM = startLength; x2 = flatCoordinates[offset]; y2 = flatCoordinates[offset + 1]; advance(); - let angleChanged = false; const PI = Math.PI; const result = []; @@ -92,24 +93,33 @@ export function drawTextOnPath( if (reverse) { previousAngle += previousAngle > 0 ? -PI : PI; } + + // All on the same segment + if (singleSegment) { + const x = (endX + beginX) / 2; + const y = (endY + beginY) / 2; + result[0] = [x, y, (endM - startM) / 2, previousAngle, text]; + return result; + } + for (let i = 0, ii = text.length; i < ii; ++i) { const index = reverse ? ii - i - 1 : i; const char = text[index]; const charLength = scale * measureAndCacheTextWidth(font, char, cache); const charM = startM + charLength / 2; + let angle; while (offset < end - stride && segmentM + segmentLength < charM) { advance(); - let angle = Math.atan2(y2 - y1, x2 - x1); + angle = Math.atan2(y2 - y1, x2 - x1); if (reverse) { angle += angle > 0 ? -PI : PI; } - if (previousAngle !== undefined && angle !== previousAngle) { - let delta = angle - previousAngle; - delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0; - if (Math.abs(delta) > maxAngle) { - return null; - } - angleChanged = true; + } + if (angle !== undefined) { + let delta = angle - previousAngle; + delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0; + if (Math.abs(delta) > maxAngle) { + return null; } previousAngle = angle; } @@ -119,7 +129,5 @@ export function drawTextOnPath( result[index] = [x, y, charLength / 2, previousAngle, char]; startM += charLength; } - return angleChanged - ? result - : [[result[0][0], result[0][1], result[0][2], result[0][3], text]]; + return result; } From 03f6cbb5a8a9b97c2fa1d9e0d211fad8592b757c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Fri, 25 Sep 2020 21:13:54 +0200 Subject: [PATCH 3/5] Draw substrings on straight line in one operation --- src/ol/geom/flat/textpath.js | 62 ++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/ol/geom/flat/textpath.js b/src/ol/geom/flat/textpath.js index 2f10739157..0f04401acf 100644 --- a/src/ol/geom/flat/textpath.js +++ b/src/ol/geom/flat/textpath.js @@ -78,6 +78,8 @@ export function drawTextOnPath( reverse = beginX > endX; } + const PI = Math.PI; + const result = []; const singleSegment = startOffset + stride === offset; offset = startOffset; @@ -85,48 +87,60 @@ export function drawTextOnPath( segmentM = startLength; x2 = flatCoordinates[offset]; y2 = flatCoordinates[offset + 1]; - advance(); - - const PI = Math.PI; - const result = []; - let previousAngle = Math.atan2(y2 - y1, x2 - x1); - if (reverse) { - previousAngle += previousAngle > 0 ? -PI : PI; - } // All on the same segment if (singleSegment) { + advance(); + + let previousAngle = Math.atan2(y2 - y1, x2 - x1); + if (reverse) { + previousAngle += previousAngle > 0 ? -PI : PI; + } const x = (endX + beginX) / 2; const y = (endY + beginY) / 2; result[0] = [x, y, (endM - startM) / 2, previousAngle, text]; return result; } - for (let i = 0, ii = text.length; i < ii; ++i) { - const index = reverse ? ii - i - 1 : i; - const char = text[index]; - const charLength = scale * measureAndCacheTextWidth(font, char, cache); - const charM = startM + charLength / 2; - let angle; - while (offset < end - stride && segmentM + segmentLength < charM) { - advance(); - angle = Math.atan2(y2 - y1, x2 - x1); - if (reverse) { - angle += angle > 0 ? -PI : PI; - } + 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 ? -PI : PI; } - if (angle !== undefined) { + if (previousAngle !== undefined) { let delta = angle - previousAngle; delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0; if (Math.abs(delta) > maxAngle) { return null; } - previousAngle = angle; } - interpolate = (charM - segmentM) / segmentLength; + previousAngle = angle; + + 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, previousAngle, char]; + result.push([x, y, charLength / 2, angle, chars]); startM += charLength; } return result; From b6ee7084c9cdebd4087fd7aec32618b31102af27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sat, 26 Sep 2020 14:31:40 +0200 Subject: [PATCH 4/5] Fix stroke for text along path wiht negative x scale --- src/ol/render/canvas/Executor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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]) / From b3968a42198f3f1f3d4d659fe3cf60fdb57a5b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 20 Oct 2020 23:20:10 +0200 Subject: [PATCH 5/5] Change textpath tests to work with changed method / return. --- test/spec/ol/geom/flat/textpath.test.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) 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 () {