2. Отображение маркеров на карте

Добавление атрибута к сущности и UI

Давайте добавим атрибут location к сущности User:

Дважды щелкните на сущности User в окне Jmix и выберите ее последний атрибут (чтобы добавить новый атрибут в конец):

new attr for user

Нажмите Add (add) на панели инструментов Attributes. В диалоговом окне New Attribute введите location в поле Name, выберите ASSOCIATION в выпадающем списке Attribute type и Location в выпадающем списке Type. Выберите кардинальность One to One и установите флажок Owning Side.

location attr

Чтобы установить ссылку один-к-одному, Studio рекомендует сгенерировать обратный атрибут в сущности Location.

create inverse attr

Нажмите Yes, а затем OK в следующем диалоговом окне.

Выберите атрибут location и нажмите кнопку Add to Views (add attribute to screens) на панели инструментов Attributes:

add attr to view

В появившемся диалоговом окне будут представлены все экраны, отображающие сущность User. Выберите экран User.detail:

add attr to detail

После этого Studio добавит свойство location в fetchPlan и разместит компонент entityPicker внутри formLayout экрана User.detail.

Нажмите кнопку Debug (start debugger) на главной панели инструментов.

Перед выполнением приложения Studio создаст файл Liquibase changelog:

changelog user

Нажмите Save and run.

Studio выполнит changelog, затем выполнит сборку и запуск приложения.

Перейдите по адресу http://localhost:8080 в вашем веб-браузере и войдите в приложение с учетными данными admin/admin.

Выберите пункт Users в меню Application.

Нажмите Create. Элемент управления UI для выбора местоположения будет отображен в нижней части формы:

user with location detail

Создание пустого экрана

Если ваше приложение в данный момент запущено, остановите его, нажав кнопку Stop (suspend) на главной панели инструментов.

В окне Jmix выберите New (add) → View:

create blank view

В окне Create Jmix View выберите шаблон Blank view:

create view template

Нажмите Next.

На следующем шаге мастера введите следующее:

  • Descriptor name: location-lookup-view

  • Controller name: LocationLookupView

  • Package name: com.company.onboarding.view.locationlookup

Очистите поле Parent menu item, так как оно не нужно для этого экрана.

create blank view params

Нажмите Next, а затем Create.

Studio сгенерирует пустой экран и отобразит его в дизайнере:

create view designer

Настройка открытия экрана

Наш новый экран предназначен для открытия из экрана деталей пользователя. Для этого будет использоваться поле Location.

Нам нужно заменить сгенерированный Studio компонент entityPicker на компонент valuePicker. Откройте user-detail-view.xml и найдите компонент entityPicker внутри formLayout:

<layout>
    <formLayout id="form" dataContainer="userDc">
        ...
        <entityPicker id="locationField" property="location">
            <actions>
                <action id="entityLookup" type="entity_lookup"/>
                <action id="entityClear" type="entity_clear"/>
            </actions>
        </entityPicker>
        ...
    </formLayout>
</layout>

Измените XML-элемент компонента на valuePicker и удалите вложенный элемент actions.

Выберите valuePicker на панели структуры Jmix UI или в XML-дескрипторе экрана, затем нажмите кнопку Add на панели инспектора. Выберите Actions → Action в выпадающем списке.

value picker actions

Сначала выберите New Base Action и нажмите ОК.

new base action

В качестве идентификатора действия введите select, а для атрибута icon выберите vaadin:search.

select action

Затем добавьте предопределенное действие value_clear для locationField:

add value clear action

Выберите действие select на панели структуры Jmix UI или в XML-дескрипторе экрана. Затем перейдите на вкладку Handlers на панели Jmix Inspector, чтобы сгенерировать метод-обработчик события ActionPerformedEvent:

action performed event

Добавьте логику открытия LocationLookupView в метод-обработчик события ActionPerformedEvent:

@Autowired
private DialogWindows dialogWindows; (1)

@Subscribe("locationField.select")
public void onLocationFieldSelect(final ActionPerformedEvent event) {
    dialogWindows.view(this, LocationLookupView.class).open();
}
1 Бин DialogWindows предоставляет fluent interface для открытия экранов в диалоговых окнах.

Запустите приложение. Выберите пункт Users в меню Application. Нажмите Create. Появится экран User.detail. Найдите поле Location и нажмите на кнопку Search (search button). Это действие приведет к открытию LocationLookupView в диалоговом окне.

blank view as dialog

Теперь у вас будет возможность просмотреть изменения, происходящие в нашем экране.

Добавление компонентов на LocationLookupView

Сначала добавьте поле, в котором будет отображаться текущее местоположение, выбранное на карте. Перейдите на панель действий, нажмите Add Component, найдите entityPicker и дважды щелкните по нему. Настройте свойства компонента следующим образом:

<entityPicker id="currentLocationField"
              metaClass="Location"
              readOnly="true"
              width="20em"
              label="msg://currentLocationField.label"/>

Затем мы добавим два контейнера hbox:

  1. Первый будет содержать список местоположений и карту.

  2. Второй будет содержать кнопки Select и Cancel.

<hbox padding="false"
      height="100%"
      width="100%"/>
<hbox id="controlLayout"/>

Нажмите Add Component на панели действий, а затем перетащите Layouts → VBox (вертикальный контейнер) в первый элемент hbox на панели структуры Jmix UI. Настройте свойства vbox следующим образом:

<vbox padding="false" width="25em"/>

Затем добавьте поле для выбора типа местоположения. Нажмите Add Component на панели действий, найдите select, а затем перетащите его в vbox. Настройте свойства компонента следующим образом:

<select id="locationTypeField"
        emptySelectionAllowed="true"
        width="20em"
        itemsEnum="com.company.onboarding.entity.LocationType"/>

Для отображения списка местоположений мы будем использовать компонент listBox. Сначала добавьте контейнер данных, который будет предоставлять коллекцию сущностей Location для виртуального списка. Для этого нажмите Add Component на панели действий, перейдите в раздел Data components и дважды щелкните элемент Collection. В окне Collection Properties Editor выберите Location в поле Entity и нажмите ОК:

location collection container

Studio сгенерирует контейнер коллекции:

<data>
    <collection id="locationsDc" class="com.company.onboarding.entity.Location">
        <fetchPlan   extends="_base"/>
        <loader id="locationsDl" readOnly="true">
            <query>
                <![CDATA[select e from Location e]]>
            </query>
        </loader>
    </collection>
</data>

Загрузка данных

Чтобы активировать созданный загрузчик, добавьте фасет dataLoadCoordinator.

add data load coordinator

Запрос по умолчанию извлекает все экземпляры Location, но вам нужно фильтровать только местоположения, выбранные в компоненте locationTypeField. В результате мы объявляем условия запроса, связанные с полем ввода через DataLoadCoordinator.

Мы будем использовать префикс component_ в условии запроса для ссылки на компонент locationTypeField.

Давайте настроим условия запроса декларативно в XML-элементе <condition>:

<view xmlns="http://jmix.io/schema/flowui/view"
      title="msg://locationLookupView.title"
      xmlns:c="http://jmix.io/schema/flowui/jpql-condition"> (1)
    <data>
        <collection id="locationsDc" class="com.company.onboarding.entity.Location">
            <fetchPlan extends="_base"/>
            <loader id="locationsDl" readOnly="true">
                <query>
                    <![CDATA[select e from Location e]]>
                    <condition> (2)
                        <c:jpql> (3)
                            <c:where>e.type = :component_locationTypeField</c:where> (4)
                        </c:jpql>
                    </condition>
                </query>
            </loader>
        </collection>
    </data>
1 Добавляет пространство имен условий JPQL.
2 Определяет элемент condition внутри запроса.
3 Определяет условие JPQL с необязательным элементом join и обязательным элементом where.
4 Включает предложение WHERE по атрибуту type с параметром :component_locationTypeField.

Добавление ListBox

Нажмите Add Component на панели действий, найдите listBox, а затем перетащите его в vbox. Настройте свойства компонента следующим образом:

<listBox id="listBox"
         itemsContainer="locationsDc"
         minHeight="20em"
         width="20em"/>

