Skip to main content

Developing Cesium with GWT and JsInterop

This is a guest post by Serge Silaev about his GWT wrapper, a Java application for developing with Cesium using Google Web Toolkit (GWT) 2.8.0 and JsInterop. The work expands that of Rich Kadel, who introduced a GWT wrapper for Cesium in 2014. - Sarah

JavaScript is the most common language for Web development and is suitable for very diverse uses. But some developers prefer to work in Java (Java EE, Java SB, EJB), especially those involved in large, complex development efforts. For teams with some members working in Java, it is inconvenient and inefficient to do the integration of JavaScript code in the project directly; it is much easier to implement and use a JavaScript wrapper.

The Google Web Toolkit (GWT) is ideal for these situations, with its solutions for integrating JavaScript code: JSNI (JavaScript Native Interface GWT < = 2.7.0) and JsInterop (GWT > = 2.8.0). GWT allows Java developers to create browser client-side user interfaces in Java, and it compiles Java code into minimized and optimized JavaScript. Because it is a low-level library, GWT is a useful Java user interface, and, accordingly, all the solutions developed on GWT are easily integrated into high-level decisions. Smart GWT, GXT, VAADIN, and others are all sophisticated and functional solutions based on GWT.

GXT Application uses gtw-cs (gwt-olcs).

Until October 20, 2016 (Release 2.8.0), there was GWT 2.7.0 and JSNI. While this was a helpful tool, it had drawbacks:

  1. The redundancy of the code for the wrapper
  2. No opportunity for easy Callback implementation
  3. No easy function creation, and setting this function as a parameter of other functions
  4. No operator new
  5. Getters/setters methods are required to access class properties

Since the transition to JsInterop, Java code has become more like native JavaScript code (that’s what JsInterop was created for), and accordingly, a company that develops or writes a JavaScript application can easily work with the Java community with minimal alteration of the code. This can be seen in the examples in my library. Some things can’t be realized on JsInterop, and we have to use JSNI with its JavaScriptObject, but I think in the foreseeable future JsInterop opportunities will grow and may completely replace JSNI.

GWT and JsInterop

In addition to JSNI, JsInterop appeared in GWT 2.8.0. JsInterop makes it easy to implement the wrapper in JavaScript, and also greatly reduces the amount of code. JsInterop is described in detail in Nextget GWT / JS Interop. An example of JSNI implementation class instance creation, a class variable, and function description is as follows:

public class Cartesian4 extends JavaScriptObject {
    // Mandatory by JSNI
    protected Cartesian4() {}
    // Read constructor for Cartesian4
    public static native Cartesian4 create() /*-{
        return new Cesium.Cartesian4();
    }-*/;
    // Setter for parameter X of class Cartesian4
    public native void setX(double x) /*-{
        this.x = x;
    }-*/;
    // Getter parameter X of class Cartesian4
    public native double getX() /*-{
        return this.x;
    }-*/;
    public static native Cartesian4 fromArray(double[] array) /*-{
        return Cartesian4.fromArray(array);
    }-*/;

    @Override
    public native String toString() /*-{
        return this.toString();
    }-*/;
}

Usage:

Cartesian4 cartesian4 = Cartesian4.create();
cartesian4.setX(1.0);
cartesian4.setX(cartesian4.getX() * 2);

So, first we need to create an instance of the class through the method create () and then start using it.

What does JsInterop offer? Here is an example of the same code above, but this time implemented by JsInterop:

@JsType(isNative = true, namespace = “Cesium“ name = “Cartesian4“)
public class Cartesian4 {
    // X property of Cartesian4, you can read they or write into (like as Java and JavaScript)
    @JsProperty
    public double x;
    // Constuctor of Cartesian4 class (like as Java and JavaScript)
    @JsConstructor
    public Cartesian4() {}
    // Static method, if the method name matches the name of the method in the JS, then nothing more is needed to build upon.
    @JsMethod
    public static native Cartesian4 fromArray(double[] array);
    // Override method (All classes JsType inherited from Object)
    @Override
    @JsMethod
    public native String toString();
}

Usage:

Cartesian4 cartesian4 = new Cartesian4();
cartesian4.x = 1.0;
cartesian4.x *= 2

