diff --git a/Apps/Sandcastle/gallery/Labels.html b/Apps/Sandcastle/gallery/Labels.html
index aaf32e0826d2..d77681485b4e 100644
--- a/Apps/Sandcastle/gallery/Labels.html
+++ b/Apps/Sandcastle/gallery/Labels.html
@@ -121,6 +121,17 @@
});
}
+function setRtl() {
+ Sandcastle.declare(setRtl);
+ viewer.entities.add({
+ position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
+ label : {
+ text : 'שלום',
+ rtl : true
+ }
+ });
+}
+
Sandcastle.addToolbarMenu([{
text : 'Add label',
onselect : function() {
@@ -157,6 +168,12 @@
scaleByDistance();
Sandcastle.highlight(scaleByDistance);
}
+}, {
+ text : 'Set rtl',
+ onselect : function() {
+ setRtl();
+ Sandcastle.highlight(setRtl);
+ }
}]);
Sandcastle.reset = function() {
diff --git a/CHANGES.md b/CHANGES.md
index 34f80cc4f458..f2abb2eebe87 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,7 +2,7 @@ Change Log
==========
### 1.37 - 2017-09-01
-
+* Added suppport for RTL labels
* Fixed `replaceState` bug that was causing the `CesiumViewer` demo application to crash in Safari and iOS
* Fixed issue where `Model` and `BillboardCollection` would throw an error if the globe is undefined [#5638](https://github.com/AnalyticalGraphicsInc/cesium/issues/5638)
* Fixed issue where the `Model` glTF cache loses reference to the model's buffer data. [#5720](https://github.com/AnalyticalGraphicsInc/cesium/issues/5720)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 089f762d9b52..257620f225be 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -86,6 +86,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
* [Jason Crow](https://github.com/jason-crow)
* [Flightradar24 AB](https://www.flightradar24.com)
* [Aleksei Kalmykov](https://github.com/kalmykov)
+* [webiks](https://www.webiks.com)
+ * [Hod Bauer](https://github.com/hodbauer)
+ * [Yonatan Kra](https://github.com/yonatankra)
## [Individual CLA](Documentation/Contributors/CLAs/individual-cla-agi-v1.0.txt)
* [Victor Berchet](https://github.com/vicb)
@@ -152,4 +155,5 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
* [Rishabh Shah](https://github.com/rms13)
* [Rudraksha Shah](https://github.com/Rudraksha20)
* [Cody Guldner](https://github.com/burn123)
+* [Yonatan Kra](https://github.com/yonatankra)
diff --git a/Source/DataSources/LabelGraphics.js b/Source/DataSources/LabelGraphics.js
index 52cf604de454..0b8b81217810 100644
--- a/Source/DataSources/LabelGraphics.js
+++ b/Source/DataSources/LabelGraphics.js
@@ -47,6 +47,7 @@ define([
* @param {Property} [options.scaleByDistance] A {@link NearFarScalar} Property used to set scale based on distance from the camera.
* @param {Property} [options.heightReference=HeightReference.NONE] A Property specifying what the height is relative to.
* @param {Property} [options.distanceDisplayCondition] A Property specifying at what distance from the camera that this label will be displayed.
+ * @param {Property} [options.rtl=false] A Property specifying if to modify text when there is possibly rtl characters.
*
* @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Labels.html|Cesium Sandcastle Labels Demo}
*/
@@ -93,6 +94,8 @@ define([
this._distanceDisplayConditionSubscription = undefined;
this._disableDepthTestDistance = undefined;
this._disableDepthTestDistanceSubscription = undefined;
+ this._rtl = undefined;
+ this._rtlSubscribtion = undefined;
this._definitionChanged = new Event();
this.merge(defaultValue(options, defaultValue.EMPTY_OBJECT));
@@ -318,7 +321,15 @@ define([
* @memberof LabelGraphics.prototype
* @type {Property}
*/
- disableDepthTestDistance : createPropertyDescriptor('disableDepthTestDistance')
+ disableDepthTestDistance : createPropertyDescriptor('disableDepthTestDistance'),
+
+ /**
+ * Gets or sets the ability to modify text characters direction.
+ * @memberof LabelGraphics.prototype
+ * @type {Property}
+ * @default false
+ */
+ rtl: createPropertyDescriptor('rtl')
});
/**
@@ -352,6 +363,7 @@ define([
result.scaleByDistance = this.scaleByDistance;
result.distanceDisplayCondition = this.distanceDisplayCondition;
result.disableDepthTestDistance = this.disableDepthTestDistance;
+ result.rtl = this.rtl;
return result;
};
@@ -389,6 +401,7 @@ define([
this.scaleByDistance = defaultValue(this.scaleByDistance, source.scaleByDistance);
this.distanceDisplayCondition = defaultValue(this.distanceDisplayCondition, source.distanceDisplayCondition);
this.disableDepthTestDistance = defaultValue(this.disableDepthTestDistance, source.disableDepthTestDistance);
+ this.rtl = defaultValue(this.rtl, source.rtl);
};
return LabelGraphics;
diff --git a/Source/DataSources/LabelVisualizer.js b/Source/DataSources/LabelVisualizer.js
index 208fd19c31c0..87df78e5692c 100644
--- a/Source/DataSources/LabelVisualizer.js
+++ b/Source/DataSources/LabelVisualizer.js
@@ -49,6 +49,7 @@ define([
var defaultHorizontalOrigin = HorizontalOrigin.CENTER;
var defaultVerticalOrigin = VerticalOrigin.CENTER;
var defaultDisableDepthTestDistance = 0.0;
+ var defaultRtl = false;
var position = new Cartesian3();
var fillColor = new Color();
@@ -165,6 +166,7 @@ define([
label.scaleByDistance = Property.getValueOrUndefined(labelGraphics._scaleByDistance, time, scaleByDistance);
label.distanceDisplayCondition = Property.getValueOrUndefined(labelGraphics._distanceDisplayCondition, time, distanceDisplayCondition);
label.disableDepthTestDistance = Property.getValueOrDefault(labelGraphics._disableDepthTestDistance, time, defaultDisableDepthTestDistance);
+ label.rtl = Property.getValueOrDefault(labelGraphics._rtl, time, defaultRtl);
}
return true;
};
diff --git a/Source/Scene/Label.js b/Source/Scene/Label.js
index 813522182a0e..172977aa038e 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,
@@ -147,6 +149,9 @@ define([
this._clusterShow = true;
+ this._rtl = defaultValue(options.rtl, false);
+ this.text = defaultValue(options.text, '');
+
this._updateClamping();
}
@@ -279,6 +284,15 @@ define([
}
//>>includeEnd('debug');
+ if (this.rtl) {
+ if (this._originalValue === value) {
+ value = this._text;
+ return;
+ }
+ this._originalValue = value;
+ value = this.reverseRtl(value);
+ }
+
if (this._text !== value) {
this._text = value;
rebindAllGlyphs(this);
@@ -1030,6 +1044,42 @@ define([
}
}
}
+ },
+
+ /**
+ * Determines whether or not run the reverseRtl algorithm on the text of the label
+ * @memberof Label.prototype
+ * @type {Boolean}
+ * @default false
+ *
+ * @example
+ * // Example 1.
+ * // Set a label's rtl during init
+ * var myLabelEntity = viewer.entities.add({
+ * id: 'my label',
+ * text: 'זה טקסט בעברית \n ועכשיו יורדים שורה',
+ * rtl: true
+ * });
+ *
+ * @example
+ * // Example 2.
+ * var myLabelEntity = viewer.entities.add({
+ * id: 'my label',
+ * text: 'English text'
+ * });
+ * // Set a label's rtl after init
+ * myLabelEntity.rtl = true;
+ * myLabelEntity.text = 'טקסט חדש'
+ */
+ rtl : {
+ get : function() {
+ return this._rtl;
+ },
+ set : function(value) {
+ if (this._rtl !== value) {
+ this._rtl = value;
+ }
+ }
}
});
@@ -1196,5 +1246,171 @@ define([
return false;
};
+ function declareTypes() {
+ var TextTypes = {
+ LTR : 0,
+ RTL : 1,
+ WEAK : 2,
+ BRACKETS : 3
+ };
+ return freezeObject(TextTypes);
+ }
+
+ function convertTextToTypes(text, rtlDir, rtlChars) {
+ var ltrChars = /[a-zA-Z0-9]/;
+ var bracketsChars = /[()[\]{}<>]/;
+ var parsedText = [];
+ var word = '';
+ var types = declareTypes();
+ var lastType = rtlDir ? types.RTL : types.LTR;
+ var currentType = '';
+ var textLength = text.length;
+ for (var textIndex = 0; textIndex < textLength; ++textIndex) {
+ var character = text.charAt(textIndex);
+ if (rtlChars.test(character)) {
+ currentType = types.RTL;
+ }
+ else if (ltrChars.test(character)) {
+ currentType = types.LTR;
+ }
+ else if (bracketsChars.test(character)) {
+ currentType = types.BRACKETS;
+ }
+ else {
+ currentType = types.WEAK;
+ }
+
+ if (lastType === currentType && currentType !== types.BRACKETS) {
+ word += character;
+ }
+ else {
+ 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} text the text to parse and reorder
+ * @returns {String} the text as rtl direction
+ */
+ Label.prototype.reverseRtl = function(value) {
+ var rtlChars = /[א-ת]/;
+ 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, rtlDir, rtlChars);
+
+ var types = declareTypes();
+
+ var splicePointer = 0;
+ for(var wordIndex = 0; wordIndex < parsedText.length; ++wordIndex) {
+ var subText = parsedText[wordIndex];
+ var reverse = subText.Type === types.BRACKETS ? reverseBrackets(subText.Word) : subText.Word;
+ if(rtlDir) {
+ if (subText.Type === types.RTL) {
+ result = reverseWord(subText.Word) + result;
+ splicePointer = 0;
+ }
+ else if (subText.Type === types.LTR) {
+ result = spliceWord(result,splicePointer, subText.Word);
+ splicePointer += subText.Word.length;
+ }
+ else if (subText.Type === types.WEAK || subText.Type ===types.BRACKETS) {
+ if (parsedText[wordIndex -1].Type === types.RTL) {
+ result = reverse + result;
+ splicePointer = 0;
+ }
+ else if (parsedText.length > wordIndex +1) {
+ if (parsedText[wordIndex +1].Type === types.RTL) {
+ result = reverse + result;
+ splicePointer = 0;
+ }
+ else {
+ result = spliceWord(result,splicePointer, subText.Word);
+ splicePointer += subText.Word.length;
+ }
+ }
+ else {
+ result = spliceWord(result,splicePointer, subText.Word);
+ }
+ }
+ }
+ else if (subText.Type === types.RTL) {
+ result = spliceWord(result, splicePointer, reverseWord(subText.Word));
+ }
+ else if (subText.Type === types.LTR) {
+ result += subText.Word;
+ splicePointer = result.length;
+ }
+ else if (subText.Type === types.WEAK || subText.Type ===types.BRACKETS) {
+ if (wordIndex > 0) {
+ if (parsedText[wordIndex -1].Type === types.RTL) {
+ if (parsedText.length > wordIndex +1) {
+ if (parsedText[wordIndex +1].Type === types.LTR) {
+ result += subText.Word;
+ splicePointer = result.length;
+ }
+ else if (parsedText[wordIndex +1].Type === types.RTL) {
+ result = spliceWord(result, splicePointer, reverse);
+ }
+ }
+ else {
+ result += subText.Word;
+ }
+ }
+ else {
+ result += subText.Word;
+ splicePointer = result.length;
+ }
+ }
+ else {
+ result += subText.Word;
+ splicePointer = result.length;
+ }
+ }
+ }
+ if (i < texts.length - 1) {
+ result += '\n';
+ }
+ }
+ return result;
+ };
+
return Label;
});
diff --git a/Specs/DataSources/LabelGraphicsSpec.js b/Specs/DataSources/LabelGraphicsSpec.js
index 282fc3d7a824..1539dbbe7b35 100644
--- a/Specs/DataSources/LabelGraphicsSpec.js
+++ b/Specs/DataSources/LabelGraphicsSpec.js
@@ -40,7 +40,8 @@ defineSuite([
pixelOffsetScaleByDistance : new NearFarScalar(13, 14, 15, 16),
scaleByDistance : new NearFarScalar(17, 18, 19, 20),
distanceDisplayCondition : new DistanceDisplayCondition(10.0, 100.0),
- disableDepthTestDistance : 10.0
+ disableDepthTestDistance : 10.0,
+ rtl : false
};
var label = new LabelGraphics(options);
@@ -60,6 +61,7 @@ defineSuite([
expect(label.scaleByDistance).toBeInstanceOf(ConstantProperty);
expect(label.distanceDisplayCondition).toBeInstanceOf(ConstantProperty);
expect(label.disableDepthTestDistance).toBeInstanceOf(ConstantProperty);
+ expect(label.rtl).toBeInstanceOf(ConstantProperty);
expect(label.text.getValue()).toEqual(options.text);
expect(label.font.getValue()).toEqual(options.font);
@@ -77,6 +79,7 @@ defineSuite([
expect(label.scaleByDistance.getValue()).toEqual(options.scaleByDistance);
expect(label.distanceDisplayCondition.getValue()).toEqual(options.distanceDisplayCondition);
expect(label.disableDepthTestDistance.getValue()).toEqual(options.disableDepthTestDistance);
+ expect(label.rtl.getValue()).toEqual(options.rtl);
});
it('merge assigns unassigned properties', function() {
@@ -98,6 +101,7 @@ defineSuite([
source.scaleByDistance = new ConstantProperty(new NearFarScalar(1.0, 0.0, 3.0e9, 0.0));
source.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition(10.0, 100.0));
source.disableDepthTestDistance = new ConstantProperty(10.0);
+ source.rtl = new ConstantProperty(false);
var target = new LabelGraphics();
target.merge(source);
@@ -119,6 +123,7 @@ defineSuite([
expect(target.scaleByDistance).toBe(source.scaleByDistance);
expect(target.distanceDisplayCondition).toBe(source.distanceDisplayCondition);
expect(target.disableDepthTestDistance).toBe(source.disableDepthTestDistance);
+ expect(target.rtl).toBe(source.rtl);
});
it('merge does not assign assigned properties', function() {
@@ -140,6 +145,7 @@ defineSuite([
source.scaleByDistance = new ConstantProperty(new NearFarScalar(1.0, 0.0, 3.0e9, 0.0));
source.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition(10.0, 100.0));
source.disableDepthTestDistance = new ConstantProperty(10.0);
+ source.rtl = new ConstantProperty(true);
var text = new ConstantProperty('my text');
var font = new ConstantProperty('10px serif');
@@ -158,6 +164,7 @@ defineSuite([
var scaleByDistance = new ConstantProperty(new NearFarScalar());
var distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition());
var disableDepthTestDistance = new ConstantProperty(20.0);
+ var rtl = new ConstantProperty(false);
var target = new LabelGraphics();
target.text = text;
@@ -177,6 +184,7 @@ defineSuite([
target.scaleByDistance = scaleByDistance;
target.distanceDisplayCondition = distanceDisplayCondition;
target.disableDepthTestDistance = disableDepthTestDistance;
+ target.rtl = rtl;
target.merge(source);
@@ -197,6 +205,7 @@ defineSuite([
expect(target.scaleByDistance).toBe(scaleByDistance);
expect(target.distanceDisplayCondition).toBe(distanceDisplayCondition);
expect(target.disableDepthTestDistance).toBe(disableDepthTestDistance);
+ expect(target.rtl).toBe(rtl);
});
it('clone works', function() {
@@ -218,6 +227,7 @@ defineSuite([
source.scaleByDistance = new ConstantProperty(new NearFarScalar(1.0, 0.0, 3.0e9, 0.0));
source.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition(10.0, 100.0));
source.disableDepthTestDistance = new ConstantProperty(10.0);
+ source.rtl = new ConstantProperty(false);
var result = source.clone();
expect(result.text).toBe(source.text);
@@ -237,6 +247,7 @@ defineSuite([
expect(result.scaleByDistance).toBe(source.scaleByDistance);
expect(result.distanceDisplayCondition).toBe(source.distanceDisplayCondition);
expect(result.disableDepthTestDistance).toBe(source.disableDepthTestDistance);
+ expect(result.rtl).toBe(source.rtl);
});
it('merge throws if source undefined', function() {
diff --git a/Specs/DataSources/LabelVisualizerSpec.js b/Specs/DataSources/LabelVisualizerSpec.js
index c5a2112c6ac6..070355f7d155 100644
--- a/Specs/DataSources/LabelVisualizerSpec.js
+++ b/Specs/DataSources/LabelVisualizerSpec.js
@@ -161,6 +161,7 @@ defineSuite([
label.scaleByDistance = new ConstantProperty(new NearFarScalar());
label.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition());
label.disableDepthTestDistance = new ConstantProperty(10.0);
+ label.rtl = new ConstantProperty(false);
visualizer.update(time);
@@ -188,6 +189,7 @@ defineSuite([
expect(l.scaleByDistance).toEqual(testObject.label.scaleByDistance.getValue(time));
expect(l.distanceDisplayCondition).toEqual(testObject.label.distanceDisplayCondition.getValue(time));
expect(l.disableDepthTestDistance).toEqual(testObject.label.disableDepthTestDistance.getValue(time));
+ expect(l.rtl).toEqual(testObject.label.rtl.getValue(time));
testObject.position = new ConstantProperty(new Cartesian3(5678, 1234, 1293434));
label.text = new ConstantProperty('b');
@@ -207,6 +209,7 @@ defineSuite([
label.scaleByDistance = new ConstantProperty(new NearFarScalar());
label.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition());
label.disableDepthTestDistance = new ConstantProperty(20.0);
+ label.rtl = new ConstantProperty(true);
visualizer.update(time);
expect(l.position).toEqual(testObject.position.getValue(time));
@@ -227,6 +230,7 @@ defineSuite([
expect(l.scaleByDistance).toEqual(testObject.label.scaleByDistance.getValue(time));
expect(l.distanceDisplayCondition).toEqual(testObject.label.distanceDisplayCondition.getValue(time));
expect(l.disableDepthTestDistance).toEqual(testObject.label.disableDepthTestDistance.getValue(time));
+ expect(l.rtl).toEqual(testObject.label.rtl.getValue(time));
label.show = new ConstantProperty(false);
visualizer.update(time);
diff --git a/Specs/Scene/LabelCollectionSpec.js b/Specs/Scene/LabelCollectionSpec.js
index 2f999ca1ea9d..0ff08a6035b9 100644
--- a/Specs/Scene/LabelCollectionSpec.js
+++ b/Specs/Scene/LabelCollectionSpec.js
@@ -1840,6 +1840,43 @@ defineSuite([
expect(newlinesBbox.height).toBeGreaterThan(originalBbox.height);
});
+ it('should not modify text when rtl is false', function() {
+ var text = 'bla bla bla';
+ var label = labels.add({
+ text : text
+ });
+ scene.renderForSpecs();
+
+ expect(label.rtl).toEqual(false);
+ expect(label.text).toEqual(text);
+ });
+
+ it('should not modify text when rtl is true and there is no hebrew characters', function() {
+ var text = 'bla bla bla';
+ var label = labels.add({
+ text : text,
+ rtl : true
+ });
+ scene.renderForSpecs();
+
+ expect(label.rtl).toEqual(true);
+ expect(label.text).toEqual(text);
+ });
+
+ it('should reverse text when there is only hebrew characters and rtl is true', function() {
+ var text = 'שלום';
+ var label = labels.add({
+ text : text,
+ rtl: true
+ });
+
+ scene.renderForSpecs();
+
+ expect(label.rtl).toEqual(true);
+ expect(label.text).not.toEqual(text);
+ expect(label.text).toEqual(text.split('').reverse().join(''));
+ });
+
}, 'WebGL');
it('computes bounding sphere in 3D', function() {