На этом этапе XML экрана должен выглядеть так, как показано ниже:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<view xmlns="http://jmix.io/schema/flowui/view"
      title="msg://locationLookupView.title"
      xmlns:c="http://jmix.io/schema/flowui/jpql-condition">
    <data>
        <collection id="locationsDc" class="com.company.onboarding.entity.Location">
            <fetchPlan extends="_base"/>
            <loader id="locationsDl" readOnly="true">
                <query>
                    <![CDATA[select e from Location e]]>
                    <condition>
                        <c:jpql>
                            <c:where>e.type = :component_locationTypeField</c:where>
                        </c:jpql>
                    </condition>
                </query>
            </loader>
        </collection>
    </data>
    <layout>
        <entityPicker id="currentLocationField"
                      metaClass="Location"
                      readOnly="true"
                      width="20em"
                      label="msg://currentLocationField.label"/>
        <hbox padding="false"
              height="100%"
              width="100%">
            <vbox padding="false"
                  width="25em">
                <select id="locationTypeField"
                        emptySelectionAllowed="true"
                        width="20em"
                        itemsEnum="com.company.onboarding.entity.LocationType">
                </select>
                <listBox id="listBox"
                         itemsContainer="locationsDc"
                         minHeight="20em"
                         width="20em"/>
            </vbox>
        </hbox>
        <hbox id="controlLayout"/>
    </layout>
</view>

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

location lookup view

Добавление Map

Поместите курсор после элемента vbox. Нажмите Add Component на панели действий, найдите элемент GeoMap и дважды щелкните по нему.

Новый элемент map будет создан под элементом vbox на панели структуры Jmix UI и в XML. Настройте атрибуты id, height и width, как показано ниже.

<maps:geoMap id="map"
             height="100%"
             width="100%"/>

Затем добавьте тайловый слой с OsmSource, установите отображение карты и добавьте векторный слой с DataVectorSource. Готовая карта должна выглядеть так, как показано ниже:

<maps:geoMap id="map"
             height="100%"
             width="100%">
    <maps:mapView centerX="0" centerY="51">
        <maps:extent minX="-15" minY="30" maxX="40" maxY="60"/>
    </maps:mapView>
    <maps:layers>
        <maps:tile>
            <maps:osmSource/>
        </maps:tile>
        <maps:vector id="dataVectorLayer">
            <maps:dataVectorSource id="buildingSource"
                                   dataContainer="locationsDc"
                                   property="building"/>
        </maps:vector>
    </maps:layers>
</maps:geoMap>

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

location lookup view with map

Как мы видим, карта не помещается на экране. Поэтому нам нужно изменить размер экрана. Добавьте аннотацию @DialogMode в контроллер LocationLookupView:

@Route(value = "LocationLookupView", layout = MainView.class)
@ViewController("LocationLookupView")
@ViewDescriptor("location-lookup-view.xml")
@DialogMode(width = "60em", height = "45em")
public class LocationLookupView extends StandardView {
}

Нажмите Ctrl/Cmd+S и перейдите в запущенное приложение. Нажмите на кнопку Search (search button) рядом с полем Location. Экран LocationLookupView откроется в диалоговом окне с заданными нами шириной и высотой.

location lookup with map

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

Добавление кнопок

Давайте добавим кнопку Select для сохранения текущего местоположения пользователя и кнопку Cancel для выхода без сохранения.

Откройте XML-дескриптор location-lookup-view.xml и найдите controlLayout hbox. Нажмите Add Component на панели действий, затем перетащите две кнопки в controlLayout.

Созданные кнопки должны быть связаны с действиями. Определите элемент actions, включая вложенные элементы action, как показано ниже.

<actions>
    <action id="select"
            text="msg://selectAction.text"
            icon="CHECK"
            actionVariant="PRIMARY"
            enabled="false"/> (1)
    <action id="cancel"
            type="view_close"/> (2)
</actions>
1 Пользовательские свойства действия select определены непосредственно в месте использования.
2 Стандартное действие закрытия экрана.

Сгенерируйте событие выполнения действия с помощью панели инспектора Jmix UI → вкладка Handlers.

action select event

Реализуйте обработчик действия select:

@Subscribe("select")
public void onSelect(final ActionPerformedEvent event) {
    close(StandardOutcome.SELECT); (1)
}
1 Метод close() закрывает экран. Он принимает объект StandardOutcome.SELECT, который может быть обработан вызывающим кодом. Мы обработаем его позже.

Назначьте идентификаторы кнопок и свяжите каждую кнопку с соответствующим действием, как показано ниже:

<hbox id="controlLayout">
    <button id="selectBtn" action="select"/>
    <button id="cancelBtn" action="cancel"/>
</hbox>

Использование пользовательских маркеров

Перейдите в окно Project и найдите различные маркеры для офисов и коворкингов в директории /src/main/resources/META-INF/resources/icons/ в classpath:

locate markers

Откройте контроллер LocationLookupView и инжектируйте buildingSource.

@ViewComponent("map.dataVectorLayer.buildingSource")
private DataVectorSource<Location> buildingSource;

Вы можете инжектировать компоненты экрана и Spring-бины с помощью кнопки Inject на панели действий:

inject map

Затем добавьте метод для настройки отображения маркеров:

private void initBuildingSource(){
    buildingSource.setStyleProvider(location -> new Style() (1)
            .withImage(new IconStyle()
                    .withScale(0.5)
                    .withAnchorOrigin(IconOrigin.BOTTOM_LEFT)
                    .withAnchor(new Anchor(0.49, 0.12))
                    .withSrc(location.getType() == LocationType.OFFICE
                            ? "icons/office-marker.png"
                            : "icons/coworking-marker.png"))
            .withText(new TextStyle()
                    .withBackgroundFill(new Fill("rgba(255, 255, 255, 0.6)"))
                    .withPadding(new Padding(5, 5, 5, 5))
                    .withOffsetY(15)
                    .withFont("bold 15px sans-serif")
                    .withText(location.getCity())));
}
1 Устанавливает новый стиль, который объединяет изображение с текстовой меткой для наших маркеров. Изображение варьируется в зависимости от типа местоположения.

Нажмите кнопку Generate Handler на верхней панели действий и выберите Controller handlers → InitEvent:

init event generate

Нажмите ОК. Studio сгенерирует заглушку метода-обработчика. Вызовите initBuildingSource() из обработчика InitEvent:

@Subscribe
public void onInit(final InitEvent event) {
    initBuildingSource();
}

Запустите приложение и откройте LocationLookupView. Оцените внешний вид маркеров для различных типов местоположений.

different markers

Обработка событий маркеров

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

Откройте контроллер LocationLookupView и добавьте метод setMapCenter():

private void setMapCenter(Geometry center) {
    map.fit(new FitOptions(center)
            .withDuration(2000)
            .withEasing(Easing.LINEAR)
            .withMaxZoom(20d));
}

Затем перейдите к методу initBuildingSource() и добавьте следующий код в конец тела метода:

private void initBuildingSource(){
    //...
    buildingSource.addGeoObjectClickListener(clickEvent -> {
        Location location = clickEvent.getItem();

        setMapCenter(location.getBuilding());
    });
}

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

centered map

Теперь нам нужно отобразить выбранное местоположение в поле Current location и сделать кнопку Select доступной.

Вернитесь к контроллеру LocationLookupView. Инжектируйте currentLocationField и действие select. Определите переменную selected:

@ViewComponent
private EntityPicker<Location> currentLocationField;

@ViewComponent
private BaseAction select;

private Location selected;

Затем добавьте метод onLocationChanged():

private void onLocationChanged(Location newLocation) {
    if (newLocation != null)
        if (!Objects.equals(newLocation, selected)) {
            selected = newLocation;
            select.setEnabled(true); (1)

            setMapCenter(newLocation.getBuilding());

            currentLocationField.setValue(newLocation); (2)
        }
}
1 Делает действие Select доступным.
2 Устанавливает выбранное местоположение в поле Current location.

Вызовите onLocationChanged() из метода initBuildingSource():

private void initBuildingSource(){
    //...
    buildingSource.addGeoObjectClickListener(clickEvent -> {
        //...
        onLocationChanged(location);
    });
}