This code is smaller and, for many, easier to read. As can be seen, the variable x has access to both reading and writing, as in the native JavaScript. Cesium injection based on GWT AbstractLinker, Cesium.js, and other files (css, images, etc.) includes the jar library. You may use the build provided by the Cesium core, which implements Cesium.gwt.xml, or you may use another version of Cesium by using CesiumNoScript.gwt.xml and include Cesium.js directly into the HTML header file. There are no callbacks on injecting JavaScript or on AttachOrDetach. It’s all simple.

The first option is to introduce Cesium.js directly to the Java class:

public class HelloWorld extends AbstractExample {

    @Inject
    public HelloWorld(ShowcaseExampleStore store) {
    super("Hello World", "Use Viewer to start building new applications or easily embed Cesium into existing applications", new String[]{"Showcase", "Cesium", "3d", "Viewer"}, store);
    }

    @Override
    public void buildPanel() {
        // Create Cesium Viewer
        ViewerPanel csVPanel = new ViewerPanel();

        contentPanel.add(new HTML("<p>Use Viewer to start building new applications or easily embed Cesium into existing applications.</p>"));
        contentPanel.add(csVPanel);

        initWidget(contentPanel);
    }

    @Override
    public String[] getSourceCodeURLs() {
        String[] sourceCodeURLs = new String[1];
        sourceCodeURLs[0] = GWT.getModuleBaseURL() + "examples/" + "HelloWorld.txt";
        return sourceCodeURLs;
    }
}

Features

Next, let’s look at what new solutions were applied to the following:

  1. Promise
  2. Events and Callbacks
  3. Dynamic Properties
  4. Inheritance

Promises

Promises are used to organize asynchronous code, useful for when there is a need to download remote data, and there is a need to work with the loaded data.

For the example Clustering, here is the code in Clustering.html:

