2. Display Markers on Map

Add Attribute to Entity and UI

Let’s add the location attribute to the User entity:

Double-click on the User entity in Jmix tool window and choose its last attribute (to append the new attribute at the end):

new attr for user

Click Add (add) in the Attributes toolbar. In the New Attribute dialog, input location in the Name field, choose ASSOCIATION in the Attribute type dropdown, and Location in the Type dropdown. Select One to One cardinality and mark the Owning Side checkbox.

location attr

To establish a one-to-one reference, Studio recommends generating an inverse attribute in the Location entity.

create inverse attr

Click Yes, and then OK in the next dialog.

Choose the location attribute and click the Add to Views (add attribute to screens) button in the Attributes toolbar:

add attr to view

The displayed dialog window will present all views that exhibit the User entity. Select the User.detail view:

add attr to detail

Subsequently, Studio will append the location property to fetchPlan and place the entityPicker component within formLayout of the User.detail view.

Click the Debug button (start debugger) in the main toolbar.

Prior to application execution, Studio will draft a Liquibase changelog:

changelog user

Click Save and run.

Studio will execute the changelog, proceed with building and running the application.

Access http://localhost:8080 using your web browser and log into the application using the credentials admin/admin.

Choose the Users item in the Application menu.

Click Create. The UI control for selecting a location will be displayed at the bottom of the form:

user with location detail

Create Blank View

If your application is currently running, terminate it by clicking the Stop button (suspend) in the main toolbar.

In the Jmix tool window, select New (add) → View:

create blank view

In the Create Jmix View window, choose the Blank view template:

create view template

Click Next.

On the subsequent step of the wizard, input the following:

  • Descriptor name: location-lookup-view

  • Controller name: LocationLookupView

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

Clear Parent menu item, as it is unnecessary for this view.

create blank view params

Click Next and then Create.

Studio will generate an empty view and display it in the designer:

create view designer

Set Up View Opening

Our new view is intended to open from the user’s detail view. To achieve this, the Location field will be utilized.

We will need to replace the Studio-generated entityPicker component with the valuePicker component. Open user-detail-view.xml and locate the entityPicker component within the 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>

Modify the XML element of the component to valuePicker and eliminate the nested actions element.

Choose valuePicker within the Jmix UI hierarchy panel or in the view XML descriptor, then click the Add button in the inspector panel. Opt for Actions → Action from the drop-down list.

value picker actions

First, select a New Base Action and click OK.

new base action

Set the id of the action to select and icon to vaadin:search.

select action

Next, add a predefined value_clear action for locationField:

add value clear action

Choose the select action in the Jmix UI hierarchy panel or in the view XML descriptor. Then, navigate to the Handlers tab in the Jmix Inspector panel to generate an ActionPerformedEvent handler method:

action performed event

Add the logic of opening LocationLookupView to ActionPerformedEvent handler method:

@Autowired
private DialogWindows dialogWindows; (1)

@Subscribe("locationField.select")
public void onLocationFieldSelect(final ActionPerformedEvent event) {
    dialogWindows.view(this, LocationLookupView.class).open();
}
1 The DialogWindows bean provides a fluent interface for opening views in dialog windows.

Launch the application. Select the Users item from the Application menu. Click Create. The User.detail view will appear. Locate the Location field and click on the (search button) Search button. This action will prompt the LocationLookupView to open in a dialog.

blank view as dialog

Now you’ll have the opportunity to review the alterations occurring in our view.

Add Components on LocationLookupView

First, include a field where the current location selected on the map will be shown. Navigate to the actions panel, click Add Component, locate entityPicker, and double-click it. Configure the component’s properties as follows:

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

Next, we’ll add two hbox containers:

  1. The first will encompass a list of locations and a map.

  2. The second will contain the Select and Cancel buttons.

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

Click Add Component in the actions panel and then drag and drop Layouts → VBox (vertical box) to the first hbox element within the Jmix UI hierarchy panel. Configure the properties of the vbox as follows:

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

Subsequently, incorporate a field for selecting the type of location. Click Add Component in the actions panel, locate select, and then drag and drop it into the vbox. Configure the component’s properties as follows:

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

To showcase the list of locations, we’ll utilize the listBox component. Firstly, introduce a data container which will supply a collection of Location entities for the virtual list. To achieve this, click Add Component in the actions panel, navigate to the Data components section, and proceed to double-click the Collection item. Within the Collection Properties Editor window, select Location in the Entity field and then click OK:

location collection container

Studio will generate the collection container:

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

Load Data

To trigger the created loader, include the dataLoadCoordinator facet.

add data load coordinator

The default query retrieves all Location instances, but you’ll need to filter only the locations selected in the locationTypeField component. As a result, we declare query conditions associated with an input field via DataLoadCoordinator.

