maps


Работа с геопространственными данными и отображение карт являются неотъемлемыми составляющими множества бизнес-приложений. Это могут быть городские и региональные информационные системы, приложения для нефтегазовой отрасли, системы управления транспортной инфраструктурой, а также службы доставки и многие другие. У нас в 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-сервисами.
  • Векторный слой содержит гео-объекты (сущности с геометрическими атрибутами).

Эти слои представляют собой структурные элементы карт. Например, нижний слой может быть базовой картой, состоящей из тайлов, второй слой может содержать полигоны, описывающие территориальные единицы, например, районы, а верхний слой может содержать географические точки (местонахождение клиентов, магазинов и т.д.). Накладывая эти слои друг на друга, мы получаем итоговую карту:


layers


Благодаря этому подходу можно создавать четко структурированные карты с любым содержанием.


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


Кластеризация включается добавлением тега 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).


heatmap


Заключение


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


Структура, основанная на слоях, помогает в построении карт с любым содержимым. Со слоями тайлов/WMS вы можете использовать любой нужный вам провайдер в качестве базовой карты. Векторные слои позволяют эффективно работать с наборами однородных гео-объектов. Canvas предоставляет простой API для отображения и рисования геометрий на карте.


Компонент работает с пространственными типами из библиотеки JTS, что делает его совместимым со многими другими фреймворками (например, GeoTools) для решения широкого круга задач, связанных с обработкой и анализом географических данных.


Надеемся, что вам понравится компонент. Ждем ваших отзывов!

Комментарии (2)


  1. ivanokunev
    23.08.2019 14:30

    Реализация кластеров все еще далека от того, как это необходимо бизнесу. Проблема — группировка вне границ населенных пунктов, т.е. при кластеризации пунктов они находятся в «поле»


    1. glebshalyganov Автор
      23.08.2019 15:08

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