JavaScript – очень распространённый язык для программирования в WEB среде. Он подходит для очень разнообразных и в тоже время обобщённых задач, но для задач средних и крупных предприятий используются реализации на языке Java (Java EE, Java SB, EJB).

Для командной разработки неудобно и неэффективно делать интегрирование JavaScript кода в проект напрямую, гораздо удобнее реализовать полностью обёртку JavaScript проекта, и только потом её (обёртку) использовать.

Для этих целей на помощь нам приходит GWT со своими решениями по интеграции JavaScript кода - JSNI (JavaScript Native Interface. GWT <= 2.7.0) и JsInterop (GWT >= 2.8.0). GWТ, на мой взгляд, является мощным решением для разработки пользовательского интерфейса на Java, так как является библиотекой низкого уровня. На её основе сделаны такие сложные и функциональные решения, как Smart GWT, GXT, VAADIN и другие. И соответственно, все решения, разработанные на GWT легко интегрируются в решения высокого уровня.

И так, что у нас было… До 20 октября 2016(релиз 2.8.0) года был GWT 2.7.0 и JSNI. Очень хорошее решение, оно работало, и вполне не плохо, но были свои недостатки:

 

  1. Избыточность кода для обёртки
  2. Невозможность лёгкой реализации Callback’s
  3. Невозможность лёгкой реализации функций как параметра функций
  4. Нельзя создать объект через оператор new.
  5. Для доступа к членам класса необходимо было писать getter/setter методы (Избыточность)

При переходе на JsInterop, Java код стал больше походить на нативный JavaScript код (для этого JsInterop и создавался), и соответственно, общество, которое разрабатывает/пишет приложения на JavaScript, легко может помогать Java сообществу, при минимальной переделке кода. Это можно увидеть на примерах, которые идут вместе с моей  библиотекой (Showcase). Некоторые вещи реализовать на JsInterop нельзя, и приходится использовать JSNI со своим JavaScriptObject’ом, но я думаю в обозримом будущем  возможности JsInterop будут расти и, возможно, полностью заменят JSNI.

GWT и JsInterop

В GWT 2.8.0 помимо JSNI появился и JsInterop. JsInterop помогает с лёгкостью реализовывать обёртки для JavaScript, а также сильно сокращает объём кода.

Очень подробно о JsInterop описано в документации – Nextget GWT/JS Interop (Public) https://docs.google.com/document/d/10fmlEYIHcyead_4R1S5wKGs1t2I7Fnp_PaNaa7XTEk0/edit

Пример, на JSNI реализация создания экземпляра класса, переменной класса и описание функции выглядело следующим образом:

public class Cartesian4 extends JavaScriptObject {
// Нужен по требованию JSNI
protected Cartesian4() {}
// Реальный конструктор для Cartesian4
public static native Cartesian4 create() /*-{
     return new Cesium.Cartesian4();
}-*/;
    // Сеттер для переменной класса Cartesian4
public native void setX(double x) /*-{
this.x = x;
}-*/;
    // Геттер для переменной класса 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()
}-*/;
}

Использование:

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

Таким образом, сначала нам нужно создать экземпляр класса через метод create(), а затем начать пользоваться им.

Что же нам предлагает JsInterop. Пример такого же кода который выше, но уже реализованного с помощью JsInterop.

@JsType(isNative = true, namespace = “Cesium“ name = “Cartesian4“)
public class Cartesian4 {
// Переменная x, в неё можно как читать так и писать (как в JS)
@JsProperty
public double x;
// Конструктор класса (Как в Java так и в JS)
@JsConstructor
public Cartesian4() {}
// Статический метод, если название метода совпадает с названием метода в JS, то больше ничего дописывать не нужно.
    @JsMethod
    public static native Cartesian4 fromArray(double[] array);
    // Переопределённый метод (Все классы JsType унаследованы от Object)
    @Override
    @JsMethod
    public native String toString();
}

Использование: 

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

На мой взгляд код стал меньше и удобно читаемым. Как можно увидеть, для переменной x есть доступ как на чтение, так и на запись, как и в нативном JavaScript.

Честно признаться, я взял за основу внедрения Cesium.js в код программы методы Rich Kadel\'а, и упростил/изменил их под новые реалии GWT 2.8.0. А также добавил методы и классы для тех, кто хочет внедрять Cesium.js непосредственно в главном HTML файле через <script></script>. Я назвал этот метод статическим (Смотрите примеры со Static).

Первый вариант, внедрение Cesium.js непосредственно в Java классе:

