Skip to content
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

Merged
merged 60 commits into from
Oct 25, 2017
Merged

Rtl labels support #5771

merged 60 commits into from
Oct 25, 2017

Conversation

hodbauer
Copy link
Contributor

@hodbauer hodbauer commented Aug 23, 2017

reference: #2543
For usage see Label documentation and Label sandcastle example.
Supports new lines
Currently supports Hebrew (should easily support other RTL languages)
Tasks:

  • Add support for more languages
  • Fix case when there is RTL characters, numbers and space (ex: משתמש10 )

@pjcozzi pjcozzi requested review from bagnell and ggetz August 23, 2017 23:31
@pjcozzi
Copy link
Contributor

pjcozzi commented Aug 23, 2017

Wow, very cool thanks for the nice contribution @hodbauer and @YonatanKra!

@ggetz could you take a first look at this then pass it on to @bagnell for a final review and merge?

Copy link
Contributor

@ggetz ggetz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hodbauer! I have a few comments.

@@ -157,6 +168,12 @@
scaleByDistance();
Sandcastle.highlight(scaleByDistance);
}
}, {
text : 'Set rtl',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more descriptive for the button label? Maybe Set label with right-to-left language

CHANGES.md Outdated
@@ -2,7 +2,7 @@ Change Log
==========

### 1.37 - 2017-09-01

* Added suppport for RTL labels
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a little more descriptive, maybe Added support for right-to-left languages in labels

},
set : function(value) {
if (this._rtl !== value) {
this._rtl = value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should call reverseRtl here in addition to when setting the text.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @ggetz for your comments. I will fix them after the weekend.
I have a specific question about this comment. Why to call reverseRtl here? If the text is written in rtl language, I don't see a reason to change the rtl flag while using it, the algorithm will work good even for ltr (left to right) languages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hodbauer!

If I understand correctly, if rtl is set to false, and you update text to an rtl language, then set rtl to true, it won't display correctly:

label.text = 'שלום';
label.rtl = true;

That may be confusing to people using this feature.

* @param {String} text the text to parse and reorder
* @returns {String} the text as rtl direction
*/
Label.prototype.reverseRtl = function(value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should be private, I don't think anyone should be calling this directly.

});

it('should reverse text when there is only hebrew characters and rtl is true', function() {
var text = 'שלום';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an additional tests that cover different scenarios like spaces and brackets in words?

result += subText.Word;
splicePointer = result.length;
}
else if (subText.Type === types.WEAK || subText.Type ===types.BRACKETS) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing after ===

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and on line 1354

splicePointer += subText.Word.length;
}
else if (subText.Type === types.WEAK || subText.Type ===types.BRACKETS) {
if (parsedText[wordIndex -1].Type === types.RTL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space after the operator here and throughout.

* make `reverseRtl` private
* fix multi-lines support
* add complex test
* fix some edge cases:
  * special characters after rtl characters when direction is not rtl
  * "Weak" characters after brackets at rtl direction
# Conflicts:
#	Apps/Sandcastle/gallery/Labels.html
#	CHANGES.md
#	CONTRIBUTORS.md
#	Source/Scene/Label.js
#	Specs/Scene/LabelCollectionSpec.js
@ggetz
Copy link
Contributor

ggetz commented Sep 6, 2017

Hi @hodbauer, I see you made updates. Are you ready for another look?

@hodbauer
Copy link
Contributor Author

hodbauer commented Sep 6, 2017

Hi @ggetz
I'll be glad if you go over it again.
I want to clarify why I didn't change the logic at the rtl setter. From a check that I've done it seems that the label ran the algorithm even if I didn't call the reverseRtl from the setter of rtl.

@ggetz
Copy link
Contributor

ggetz commented Sep 7, 2017

@hodbauer, everything looks good except I still think you should reconsider the rtl setter.

In the sandcastle example, if I run this:

    var label = viewer.entities.add({
        position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
        label : {
            text : 'Master (אדון): Hello\nתלמיד (student): שלום'
        }
    });
    
    label.rtl = true;

The rtl portion of the text will be reversed, which is not the same results as this:

viewer.entities.add({
        position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
        label : {
            text : 'Master (אדון): Hello\nתלמיד (student): שלום',
            rtl : true
        }
    });

@hodbauer
Copy link
Contributor Author

hodbauer commented Sep 7, 2017

Ok @ggetz I'll check this case again and update the code according to the results

@hodbauer
Copy link
Contributor Author

hodbauer commented Sep 7, 2017

I have a question @ggetz.
At the sandcastle I ran a simple case like that:

var label = viewer.entities.add({
        position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
        label : {
            text : 'Hi'
        }
    });
    label.text = 'Bye';

I was expect that the label at Cesium will be Bye, but it was Hi.
If this case not work, how the case that you mention before will work for me? What I was missing?
(It also was checked at the official Sandcastle)

@hodbauer
Copy link
Contributor Author

hodbauer commented Sep 7, 2017

After another check at Sandcastle I've got the next solution.

    var entity = viewer.entities.add({
        position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
        label : {
            text : 'Master (אדון): Hello\nתלמיד (student): שלום'
        }
    });
    entity.label.rtl = true;

now it just work fine.
Waiting for your response @ggetz.

@ggetz
Copy link
Contributor

ggetz commented Sep 7, 2017

@hodbauer My mistake, you're correct!

@bagnell You're up for final review and merge.

@mramato
Copy link
Contributor

mramato commented Sep 7, 2017

Are we okay with using rtl rather than rightToLeft, we usually don't use abbreviations in our API, but rtl is a fairly common one.

* fix some edge cases
* change the regex that identify right-to-left languages to use unicode instead of hebrew chars for js engines that can't parse this letters (like jscoverage)
@hodbauer
Copy link
Contributor Author

summary after another iteration:

Have we tried this on all browsers?

It tested on Chrome, firefox and edge

Please make sure that test coverage is high enough as there are a lot of branches.

hopefully all the branches covered by adding more tests. When Iv'e added the tests I fix some edge cases.
Another interesting thing that I found during this tests is that JScoverage lib not support hebrew characters, so I changed my regex that detect Hebrew from /[א-ת]/ to the unicode represent /[\u05D0-\u05EA]/.
Also I was tried to understand if I can support Arabic or other rtl languages and understand that if I cannot read it I shouldn't promise that it work fine...

@pjcozzi
Copy link
Contributor

pjcozzi commented Oct 18, 2017

@hodbauer thanks for the diligent updates.

@mramato did you want to look at this again and merge when ready?

@mramato
Copy link
Contributor

mramato commented Oct 19, 2017

Sure, I'll try and get to this before the end of the week.

* @private
*/
function reverseRtl(value) {
var rtlChars = /[\u05D0-\u05EA]/;
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but otherwise the rest of the code is the same for other languages?

yes. it only necessary when writing a RTL language in Cesium.

we can easily update this to handle the other languages.

basically yes, I do not do that, because i cannot read other RTL languages.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

* @type {Property}
* @default false
*/
rightToLeft: createPropertyDescriptor('rightToLeft')
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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?

Copy link
Contributor

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.

   /**
     * Enabled detection and support of right to left text.
     *
     * @type {Boolean}
     * @default false
     *
     * @example
     * //Set once at the start of the application
     * Cesium.Label.enableRightToLeftDetection = true;
     * 
     * var myLabelEntity = viewer.entities.add({
     *   label: {
     *     id: 'my label',
     *     text: 'זה טקסט בעברית \n ועכשיו יורדים שורה'
     *   }
     * });
     */
    Label.enableRightToLeftDetection = false;

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 check Label.enableRightToLeftDetection directly as well.

Thanks.

@@ -1196,5 +1249,184 @@ define([
return false;
};

function declareTypes() {
var TextTypes = {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 TextTypes everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, though you won't need the return statement. You can just put the declaration at the top of the file (above rebindAllGlyphs), it will be scoped to Label.js.

@mramato
Copy link
Contributor

mramato commented Oct 20, 2017

I merged in master for you and just have those few suggestions/questions.

Thanks again for the awesome work here, I'm sure a huge section of our community will be excited to have RTL support at last.

@hodbauer
Copy link
Contributor Author

I don't know why but I've got this error while running travis ci:

Core/WebGLConstants
 ✗ freezes render states
	Expected function to throw an exception.

Any help about this issue, it not part of my changes

@ggetz
Copy link
Contributor

ggetz commented Oct 23, 2017

@hodbauer Seems to be failing across multiple branches, I opened #5924

@hpinkos
Copy link
Contributor

hpinkos commented Oct 23, 2017

@hodbauer I fixed that test in master, so if you merge that into your branch CI should pass. Thanks!

@hodbauer
Copy link
Contributor Author

@ggetz @hpinkos @mramato thanks for solving the problem!
@mramato we could continue with the PR

@hodbauer
Copy link
Contributor Author

As @mramato offered, I changed the way to enable the right-to-left algorithm.
The new way is by setting a static parameter of Label, currently named Label.enableRightToLeftDetection to true, then every update to any label will run the algorithm.
The only issue about this as you can see is that the tests failed.
for example:

            Label.enableRightToLeftDetection = true;
            var text = 'שלום';
            var label = labels.add({
                text : text
            });

            scene.renderForSpecs();

            expect(label.text).not.toEqual(text);
            expect(label.text).toEqual(text.split('').reverse().join(''));

This test failed although I enable the right-to-left algorithm. moreover, when I've checked the sandecastle it worked as expected.
So, how to use this parameter during tests?

@mramato
Copy link
Contributor

mramato commented Oct 24, 2017

So, how to use this parameter during tests?

It looks like your code should be activating it, so I'm not sure what's going on. I can dig in a little further and let you know what I find.

@hodbauer
Copy link
Contributor Author

The code was OK, the problem was that during creation of a Label nothing calls to the text setter, so nothing run the algorithm.
The mandatory change that I've done at the last commit was to call the text setter in the constructor:

this.text = defaultValue(options.text, '');

my friend @YonatanKra that help me with this PR done it before. and me, by mistake, removed it in the last changes.

return;
}
this._originalValue = value;
value = reverseRtl(value);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@mramato
Copy link
Contributor

mramato commented Oct 25, 2017

Awesome work here @hodbauer and @YonatanKra! I submitted the tweak I mentioned, but otherwise I think this is ready so I'm going to merge.

If you find any issues or I accidentally broke something with my tweaks, please let me know.

Thanks again!

@mramato mramato merged commit 9933716 into CesiumGS:master Oct 25, 2017
@OmKoren OmKoren mentioned this pull request Jan 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants