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() {