From 7cc45c967b2abd48e8f2d3dab404dee148c0a665 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 6 Nov 2020 17:54:02 +0100 Subject: [PATCH] Correct meaning of 'start' and 'end' text align for LTR text --- rendering/cases/rtl-text-align/expected.png | Bin 0 -> 5724 bytes rendering/cases/rtl-text-align/main.js | 192 ++++++++++++++++++ .../cases/text-style-linestring-nice/main.js | 4 +- src/ol/render/canvas/Executor.js | 24 ++- 4 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 rendering/cases/rtl-text-align/expected.png create mode 100644 rendering/cases/rtl-text-align/main.js diff --git a/rendering/cases/rtl-text-align/expected.png b/rendering/cases/rtl-text-align/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2dd0b456360a8a7ca5570407561f24d512f71e GIT binary patch literal 5724 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 literal 0 HcmV?d00001 diff --git a/rendering/cases/rtl-text-align/main.js b/rendering/cases/rtl-text-align/main.js new file mode 100644 index 0000000000..0fa3452173 --- /dev/null +++ b/rendering/cases/rtl-text-align/main.js @@ -0,0 +1,192 @@ +import CircleStyle from '../../../src/ol/style/Circle.js'; +import Feature from '../../../src/ol/Feature.js'; +import Fill from '../../../src/ol/style/Fill.js'; +import Map from '../../../src/ol/Map.js'; +import Point from '../../../src/ol/geom/Point.js'; +import Stroke from '../../../src/ol/style/Stroke.js'; +import Style from '../../../src/ol/style/Style.js'; +import Text from '../../../src/ol/style/Text.js'; +import VectorLayer from '../../../src/ol/layer/Vector.js'; +import VectorSource from '../../../src/ol/source/Vector.js'; +import View from '../../../src/ol/View.js'; + +const vectorSource = new VectorSource(); +let feature; + +// Latin - end (right) +feature = new Feature({ + geometry: new Point([-10, 50]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'Latin', + font: '24px Ubuntu', + textAlign: 'end', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +// Hebrew - start (right) +feature = new Feature({ + geometry: new Point([-10, 0]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'עִברִית', + font: '24px Ubuntu', + textAlign: 'start', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +// Arabic - start (right) +feature = new Feature({ + geometry: new Point([-10, -50]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'عربى', + font: '24px Ubuntu', + textAlign: 'start', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +// Latin - start (left) +feature = new Feature({ + geometry: new Point([10, 50]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'Latin', + font: '24px Ubuntu', + textAlign: 'start', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +// Hebrew - end (left) +feature = new Feature({ + geometry: new Point([10, 0]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'עִברִית', + font: '24px Ubuntu', + textAlign: 'end', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +// Arabic - end (left) +feature = new Feature({ + geometry: new Point([10, -50]), +}); +feature.setStyle( + new Style({ + text: new Text({ + text: 'عربى', + font: '24px Ubuntu', + textAlign: 'end', + fill: new Fill({ + color: 'black', + }), + stroke: new Stroke({ + color: 'white', + }), + }), + image: new CircleStyle({ + radius: 10, + fill: new Fill({ + color: 'cyan', + }), + }), + }) +); +vectorSource.addFeature(feature); + +new Map({ + pixelRatio: 1, + layers: [ + new VectorLayer({ + source: vectorSource, + }), + ], + target: 'map', + view: new View({ + center: [0, 0], + resolution: 1, + }), +}); + +render({tolerance: 0.01}); diff --git a/rendering/cases/text-style-linestring-nice/main.js b/rendering/cases/text-style-linestring-nice/main.js index 4dbdec77fe..9cfb1c0703 100644 --- a/rendering/cases/text-style-linestring-nice/main.js +++ b/rendering/cases/text-style-linestring-nice/main.js @@ -114,7 +114,7 @@ feature4.setStyle( text: 'negative offsetX', font: 'normal 400 10px/1 Ubuntu', offsetX: -10, - textAlign: 'start', + textAlign: 'end', textBaseline: 'top', placement: 'line', }), @@ -133,7 +133,7 @@ feature5.setStyle( font: '10px Ubuntu', offsetY: 5, scale: 0.7, - textAlign: 'end', + textAlign: 'start', placement: 'line', }), }) diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index d7a049c00b..1ec4c94eb0 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -75,6 +75,20 @@ function getDeclutterBox(replayImageOrLabelArgs) { return replayImageOrLabelArgs[3].declutterBox; } +const rtlRegEx = /[\u0591-\u07FF]/; + +/** + * @param {string} text Text. + * @param {string} align Alignment. + * @return {number} Text alignment. + */ +function horizontalTextAlign(text, align) { + if ((align === 'start' || align === 'end') && !rtlRegEx.test(text)) { + align = align === 'start' ? 'left' : 'right'; + } + return TEXT_ALIGN[align]; +} + class Executor { /** * @param {number} resolution Resolution. @@ -205,7 +219,10 @@ class Executor { textState.scale[0] * pixelRatio, textState.scale[1] * pixelRatio, ]; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const align = horizontalTextAlign( + text, + textState.textAlign || defaultTextAlign + ); const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0; @@ -541,7 +558,10 @@ class Executor { const strokeState = this.strokeStates[strokeKey]; const pixelRatio = this.pixelRatio; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const align = horizontalTextAlign( + text, + textState.textAlign || defaultTextAlign + ); const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline]; const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;