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),