-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rtl labels support #5771
Rtl labels support #5771
Changes from 53 commits
60da6aa
fc812bc
8762607
64fd8d2
e3c601f
db98c03
5826c7e
775e637
f09b883
70c3ab6
8d18aa3
52dc64f
8350668
a0f0c1a
41cea83
84354e4
c22adcf
f91e96d
281d4d3
c1adde1
6d65918
5f304a8
304e774
f7dfcaa
10b05d2
8878a7b
12fe81d
15a3c9c
9ab1704
04538ba
eb9e155
97c5c36
7245d72
4a69abf
cfb6419
ea5ceeb
c098454
d3df024
7068841
bdb582e
99010f2
ab9ac19
84e5d95
eec1e4d
b3971a9
69a49e8
bcc762c
7a0c0e3
3432f03
df1ddea
80b370b
0020337
abc7b9e
fac9512
11b485e
32c5657
118abe4
e109e98
c2aa9cc
6bca30d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -110,7 +112,6 @@ define([ | |
distanceDisplayCondition = DistanceDisplayCondition.clone(distanceDisplayCondition); | ||
} | ||
|
||
this._text = defaultValue(options.text, ''); | ||
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 +148,9 @@ define([ | |
|
||
this._clusterShow = true; | ||
|
||
this._rightToLeft = defaultValue(options.rightToLeft, false); | ||
this.text = defaultValue(options.text, ''); | ||
|
||
this._updateClamping(); | ||
} | ||
|
||
|
@@ -279,6 +283,15 @@ define([ | |
} | ||
//>>includeEnd('debug'); | ||
|
||
if (this.rightToLeft) { | ||
if (this._originalValue === value) { | ||
value = this._text; | ||
return; | ||
} | ||
this._originalValue = value; | ||
value = reverseRtl(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed that we are mutating the value supplied by the user so that the getter is actually the reversed text. I think this is undesired behavior. With few exceptions, a simple setter/getter should always return the value assigned to it. I can submit a change to prevent this, but if you feel strongly about it, please let me know. |
||
} | ||
|
||
if (this._text !== value) { | ||
this._text = value; | ||
rebindAllGlyphs(this); | ||
|
@@ -1030,6 +1043,46 @@ 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 rightToLeft during init | ||
* var myLabelEntity = viewer.entities.add({ | ||
* label: { | ||
* id: 'my label', | ||
* text: 'זה טקסט בעברית \n ועכשיו יורדים שורה', | ||
* rightToLeft: true | ||
* } | ||
* }); | ||
* | ||
* @example | ||
* // Example 2. | ||
* var myLabelEntity = viewer.entities.add({ | ||
* label: { | ||
* id: 'my label', | ||
* text: 'English text' | ||
* } | ||
* }); | ||
* // Set a label's rightToLeft after init | ||
* myLabelEntity.rightToLeft = true; | ||
* myLabelEntity.text = 'טקסט חדש'; | ||
*/ | ||
rightToLeft : { | ||
get : function() { | ||
return this._rightToLeft; | ||
}, | ||
set : function(value) { | ||
if (this._rightToLeft !== value) { | ||
this._rightToLeft = value; | ||
} | ||
} | ||
} | ||
}); | ||
|
||
|
@@ -1196,5 +1249,184 @@ define([ | |
return false; | ||
}; | ||
|
||
function declareTypes() { | ||
var TextTypes = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just declare this at the top of the file instead of creating it every time. Then just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mramato If I understand you, you want me to change the code to something like: var TextTypes = freezeObject({
LTR : 0,
RTL : 1,
WEAK : 2,
BRACKETS : 3
});
return TextTypes; and put it inside the define near to the glyph functions as is (without a function scope)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, though you won't need the |
||
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 (textIndex === 0) { | ||
lastType = currentType; | ||
} | ||
|
||
if (lastType === currentType && currentType !== types.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]/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So if I'm understanding this code correctly, this regex detects whether the letter is RTL (in this case specifically Hebrew) but otherwise the rest of the code is the same for other languages? So if we update this regex for other known RTL character sets (perhaps thise discussed here and here Is that correct? If so that's awesome and we can easily update this to handle the other languages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes. it only necessary when writing a RTL language in Cesium.
basically yes, I do not do that, because i cannot read other RTL languages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, we can merge this with just Hebrew and then I'll open a second PR with an updated regex to support additional languages. |
||
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; | ||
var line = ''; | ||
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) { | ||
line = reverseWord(subText.Word) + line; | ||
splicePointer = 0; | ||
} | ||
else if (subText.Type === types.LTR) { | ||
line = spliceWord(line, splicePointer, subText.Word); | ||
splicePointer += subText.Word.length; | ||
} | ||
else if (subText.Type === types.WEAK || subText.Type === types.BRACKETS) { | ||
if (subText.Type === types.WEAK && parsedText[wordIndex - 1].Type === types.BRACKETS) { | ||
line = reverseWord(subText.Word) + line; | ||
} | ||
else if (parsedText[wordIndex - 1].Type === types.RTL) { | ||
line = reverse + line; | ||
splicePointer = 0; | ||
} | ||
else if (parsedText.length > wordIndex + 1) { | ||
if (parsedText[wordIndex + 1].Type === types.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 === types.RTL) { | ||
line = spliceWord(line, splicePointer, reverseWord(subText.Word)); | ||
} | ||
else if (subText.Type === types.LTR) { | ||
line += subText.Word; | ||
splicePointer = line.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.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; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically an optimization, right? If the string doesn't contain RTL characters it is a no-op?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is just an optimization, how do you feel about having a single static
Label.enableRightToLeftDetection
variable that an application sets once globally rather than having to set it on every label? It will save memory when there are many labels and I doubt it will have a performance impact in RTL apps since I would expect most of the labels to be RTL in that case. Thoughts?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds very interesting.
I'm not sure 100% how to implement it, but if it is the right way then I should do it as you offered
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after a little bit of thinking I understand how to implement it, it will clean a lot of the work and it is nice.
my question is: how to declare the static variable so each application could manage it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just add a property directly on the
Label
object. Something like this should work.This will show up as static in the documentation and users just set
Cesium.Label.enableRightToLeftDetection = true
once in their app to use it. In your own code, you would just checkLabel.enableRightToLeftDetection
directly as well.Thanks.