Работа с геопространственными данными и отображение карт являются неотъемлемыми составляющими множества бизнес-приложений. Это могут быть городские и региональные информационные системы, приложения для нефтегазовой отрасли, системы управления транспортной инфраструктурой, а также службы доставки и многие другие. У нас в CUBA Platform для построения подобных приложений помимо базовых возможностей, предоставляемых из коробки, существует довольно обширный набор дополнений и компонентов. Одним из них является Charts and Maps, которое помимо отображения графиков позволяет интегрировать в визуальную часть приложения Google-карты. В прошлом году Google обновил условия использования своих картографических сервисов, что повлекло за собой рост стоимости, а также ввел условие обязательного наличия платежного профиля для использования API. Эти обстоятельства заставили большинство наших клиентов задуматься об альтернативных поставщиках карт, а нас подтолкнули к разработке нового компонента карт.
Теперь мы рады представить совершенно новый компонент — CUBA Maps. CUBA Maps дополняет функционал приложения визуальным представлением и интуитивными инструментами редактирования геопространственных данных. Компонент работает как с растровыми данными, так и с векторными. В качестве растровых данных вы можете использовать любой провайдер карт, совместимый с протоколом Web Map Service, или предоставляющий тайлы в формате XYZ. Для работы с векторными данными компонент использует геометрические типы данных (точка, полилиния, полигон) из библиотеки JTS Topology Suite (JTS) — самой популярной Java библиотеки для работы с геопространственными данными. Компонент предоставляет все необходимые инструменты для создания комплексной геоинформационной системы на базе CUBA.
В этой статье мы расскажем о новых возможностях, предлагаемых компонентом Maps, а также сравним его с нашим предыдущим компонентом карт.
Структура, основанная на слоях
Компонент поддерживает традиционную многослойную структуру, широко используемую в профессиональных геоинформационных системах. Слои в основном подразделяются на растровые и векторные. Растровые слои состоят из растровых изображений, в то время как векторные слои содержат векторные геометрии.
Компонент поддерживает следующие типы слоев:
- Tile layer (слой тайлов) отображает тайлы, предоставляемые тайловыми сервисами в формате XYZ.
- Слой Web Map Service (WMS) отображает изображения, предоставляемые WMS-сервисами.
- Векторный слой содержит гео-объекты (сущности с геометрическими атрибутами).
Эти слои представляют собой структурные элементы карт. Например, нижний слой может быть базовой картой, состоящей из тайлов, второй слой может содержать полигоны, описывающие территориальные единицы, например, районы, а верхний слой может содержать географические точки (местонахождение клиентов, магазинов и т.д.). Накладывая эти слои друг на друга, мы получаем итоговую карту:
Благодаря этому подходу можно создавать четко структурированные карты с любым содержанием.
CUBA Maps предоставляет новый визуальный компонент — GeoMap
. В XML дескрипторе компонента можно задать основные параметры карты, а также набор отображаемых слоев. Пример такой конфигурации:
<maps:geoMap id="map" height="600px" width="100%" center="37.615, 55.752" zoom="10">
<maps:layers selectedLayer="addressLayer">
<maps:tile id="tiles" tileProvider="maps_OpenStreetMap"/>
<maps:vector id="territoryLayer" dataContainer="territoryDc"/>
<maps:vector id="addressLayer" dataContainer="addressDc" editable="true"/>
</maps:layers>
</maps:geoMap>
Подобный подход позволяет добиться большей гибкости, которой не хватало в Charts and Maps:
- Многослойность. Такая структура позволяет выстраивать карты с любым содержимым, например, комбинировать тайлы, предоставленные различными сервисами.
- Слои обеспечивают абстракцию, которая объединяет однородные объекты. В компоненте Charts and Maps все содержимое карты (например, точки, полигоны, и т.д.) было свалено в общую кучу в UI компоненте. Чтобы как-то структурировать эти объекты, проектным командам приходилось писать дополнительную логику.
- Декларативный метод описания слоев. Как было показано в примере выше, вы можете полностью задать структуру карты (набор слоев) в XML дескрипторе. Во многих случаях этого достаточно, чтобы не реализовывать никакой дополнительной логики в контроллере экрана. Используя Charts and Maps было практически невозможно обойтись без написания дополнительной логики.
Использование слоев тайлов или WMS позволяет работать с любым предпочитаемым провайдером карт. Вы не привязаны к определенному провайдеру, как это было в Charts and Maps.
Векторные слои значительно упрощают отображение, интерактивное редактирование и рисование гео-объектов на карте.
Также стоит отметить, что визуальный компонент GeoMap
по умолчанию имеет специальный вспомогательный слой — Canvas. Canvas предоставляет удобный API для отображения и рисования геометрий (точек, полилиний, полигонов) на карте. Мы рассмотрим примеры использования Canvas далее в статье.
Гео-объекты
Предположим, у нас есть сущность, содержащая атрибут, связанный с геометрией (точкой, полилинией, полигоном). Эту сущность мы назовем гео-объектом. Так вот, компонент значительно упрощает работу с гео-объектами.
Например, рассмотрим гео-объект Адрес:
@Entity
public class Address extends StandardEntity {
...
@Column(name = "LOCATION")
@Geometry
@MetaProperty(datatype = "GeoPoint")
@Convert(converter = CubaPointWKTConverter.class)
protected Point location;
...
}
У него есть атрибут location
типа org.locationtech.jts.geom.Point
из библиотеки JTS Topology Suite (JTS). Компонент поддерживает следующие геометрические типы из JTS:
org.locationtech.jts.geom.Point
— точка.org.locationtech.jts.geom.LineString
— полилиния.org.locationtech.jts.geom.Polygon
— полигон.
Атрибут location
помечен аннотацией @Geometry
. Эта аннотация объявляет о том, что значение данного атрибута должно использоваться при отображении гео-объекта на карте. Атрибут также помечен следующими аннотациями:
@MetaProperty
— в данном случае используется для указания datatype атрибута. ИнтерфейсDatatype
используется фреймворком CUBA для конвертации значений в строку и из строки.@Convert
— определяет JPA конвертер для персистентного атрибута. JPA-конвертер осуществляет конвертацию значений атрибута между его представлениями в базе данных и Java-коде. Компонент предоставляет набор пространственных datatype-ов и JPA-конвертеров. Более подробная информация доступна в документации компонента. Также можно использовать свою реализацию JPA-конвертера, что дает возможность работать с различными источниками пространственных данных (например, PostGIS).
Таким образом, чтобы превратить сущность в гео-объект, нужно определить атрибут, имеющий тип JTS-геометрии, и аннотировать его @Geometry
. Есть и еще один вариант — создать неперсистентный атрибут, предоставив getter/setter методы. Это может быть полезно в том случае, если вы не хотите вносить изменения в модель данных и перегенерировать DDL скрипты.
Например, рассмотрим сущность Адрес с отдельными атрибутами для широты и долготы:
import com.haulmont.addon.maps.gis.utils.GeometryUtils;
...
@Entity
public class Address extends StandardEntity {
...
@Column(name = "LATITUDE")
protected Double latitude;
@Column(name = "LONGITUDE")
protected Double longitude;
...
@Geometry
@MetaProperty(datatype = "GeoPoint", related = {"latitude", "longitude"})
public Point getLocation() {
if (getLatitude() == null || getLongitude() == null) {
return null;
}
return GeometryUtils.createPoint(getLongitude(), getLatitude());
}
@Geometry
@MetaProperty(datatype = "GeoPoint")
public void setLocation(Point point) {
Point prevValue = getLocation();
if (point == null) {
setLatitude(null);
setLongitude(null);
} else {
setLatitude(point.getY());
setLongitude(point.getX());
}
propertyChanged("location", prevValue, point);
}
...
}
Если вы решили использовать такой подход, убедитесь, что в сеттере вызывается метод propertyChanged
, поскольку компонент реагирует на это событие обновлением геометрии на карте.
Теперь, когда мы подготовили класс нашего гео-объекта, мы можем добавлять экземпляры этого класса на векторный слой. Векторный слой по сути является связующим элементом между данными (гео-объектами) и картой. Чтобы связать гео-объекты cо слоем, нужно передать data container или, в случае работы с устаревшими экранами (до 7 версии CUBA), datasource в векторный слой. Это можно сделать в XML дескрипторе:
<maps:geoMap id="map">
<maps:layers>
...
<maps:vector id="addressesLayer" dataContainer="addressesDc"/>
</maps:layers>
</maps:geoMap>
В результате экземпляры класса Address
, содержащиеся в контейнере addressesDc
, будут отображены на карте.
Рассмотрим элементарную задачу: создание экрана редактирования гео-объекта с картой, где можно редактировать геометрию объекта. Для решения задачи нужно объявить визуальный компонент GeoMap
в XML дескрипторе экрана редактирования и добавить векторный слой, связанный с контейнером, содержащим редактируемый гео-объект:
<maps:geoMap id="map" height="600px" width="100%" center="37.615, 55.752" zoom="10">
<maps:layers selectedLayer="addressLayer">
<maps:tile ..."/>
<maps:vector id="addressLayer" dataContainer="addressDc" editable="true"/>
</maps:layers>
</maps:geoMap>
Если пометить векторный слой как редактируемый, активизируется интерактивное редактирование гео-объекта на карте. В случае, если геометрия объекта имеет пустое значение, карта автоматически перейдет в режим рисования. Как видите, для решения задачи достаточно объявить векторный слой на карте и передать ему data container/datasource.
Вот и всё. Если бы мы использовали Charts and Maps для решения этой же задачи, нам пришлось бы написать довольно много кода в контроллере экрана для обеспечения подобной функциональности. С новым компонентом Maps решать такие задачи значительно проще.
Canvas
Бывают случаи, когда вам нужно работать не с сущностями. Вместо этого вы хотите простой API для добавления и рисования геометрий на карте, как это было в Charts and Maps. Для этого у визуального компонента GeoMap
есть специальный слой — Canvas. Это вспомогательный слой, который есть на карте по умолчанию и который предоставляет простой API для добавления и рисования геометрий на карте. Получить Canvas карты можно вызвав метод map.getCanvas()
.
Далее мы рассмотрим несколько простых задач, как они решались в Charts and Maps и как можно сделать то же самое, используя Canvas.
Отображение геометрий на карте
В Charts and Maps объекты геометрий создавались с помощью визуального компонента карты, используемого в качестве фабрики, а затем уже добавлялись на карту:
Marker marker = map.createMarker();
GeoPoint position = map.createGeoPoint(lat, lon);
marker.setPosition(position);
map.addMarker(marker);
Новый компонент Maps работает непосредственно с классами из библиотеки JTS:
CanvasLayer canvasLayer = map.getCanvas();
Point point = address.getLocation();
canvasLayer.addPoint(point);
Редактирование геометрий
В Charts and Maps можно было обозначить геометрию как редактируемую. Когда такие геометрии изменялись через UI, вызывались соответствующие события:
Marker marker = map.createMarker();
GeoPoint position = map.createGeoPoint(lat, lon);
marker.setPosition(position);
marker.setDraggable(true);
map.addMarker(marker);
map.addMarkerDragListener(event -> {
// do something
});
В компоненте Maps при добавлении JTS-геометрии на Canvas соответствующий метод возвращает специальный объект, который является представлением этой геометрии на карте: CanvasLayer.Point
, CanvasLayer.Polyline
или CanvasLayer.Polygon
. Этот объект имеет fluent интерфейс для задания различных параметров геометрии, также он может быть использован для подписки на события, связанные с геометрией, либо для удаления геометрии с Canvas.
CanvasLayer canvasLayer = map.getCanvas();
CanvasLayer.Point location = canvasLayer.addPoint(address.getLocation());
location.setEditable(true)
.setPopupContent(address.getName())
.addModifiedListener(modifiedEvent ->
address.setLocation(modifiedEvent.getGeometry()));
Рисование геометрий
В старом аддоне Charts and Maps присутствовал вспомогательный компонент — DrawingOptions
. Он использовался для активации возможности рисования на карте. После того как геометрия была нарисована, вызывалось соответствующее событие:
DrawingOptions options = new DrawingOptions();
PolygonOptions polygonOptions = new PolygonOptions(true, true, "#993366", 0.6);
ControlOptions controlOptions = new ControlOptions(
Position.TOP_CENTER, Arrays.asList(OverlayType.POLYGON));
options.setEnableDrawingControl(true);
options.setPolygonOptions(polygonOptions);
options.setDrawingControlOptions(controlOptions);
options.setInitialDrawingMode(OverlayType.POLYGON);
map.setDrawingOptions(options);
map.addPolygonCompleteListener(event -> {
//do something
});
Компонент Maps позволяет сделать то же самое гораздо проще. В новом Maps Canvas содержит набор методов для рисования геометрий. Например, чтобы нарисовать полигон, используйте метод canvas.drawPolygon()
. После вызова этого метода карта перейдет в режим рисования полигона. Метод принимает функцию Consumer<CanvasLayer.Polygon>
, в которой можно осуществить дополнительные действия с нарисованным полигоном.
canvasLayer.drawPolygon(polygon -> {
territory.setPolygon(polygon.getGeometry());
});
Инструменты для гео-анализа
Кластеризация
Еще один полезный инструмент, присутствующий в новом компоненте Maps, — кластеризация точек. Если слой состоит из большого количества точек, можно включить кластеризацию для группировки близлежащих точек в кластеры, чтобы карта смотрелась аккуратнее и лучше воспринималась:
Кластеризация включается добавлением тега cluster
внутри тега vector
в XML дескрипторе:
<maps:vector id="locations" dataContainer="locationsDc" >
<maps:cluster/>
</maps:vector>
Также можно включить кластеризацию, основанную на “весах” точек. В качестве веса точки выступает значение атрибута, указанного в параметре weightProperty
.
<maps:vector id="orders" dataContainer="ordersDc" >
<maps:cluster weightProperty="amount"/>
</maps:vector>
Тепловые карты
Тепловые карты — это визуальное представление плотности данных среди множества географических точек. Визуальный компонент GeoMap содержит метод для добавления тепловой карты: addHeatMap(Map<Point, Double> intensityMap)
.
Заключение
Обработка, анализ и визуальное представление геопространственных данных является необходимым элементом множества бизнес-приложений. Компонент CUBA Maps обеспечит ваше приложение на CUBA всеми необходимыми инструментами для реализации этого функционала.
Структура, основанная на слоях, помогает в построении карт с любым содержимым. Со слоями тайлов/WMS вы можете использовать любой нужный вам провайдер в качестве базовой карты. Векторные слои позволяют эффективно работать с наборами однородных гео-объектов. Canvas предоставляет простой API для отображения и рисования геометрий на карте.
Компонент работает с пространственными типами из библиотеки JTS, что делает его совместимым со многими другими фреймворками (например, GeoTools) для решения широкого круга задач, связанных с обработкой и анализом географических данных.
Надеемся, что вам понравится компонент. Ждем ваших отзывов!
ivanokunev
Реализация кластеров все еще далека от того, как это необходимо бизнесу. Проблема — группировка вне границ населенных пунктов, т.е. при кластеризации пунктов они находятся в «поле»
glebshalyganov Автор
Да, действительно, сейчас реализован простой алгоритм кластеризации по радиусу.
На первый релиз у нас не было планов реализовывать продвинутые алгоритмы кластеризации.
Задачу кластеризации по ограничивающему контуру можно решить кастомно.