diff --git a/Apps/Sandcastle/gallery/Labels.html b/Apps/Sandcastle/gallery/Labels.html
index aaf32e0826d2..f8e623ce7e5c 100644
--- a/Apps/Sandcastle/gallery/Labels.html
+++ b/Apps/Sandcastle/gallery/Labels.html
@@ -121,6 +121,17 @@
});
}
+function setRightToLeft() {
+ Sandcastle.declare(setRightToLeft);
+ Cesium.Label.enableRightToLeftDetection = true; //Only needs to be set once at the beginning of the application.
+ viewer.entities.add({
+ position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
+ label : {
+ text : 'Master (אדון): Hello\nתלמיד (student): שלום'
+ }
+ });
+}
+
Sandcastle.addToolbarMenu([{
text : 'Add label',
onselect : function() {
@@ -157,6 +168,12 @@
scaleByDistance();
Sandcastle.highlight(scaleByDistance);
}
+}, {
+ text : 'Set label with right-to-left language',
+ onselect : function() {
+ setRightToLeft();
+ Sandcastle.highlight(setRightToLeft);
+ }
}]);
Sandcastle.reset = function() {
diff --git a/CHANGES.md b/CHANGES.md
index a2263f7fe5c1..b00c0289f2b9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,7 @@ Change Log
### 1.39 - 2017-11-01
+* Added support for right-to-left languages in labels. [#5771](https://github.com/AnalyticalGraphicsInc/cesium/pull/5771)
* Added the ability to load Cesium's assets from the local file system if security permissions allow it. [#5830](https://github.com/AnalyticalGraphicsInc/cesium/issues/5830)
* Added function that inserts missing namespace declarations into KML files. [#5860](https://github.com/AnalyticalGraphicsInc/cesium/pull/5860)
* Added support for the layer.json `parentUrl` property in `CesiumTerrainProvider` to allow for compositing of tilesets.
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a9e5f51b5e3e..3d55d677144c 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -92,6 +92,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
* [Jannes Bolling](https://github.com/jbo023)
* [Logilab](https://www.logilab.fr/)
* [Florent Cayré](https://github.com/fcayre/)
+* [webiks](https://www.webiks.com)
+ * [Hod Bauer](https://github.com/hodbauer)
+ * [Yonatan Kra](https://github.com/yonatankra)
* [Novetta](http://www.novetta.com/)
* [Natanael Rivera](https://github.com/nrivera-Novetta/)
* [Justin Burr](https://github.com/jburr-nc/)
diff --git a/Source/Scene/Label.js b/Source/Scene/Label.js
index 813522182a0e..e6e7e2bd1abb 100644
--- a/Source/Scene/Label.js
+++ b/Source/Scene/Label.js
@@ -8,6 +8,7 @@ define([
'../Core/defineProperties',
'../Core/DeveloperError',
'../Core/DistanceDisplayCondition',
+ '../Core/freezeObject',
'../Core/NearFarScalar',
'./Billboard',
'./HeightReference',
@@ -24,6 +25,7 @@ define([
defineProperties,
DeveloperError,
DistanceDisplayCondition,
+ freezeObject,
NearFarScalar,
Billboard,
HeightReference,
@@ -32,6 +34,13 @@ define([
VerticalOrigin) {
'use strict';
+ var textTypes = freezeObject({
+ LTR : 0,
+ RTL : 1,
+ WEAK : 2,
+ BRACKETS : 3
+ });
+
function rebindAllGlyphs(label) {
if (!label._rebindAllGlyphs && !label._repositionAllGlyphs) {
// only push label if it's not already been marked dirty
@@ -110,7 +119,8 @@ define([
distanceDisplayCondition = DistanceDisplayCondition.clone(distanceDisplayCondition);
}
- this._text = defaultValue(options.text, '');
+ this._renderedText = undefined;
+ this._text = undefined;
this._show = defaultValue(options.show, true);
this._font = defaultValue(options.font, '30px sans-serif');
this._fillColor = Color.clone(defaultValue(options.fillColor, Color.WHITE));
@@ -147,6 +157,8 @@ define([
this._clusterShow = true;
+ this.text = defaultValue(options.text, '');
+
this._updateClamping();
}
@@ -281,6 +293,7 @@ define([
if (this._text !== value) {
this._text = value;
+ this._renderedText = Label.enableRightToLeftDetection ? reverseRtl(value) : value;
rebindAllGlyphs(this);
}
}
@@ -1167,7 +1180,7 @@ define([
this._verticalOrigin === other._verticalOrigin &&
this._horizontalOrigin === other._horizontalOrigin &&
this._heightReference === other._heightReference &&
- this._text === other._text &&
+ this._renderedText === other._renderedText &&
this._font === other._font &&
Cartesian3.equals(this._position, other._position) &&
Color.equals(this._fillColor, other._fillColor) &&
@@ -1196,5 +1209,202 @@ define([
return false;
};
+ /**
+ * Determines whether or not run the algorithm, that match the text of the label to right-to-left languages
+ * @memberof Label
+ * @type {Boolean}
+ * @default false
+ *
+ * @example
+ * // Example 1.
+ * // Set a label's rightToLeft before init
+ * Cesium.Label.enableRightToLeftDetection = true;
+ * var myLabelEntity = viewer.entities.add({
+ * label: {
+ * id: 'my label',
+ * text: 'זה טקסט בעברית \n ועכשיו יורדים שורה',
+ * }
+ * });
+ *
+ * @example
+ * // Example 2.
+ * var myLabelEntity = viewer.entities.add({
+ * label: {
+ * id: 'my label',
+ * text: 'English text'
+ * }
+ * });
+ * // Set a label's rightToLeft after init
+ * Cesium.Label.enableRightToLeftDetection = true;
+ * myLabelEntity.text = 'טקסט חדש';
+ */
+ Label.enableRightToLeftDetection = false;
+
+ function convertTextToTypes(text, rtlChars) {
+ var ltrChars = /[a-zA-Z0-9]/;
+ var bracketsChars = /[()[\]{}<>]/;
+ var parsedText = [];
+ var word = '';
+ var lastType = textTypes.LTR;
+ var currentType = '';
+ var textLength = text.length;
+ for (var textIndex = 0; textIndex < textLength; ++textIndex) {
+ var character = text.charAt(textIndex);
+ if (rtlChars.test(character)) {
+ currentType = textTypes.RTL;
+ }
+ else if (ltrChars.test(character)) {
+ currentType = textTypes.LTR;
+ }
+ else if (bracketsChars.test(character)) {
+ currentType = textTypes.BRACKETS;
+ }
+ else {
+ currentType = textTypes.WEAK;
+ }
+
+ if (textIndex === 0) {
+ lastType = currentType;
+ }
+
+ if (lastType === currentType && currentType !== textTypes.BRACKETS) {
+ word += character;
+ }
+ else {
+ if (word !== '') {
+ parsedText.push({Type : lastType, Word : word});
+ }
+ lastType = currentType;
+ word = character;
+ }
+ }
+ parsedText.push({Type : currentType, Word : word});
+ return parsedText;
+ }
+
+ function reverseWord(word) {
+ return word.split('').reverse().join('');
+ }
+
+ function spliceWord(result, pointer, word) {
+ return result.slice(0, pointer) + word + result.slice(pointer);
+ }
+
+ function reverseBrackets(bracket) {
+ switch(bracket) {
+ case '(':
+ return ')';
+ case ')':
+ return '(';
+ case '[':
+ return ']';
+ case ']':
+ return '[';
+ case '{':
+ return '}';
+ case '}':
+ return '{';
+ case '<':
+ return '>';
+ case '>':
+ return '<';
+ }
+ }
+
+ /**
+ *
+ * @param {String} value the text to parse and reorder
+ * @returns {String} the text as rightToLeft direction
+ * @private
+ */
+ function reverseRtl(value) {
+ var rtlChars = /[\u05D0-\u05EA]/;
+ var texts = value.split('\n');
+ var result = '';
+ for (var i = 0; i < texts.length; i++) {
+ var text = texts[i];
+ var rtlDir = rtlChars.test(text.charAt(0));
+ var parsedText = convertTextToTypes(text, rtlChars);
+
+ var splicePointer = 0;
+ var line = '';
+ for (var wordIndex = 0; wordIndex < parsedText.length; ++wordIndex) {
+ var subText = parsedText[wordIndex];
+ var reverse = subText.Type === textTypes.BRACKETS ? reverseBrackets(subText.Word) : subText.Word;
+ if (rtlDir) {
+ if (subText.Type === textTypes.RTL) {
+ line = reverseWord(subText.Word) + line;
+ splicePointer = 0;
+ }
+ else if (subText.Type === textTypes.LTR) {
+ line = spliceWord(line, splicePointer, subText.Word);
+ splicePointer += subText.Word.length;
+ }
+ else if (subText.Type === textTypes.WEAK || subText.Type === textTypes.BRACKETS) {
+ if (subText.Type === textTypes.WEAK && parsedText[wordIndex - 1].Type === textTypes.BRACKETS) {
+ line = reverseWord(subText.Word) + line;
+ }
+ else if (parsedText[wordIndex - 1].Type === textTypes.RTL) {
+ line = reverse + line;
+ splicePointer = 0;
+ }
+ else if (parsedText.length > wordIndex + 1) {
+ if (parsedText[wordIndex + 1].Type === textTypes.RTL) {
+ line = reverse + line;
+ splicePointer = 0;
+ }
+ else {
+ line = spliceWord(line, splicePointer, subText.Word);
+ splicePointer += subText.Word.length;
+ }
+ }
+ else {
+ line = spliceWord(line, 0, reverse);
+ }
+ }
+ }
+ else if (subText.Type === textTypes.RTL) {
+ line = spliceWord(line, splicePointer, reverseWord(subText.Word));
+ }
+ else if (subText.Type === textTypes.LTR) {
+ line += subText.Word;
+ splicePointer = line.length;
+ }
+ else if (subText.Type === textTypes.WEAK || subText.Type === textTypes.BRACKETS) {
+ if (wordIndex > 0) {
+ if (parsedText[wordIndex - 1].Type === textTypes.RTL) {
+ if (parsedText.length > wordIndex + 1) {
+ if (parsedText[wordIndex + 1].Type === textTypes.RTL) {
+ line = spliceWord(line, splicePointer, reverse);
+ }
+ else {
+ line += subText.Word;
+ splicePointer = line.length;
+ }
+ }
+ else {
+ line += subText.Word;
+ }
+ }
+ else {
+ line += subText.Word;
+ splicePointer = line.length;
+ }
+ }
+ else {
+ line += subText.Word;
+ splicePointer = line.length;
+ }
+ }
+ }
+
+ result += line;
+ if (i < texts.length - 1) {
+ result += '\n';
+ }
+ }
+ return result;
+ }
+
return Label;
});
diff --git a/Source/Scene/LabelCollection.js b/Source/Scene/LabelCollection.js
index efb9f4fd11b0..abc1692180c1 100644
--- a/Source/Scene/LabelCollection.js
+++ b/Source/Scene/LabelCollection.js
@@ -122,7 +122,7 @@ define([
}
function rebindAllGlyphs(labelCollection, label) {
- var text = label._text;
+ var text = label._renderedText;
var textLength = text.length;
var glyphs = label._glyphs;
var glyphsLength = glyphs.length;
@@ -290,7 +290,7 @@ define([
function repositionAllGlyphs(label, resolutionScale) {
var glyphs = label._glyphs;
- var text = label._text;
+ var text = label._renderedText;
var glyph;
var dimensions;
var lastLineWidth = 0;
diff --git a/Specs/Scene/LabelCollectionSpec.js b/Specs/Scene/LabelCollectionSpec.js
index 2f999ca1ea9d..9470681dc01e 100644
--- a/Specs/Scene/LabelCollectionSpec.js
+++ b/Specs/Scene/LabelCollectionSpec.js
@@ -1840,8 +1840,91 @@ defineSuite([
expect(newlinesBbox.height).toBeGreaterThan(originalBbox.height);
});
+ it('should not modify text when rightToLeft is false', function() {
+ var text = 'bla bla bla';
+ var label = labels.add({
+ text : text
+ });
+ scene.renderForSpecs();
+
+ expect(label.text).toEqual(text);
+ });
+
}, 'WebGL');
+ describe('right to left detection', function() {
+ beforeAll(function() {
+ Label.enableRightToLeftDetection = true;
+ });
+
+ afterAll(function() {
+ Label.enableRightToLeftDetection = false;
+ });
+
+ it('should not modify text when rightToLeft is true and there is no hebrew characters', function() {
+ var text = 'bla bla bla';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(text);
+ });
+
+ it('should reverse text when there is only hebrew characters and rightToLeft is true', function() {
+ var text = 'שלום';
+ var expectedText = 'םולש';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(text);
+ expect(label._renderedText).toEqual(expectedText);
+ });
+
+ it('should reverse part of text when there is mix of right-to-left and other kind of characters and rightToLeft is true', function() {
+ var text = 'Master (אדון): "Hello"\nתלמיד (student): "שלום"';
+ var expectedText = 'Master (ןודא): "Hello"\n"םולש" :(student) דימלת';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(text);
+ expect(label._renderedText).toEqual(expectedText);
+ });
+
+ it('should reverse all text and replace brackets when there is right-to-left characters and rightToLeft is true', function() {
+ var text = 'משפט [מורכב] {עם} תווים <מיוחדים special>';
+ var expectedText = ' םיוות {םע} [בכרומ] טפשמ';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(text);
+ expect(label._renderedText).toEqual(expectedText);
+ });
+
+ it('should reverse only text that detected as rtl text when it begin with non rtl characters when rightToLeft is true', function() {
+ var text = '(interesting sentence with hebrew characters) שלום(עליך)חביבי.';
+ var expectedText = '(interesting sentence with hebrew characters) יביבח(ךילע)םולש.';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(text);
+ expect(label._renderedText).toEqual(expectedText);
+ });
+
+ it('should not change nothing if it only non alphanumeric characters when rightToLeft is true', function() {
+ var text = '([{- -}])';
+ var expectedText = '([{- -}])';
+ var label = labels.add({
+ text : text
+ });
+
+ expect(label.text).toEqual(expectedText);
+ });
+ });
+
it('computes bounding sphere in 3D', function() {
var one = labels.add({
position : Cartesian3.fromDegrees(-50.0, -50.0, 0.0),