private class ViewerPanel implements IsWidget {
    private ViewerPanelAbstract _csPanelAbstract;
    private ViewerPanel() {
        super();
        asWidget();
    }
    @Override
    public Widget asWidget() {
        if (_csPanelAbstract == null) {
            final Configuration csConfiguration = new Configuration();
            csConfiguration.setPath(GWT.getModuleBaseURL() + "JavaScript/Cesium");
            _csPanelAbstract = new ViewerPanelAbstract(csConfiguration) {
                @Override
                public Viewer createViewer(Element element) {
                    _viewer = new Viewer(element);
                    return _viewer;
                }
            };
        }
        return _csPanelAbstract;
    }
}

Как видно, инициализация такая же как и у Rich Kadel\'а.

И второй вариант, если мы делаем внедрение Cesium.js в заголовке HTML:

    <script type="text/javascript" language="javascript" src="/Showcase/JavaScript/Cesium/Cesium.js"></script>

    <link rel="stylesheet" type="text/css" href="/Showcase/JavaScript/Cesium/Widgets/widgets.css" />

То пользуемся классами – CesiumWidgetPanel или ViewerPanel

public class ViewerPanel extends SimplePanel {
    private Viewer _viewer;
    public ViewerPanel() {
        super();
        super.addAttachHandler(new AttachEvent.Handler() {
            @Override
            public void onAttachOrDetach(AttachEvent attachEvent) {
                _viewer = new Viewer(getElement());
            }
        });
    }
    public Viewer getViewer() {
        return _viewer;
    }
}

Конкретную реализацию можно посмотреть в примерах(поиск по Static). Вы должны дождаться, когда виджет добавится в HTML документ, так как Element создаётся после этого. Вы можете создать в HTML странице статический <div> и в коде создавать Viewer на этот элемент (по Id).

Далее давайте рассмотрим какие новые решения были применены для следующих вещей:

  1. Promise
  2. Events and Callback
  3. Dynamic properties
  4. Inheritance

Promise

Служит для организации асинхронного кода. Нужен когда идёт работа с удалённой загрузкой данных, и есть необходимость начать работу с данными когда они загружены. Расписывать как оно работает не буду, всё есть в документации по JavaScript, покажу только как работает это у Cesium и в моей обёртке:

Код в 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 SOMETRING
}

Код в 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 SOMETRING
    }
});

Как видно из приведённого фрагмента, возвращается Promise к которому мы можем применить метод then. Всё как и в нативном JavaScript. Подробнее ознакомится с примерами по Promise можно в Clustering, Billboards, Terrain и тд(Или поиском по Promise в Showcase).

Events and Callback

Все события в Сesium происходят от Event. Так же и в этой обёртке. С одной лишь разницей, приходится заранее определять функции которые будут вызываться при наступлении события, и набор параметров этих функций, которые будут передаваться.

Рассмотрим на примере Clustering. Там вызывается событие clusterEvent – когда будет отображаться новый кластер, и в функции на это событие будет происходить подмена стиля отрисовки объектов. Так как все события, это Event, то на метод addEventListener будет возвращаться RemoveCallback, по вызову метода function будет удаляться листенер;

Обратимся всё к тому же примеру – Clustering:

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
    va pixelRange = dataSource.clustering.pixelRange;
    dataSource.clustering.pixelRange = 0;
    dataSource.clustering.pixelRange = pixelRange;
}

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;
}

Код выглядит практически одинаковым, решения на JsInterop работают. Ещё один хороший пример для Event и Callback – Camera.

Dynamic properties

JavaScript очень гибкий язык, и для объявления новых переменных в объекте, достаточно просто присвоить им значение. Пример в 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
    }
});

Новая переменная – height – и ей присвоено 2.0.

В Java(JsInterop) такое реализовать проблематично, либо это не возможно, либо я просто не знаю как. На данном этапе я делаю это через специальный класс – JsObject (Наследие 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);

Также есть возможность установки списка параметров и их значений, аналогично Object Literal:

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

Взять значение можно следующем образом:

 

JsObject.getNumber(entity, "height").doubleValue();
// Или
((JsObject) (Object)entity).getNumber("height").doubleValue();

В данном классе (JsObject) реализована ещё одна важная функция – undefined, которая возвращает JavaScriptObject = undefined. Поясню, в JavaScript null и undefined это не одно и тоже, в то время как JsInterop воспринимает undefined и null как одно и тоже, и нету способа установить объект в undefined. Для этого и была введена функция undefined:

JavaScript:

removeListener = undefined;

Java:

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

Inheritance

В CesiumJS нет как такового наследования, а JsInterop не позволяет наследование классов, если наследование не реализовано в JavaScript’е. В первых реализациях наследования, я получал ошибку преобразования типов. Теперь реализация наследования выглядит следующим образом:

Как видно, сначала реализуется интерфейс, а затем от него реализуются классы. Да, мы теряем функциональность, и приходится переопределять функции, но данное решение работает как нужно.

Заключение

Что планируется реализовать:

  1. Полный функционал CesiumJS на Java
  2. Все примеры Sandcastle
  3. Удобные Enum