JavaScript – очень распространённый язык для программирования в WEB среде. Он подходит для очень разнообразных и в тоже время обобщённых задач, но для задач средних и крупных предприятий используются реализации на языке Java (Java EE, Java SE).
Для командной разработки неудобно и неэффективно делать интегрирование 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. Очень хорошее решение, оно работало, и вполне не плохо, но были свои недостатки:
- Избыточность кода для обёртки
- Невозможность лёгкой реализации Callback’s
- Невозможность лёгкой реализации функций как параметра функций
- Нельзя создать объект через оператор new.
- Для доступа к членам класса необходимо было писать 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).
Далее давайте рассмотрим какие новые решения были применены для следующих вещей:
- Promise
- Events and Callback
- Dynamic properties
- 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’е. В первых реализациях наследования, я получал ошибку преобразования типов. Теперь реализация наследования выглядит следующим образом:
Как видно, сначала реализуется интерфейс, а затем от него реализуются классы. Да, мы теряем функциональность, и приходится переопределять функции, но данное решение работает как нужно.
Заключение
Что планируется реализовать:
- Полный функционал CesiumJS на Java
- Все примеры Sandcastle
- Удобные Enum