We will utilize the component_ prefix in a query condition to reference the locationTypeField component.

Let’s configure query conditions declaratively in the <condition> XML element:

<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 Adds the JPQL conditions namespace.
2 Defines the condition element within the query.
3 Defines a JPQL condition with an optional join element and a mandatory where element.
4 Includes a WHERE clause by type attribute with the :component_locationTypeField parameter.

Add ListBox

Click Add Component in the actions panel, locate listBox, and then drag and drop it into the vbox. Configure the component’s properties as follows:

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

At this point, the view XML should appear as shown below:

<?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>

Let’s run the application to observe the new feature in action.

location lookup view

Add Map

Position the cursor after the vbox element. Click Add Component in the actions panel, find the GeoMap item, and then double-click it.

The new map element will be generated beneath the vbox element in the Jmix UI hierarchy panel and in XML. Configure the id, height and width attributes as shown below.

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

Next, include a tile layer with OsmSource, set a map view, and add a vector layer with DataVectorSource. The completed map should appear as shown below:

<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 dataContainer="locationsDc"
                                   property="building"/>
        </maps:vector>
    </maps:layers>
</maps:geoMap>

Let’s launch the application to see the new feature in action.

location lookup view with map

As we can see, the map does not fit on the view. Therefore, we’ll need to modify the view size. Add the @DialogMode annotation to the LocationLookupView controller:

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

Press Ctrl/Cmd+S and switch to the running application. Click on the (search button) Search button located next to the Location field. The LocationLookupView view will open as a dialog with the width and height we determined earlier.

location lookup with map

The following step will demonstrate how to utilize distinct markers for offices and coworking spaces.

Add Buttons

Let’s add the Select button to save the current location for the user, and the Cancel button to terminate without saving.

Open the location-lookup-view.xml XML descriptor and find the controlLayout hbox. Click Add Component in the actions panel, then drag and drop two buttons into controlLayout.

The created buttons should be associated with actions. Define the actions element, including nested action elements, as illustrated below.

<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 custom action properties are defined in-place.
2 The standard view close action.

Generate the action performed event using the Jmix UI inspector panel → Handlers tab.

action select event

Implement the select action handler:

@Subscribe("select")
public void onSelect(final ActionPerformedEvent event) {
    close(StandardOutcome.SELECT); (1)
}
1 The close() method closes the view. It accepts a StandardOutcome.SELECT object that can be processed by the calling code. We will handle it later.

Assign the button ids and link each button with the respective action as shown below:

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

Use Custom Markers

Switch to the Project tool window and locate various markers for offices and coworking spaces within the /src/main/resources/META-INF/resources/icons/ directory in the classpath:

locate markers

Open the LocationLookupView controller and inject the GeoMap component.

@ViewComponent
private GeoMap map;

You can inject view components and Spring beans using the Inject button in the actions panel:

inject map

Next, introduce a method to customize the display of markers:

private void initBuildingSource(){
    VectorLayer layer = map.getLayer("dataVectorLayer");

    DataVectorSource<Location> source = layer.getSource(); (1)
    source.setStyleProvider(location -> new Style() (2)
            .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 Gets DataVectorSource associated with locationsDc.
2 Establishes a new style that combines an image with a text label for our markers. The image varies based on the location type.

Click Generate Handler button in the top actions panel and select Controller handlers → InitEvent:

init event generate

Click OK. Studio will generate a handler method stub. Invoke initBuildingSource() from the InitEvent handler:

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

Launch the application and open LocationLookupView. Evaluate the appearance of markers for different location types.

different markers

Handle Marker Events

Once a user chooses a marker on the map, the selected location is assigned to the Current location field. Additionally, the map’s zoom level is adjusted, centering the map on the selected location, with the chosen marker placed in the center of the map view.

Open the LocationLookupView controller and add the setMapCenter() method:

private void setMapCenter(Coordinate center) {
    map.getMapView().setCenter(center);
    map.getMapView().setZoom(20);
}

Next, navigate to the initBuildingSource() method and incorporate the following code at the end of the method body:

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

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

Let’s run the application to observe the new feature in action. Now, when you click on the marker, the map will zoom in and center itself based on the location’s coordinates.

centered map

Now we need to display the selected location in the Current location field and make the Select button available.

Return to the LocationLookupView controller. Inject currentLocationField and the select action. Define the selected variable:

@ViewComponent
private EntityPicker<Location> currentLocationField;

@ViewComponent
private BaseAction select;

private Location selected;

Then add the onLocationChanged() method:

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

            setMapCenter(newLocation.getBuilding().getCoordinate());

            currentLocationField.setValue(newLocation); (2)
        }
}
1 Makes the Select action available.
2 Sets the selected location in the Current location field.

Invoke onLocationChanged() from the initBuildingSource() method:

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