var options = {
    camera : viewer.scene.camera,
    canvas : viewer.scene.canvas
};
var dataSourcePromise = viewer.dataSources.add(Cesium.KmlDataSource.load('../../SampleData/kml/facilities/facilities.kml', options));
dataSourcePromise.then(function(dataSource) {
    // DO SOMETHING
}

Code in Clustering.java:

KmlDataSourceOptions kmlDataSourceOptions = new KmlDataSourceOptions(_viewer.camera, _viewer.canvas());
Promise<KmlDataSource, Void> dataSourcePromise = _viewer.dataSources().add(KmlDataSource.load(GWT.getModuleBaseURL() + "SampleData/kml/facilities/facilities.kml", kmlDataSourceOptions));
dataSourcePromise.then(new Fulfill<KmlDataSource>() {
    @Override
    public void onFulfilled(KmlDataSource dataSource) {
        // DO SOMETHING
    }
});

As can be seen above, the fragment returns Promise, to which we can apply the method. Everything is just as in the native JavaScript. For more information on the examples with Promise, see Clustering, Billboards, Terrain, or by searching for “Promise” in my examples.

Events and Callbacks

All events take place from the Cesium Event, and it works the same way in the wrapper with one difference: it is necessary to determine in advance what the functions will be called when an event occurs and to set the parameters of these functions to be transferred.

Consider an example Clustering. It is called clusterEvent - when a new cluster will be displayed, and the function of this event will be a substitution of the style of drawing objects. Since all events are Event, the addEventListener method will return removeCallback, and the call function method listener will be removed.

Let us consider the same example, Clustering.

Here is the code in Clustering.html:

function customStyle() {
    if (Cesium.defined(removeListener)) {
        removeListener();
        removeListener = undefined;
    } else {
        removeListener = dataSource.clustering.clusterEvent.addEventListener(function(clusteredEntities, cluster) {
            cluster.label.show = false;
            cluster.billboard.show = true;
            cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;

            if (clusteredEntities.length >= 50) {
                cluster.billboard.image = pin50;
            } else if (clusteredEntities.length >= 40) {
                cluster.billboard.image = pin40;
            } else if (clusteredEntities.length >= 30) {
                cluster.billboard.image = pin30;
            } else if (clusteredEntities.length >= 20) {
                cluster.billboard.image = pin20;
            } else if (clusteredEntities.length >= 10) {
                cluster.billboard.image = pin10;
            } else {
                cluster.billboard.image = singleDigitPins[clusteredEntities.length - 2];
            }
        });
    }

    // force a re-cluster with the new styling
    var pixelRange = dataSource.clustering.pixelRange;
    dataSource.clustering.pixelRange = 0;
    dataSource.clustering.pixelRange = pixelRange;
}

And in Clustering.java:

public void customStyle(KmlDataSource dataSource) {
    if (Cesium.defined(removeListener)) {
        removeListener.function();
        removeListener = (Event.RemoveCallback) JsObject.undefined();
    } else {
        removeListener = dataSource.clustering.clusterEvent.addEventListener(new EntityCluster.newClusterCallback() {
            @Override
            public void function(Entity[] clusteredEntities, EntityClusterObject cluster) {
                cluster.label.show = false;
                cluster.billboard.show = true;
                cluster.billboard.verticalOrigin = VerticalOrigin.BOTTOM();
                if (clusteredEntities.length >= 50) {
                    cluster.billboard.image = pin50;
                }
                else if (clusteredEntities.length >= 40) {
                    cluster.billboard.image = pin40;
                }
                else if (clusteredEntities.length >= 30) {
                    cluster.billboard.image = pin30;
                }
                else if (clusteredEntities.length >= 20) {
                    cluster.billboard.image = pin20;
                }
                else if (clusteredEntities.length >= 10) {
                    cluster.billboard.image = pin10;
                }
                else {
                    cluster.billboard.image = singleDigitPins[clusteredEntities.length - 2];
                }
            }
        });
    }
    // force a re-cluster with the new styling
    int pixelRange = dataSource.clustering.pixelRange;
    dataSource.clustering.pixelRange = 0;
    dataSource.clustering.pixelRange = pixelRange;
}

The code looks almost the same, and the decisions on JsInterop work. Another good example for Events and Callback is Camera.

Dynamic Properties

JavaScript is a very flexible language, and to declare a new variable in the object, simply assign it a value. Here is an example in Shadows.html:

var sphereEntity = viewer.entities.add({
    name : 'Sphere',
    height : 20.0,
    ellipsoid : {
        radii : new Cesium.Cartesian3(15.0, 15.0, 15.0),
        material : Cesium.Color.BLUE.withAlpha(0.5),
        slicePartitions : 24,
        stackPartitions : 36,
        shadows : Cesium.ShadowMode.ENABLED
    }
});

This creates a new property “height” of the options object, and sets value = 20. In Java (JsInterop) this is difficult to implement, or even impossible. At this stage, I do it through a special class - JsObject (Legacy JSNI):

ModelGraphicsOptions modelGraphicsOptions = new ModelGraphicsOptions();
modelGraphicsOptions.uri = new ConstantProperty<>(GWT.getModuleBaseURL() + "SampleData/models/CesiumAir/Cesium_Air.glb");
EntityOptions entityOptions = new EntityOptions();
entityOptions.name = "Cesium Air";
entityOptions.model = new ModelGraphics(modelGraphicsOptions);
// Устанавливаем высоту в 20
((JsObject) (Object) entityOptions).setNumber("height", 20.0);
cesiumAir = _viewer.entities().add(entityOptions);

It is also possible to set the list of parameters and their values, similar to Object Literal:

JsObject.$(entityOptions, "height", 20.0, "height2", 30.0, "other", "String");

Take the value of the height:

JsObject.getNumber(entity, "height").doubleValue();
// Or
((JsObject) (Object)entity).getNumber("height").doubleValue();

In this class (JsObject), another important undefined function came up, which returns JavaScriptObject = undefined. Let me explain: in JavaScript null and undefined are not the same, while JsInterop perceives undefined and null as the same, and there is no way to set the object as undefined. For this the undefined function was introduced:

In JavaScript:

removeListener = undefined;

In Java:

removeListener = (Event.RemoveCallback) JsObject.undefined();

GXT Application uses gtw-cs (gwt-olcs).

Inheritance

In Cesium there is no actual inheritance, and JsInterop does not allow class inheritance if the inheritance is not implemented in JavaScript. With the first implementations of succession, I received an error-type conversion. Now the implementation of inheritance is as follows:

GXT Application uses gtw-cs (gwt-olcs).

GXT Application uses gtw-cs (gwt-olcs).

GXT Application uses gtw-cs (gwt-olcs).

As you can see, the interface is implemented first, and then implemented classes follow. Yes, we lose functionality, and it is necessary to redefine the function, but this solution is working as it should.

Conclusion

This wrapper provides

  1. Fully functional CesiumJS in Java
  2. All Sandcastle examples
  3. Comfortable Enum’s