2. 地图中显示标记
为实体和页面添加属性
我们为 User
实体添加 location
属性:
在 Jmix 工具窗口双击 User
实体,并选择其最后一个属性(为了将新属性添加至最后):
点击 Attributes 工具栏的 Add()。在 New Attribute 对话框中,Name 输入 location
,Attribute type 选择 ASSOCIATION
,Type 选择 Location
。勾选 One to One 和 Owning Side 复选框。
为了建立一对一的 引用,Studio 会推荐在 Location
实体中生成一个反向属性。
点击 Yes,然后下一个对话框中点击 OK。
选择 location
属性,点击 Attributes 工具栏的 Add to Views():
随后的对话框中显示所有能展示 User
实体的视图。我们选择 User.detail
视图:
Studio 会自动在 User.detail
视图的 fetchPlan
中添加 location
属性,并在 formLayout
中增加一个 entityPicker
组件。
点击主工具栏的 Debug 按钮()。
应用程序启动前,Studio 会生成一个 Liquibase changelog:
点击 Save and run。
Studio 会先在数据库运行 changelog,然后构建并启动应用程序。
应用程序启动完成后,可以在浏览器打开 http://localhost:8080
用凭证 admin/admin
登录。
从 Application
菜单选择 Users
打开用户列表视图。
点击 Create。表单的底部会显示用于选择位置的控件:
创建空视图
如果你的应用程序还在运行,请点击主工具栏的 Stop 按钮()。
在 Jmix 工具窗口,选在 New()→ View:
在 Create Jmix View 窗口,选择 Blank view
模板:
点击 Next。
向导的下一步中,输入以下内容:
-
Descriptor name:
location-lookup-view
-
Controller name:
LocationLookupView
-
Package name:
com.company.onboarding.view.locationlookup
删除 Parent menu item,这个视图不用这个设置。
点击 Next,然后 Create。
Studio 会生成一个空的视图,并在设计器显示:
打开新视图
我们的新视图是要从 user 的详情视图打开的,这里要用到 Location 字段。
需要将 Studio 生成的 entityPicker
组替换为 valuePicker
组件。打开 user-detail-view.xml
找到 formLayout
中的 entityPicker
组件:
<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
元素。
在 Jmix UI 结构面板或 XML 中选择 valuePicker
,然后点击组件面板的 Add 按钮。在下拉列表中,选择 Actions → Action。
首先,选择一个 New Base Action
并点击 OK。
设置操作的 id
为 select
, icon
为 vaadin:search
。
然后,为 locationField
添加一个内置的 value_clear
操作:
在 Jmix UI 结构面板或 XML 中选择 select
操作。然后在 Jmix Inspector 中切换至 Handlers tab,生成一个 ActionPerformedEvent
的处理方法:
在 ActionPerformedEvent
的处理方法中,添加打开 LocationLookupView
的逻辑:
@Autowired
private DialogWindows dialogWindows; (1)
@Subscribe("locationField.select")
public void onLocationFieldSelect(final ActionPerformedEvent event) {
dialogWindows.view(this, LocationLookupView.class).open();
}
1 | DialogWindows 提供以对话框的方式打开视图的流式接口。 |
启动应用程序,在 Application
菜单选择 Users
。点击 Create 打开 User.detail
视图。找到 Location 字段,点击()Search 按钮。该操作会打开 LocationLookupView
对话框。
现在有时间可以回顾一下我们在视图中的改动。
为 LocationLookupView 添加组件
首先,添加一个控件用于展示当前地图中选择的位置。在操作面板中,点击 Add Component,找到 entityPicker
,双击进行添加。配置该组件的属性如下:
<entityPicker id="currentLocationField"
metaClass="Location"
readOnly="true"
width="20em"
label="msg://currentLocationField.label"/>
下一步,我们将添加两个 hbox
容器:
-
第一个将包含一组位置以及一个地图。
-
第二个将包含 Select 和 Cancel 按钮。
<hbox padding="false"
height="100%"
width="100%"/>
<hbox id="controlLayout"/>
点击操作面板中的 Add Component,然后将 Layouts → VBox
拖放至 Jmix UI 结构面板中的第一个 hbox
元素。配置 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 窗口中,Entity 字段选 Location
,点击 OK:
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 facet 以触发刚创建的数据加载器。
默认的查询语句会加载所有的 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 | 在 query` 中定义 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>
可以运行应用程序并查看新加的功能。
添加地图
将光标移至 vbox
元素的后面。点击操作面板中的 Add Component,找到 GeoMap
并双击进行添加。
此时,Jmix UI 结构和 XML 中都会在 vbox
元素的下方添加新的 geoMap
元素。按照下面的代码配置 id
、height
和 width
属性。
<maps:geoMap id="map"
height="100%"
width="100%"/>
接下来,为地图引入一个 OsmSource 的瓦片层,设置一个 map view,然后添加一个 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>
启动应用程序查看新添加的功能。
可以看到,地图并不能适配视图。因此,我们需要修改视图的大小。为 LocationLookupView
视图控制器添加 @DialogMode
注解:
@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 保存更改并切换至运行中的程序。点击 Location 控件的 Search 按钮()。此时打开的 LocationLookupView
视图大小就是我们刚配置的大小。
使用自定义标记 部分将介绍如何为 offices 和 coworking 使用不同的标记。
添加按钮
下面我们添加两个按钮:Select
按钮用于保存当前的位置信息,Cancel
按钮可以直接取消不保存。
打开 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>
通过 Jmix UI inspector panel → Handlers tab 生成操作的处理方法:
实现 select
操作处理方法:
@Subscribe("select")
public void onSelect(final ActionPerformedEvent event) {
close(StandardOutcome.SELECT); (1)
}
1 | close() 能关闭当前视图。StandardOutcome.SELECT 参数可以由调用端代码进行处理。我们 在后面 进行处理。 |
按照下面的代码配置按钮的 ID 以及操作:
<hbox id="controlLayout">
<button id="selectBtn" action="select"/>
<button id="cancelBtn" action="cancel"/>
</hbox>
使用自定义标记
切换至 Project 工具窗口,在 /src/main/resources/META-INF/resources/icons/
目录可以看到为 offices 和 coworking 准备的不同图标:
打开 LocationLookupView
控制器,并注入 buildingSource
。
@ViewComponent("map.dataVectorLayer.buildingSource")
private DataVectorSource<Location> buildingSource;
操作面板的 Inject 按钮可以用来注入视图组件或 Spring bean: |
下一步,引入一个方法自定义标记:
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
:
点击 OK。Studio 会自动生成一个方法的桩代码。我们在里面调用 initBuildingSource()
:
@Subscribe
public void onInit(final InitEvent event) {
initBuildingSource();
}
启动应用程序并打开 LocationLookupView
。可以看到不同类型位置的标记也不同。
处理标记事件
一旦用户在地图中选择了一个标记,则标记的位置将填入 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());
});
}
运行应用程序查看最新改动。现在点击标记时,地图会以标记的坐标为中心,并进行放大。
现在我们要在 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 控件。 |
在 initBuildingSource()
中调用 onLocationChanged()
:
private void initBuildingSource(){
//...
buildingSource.addGeoObjectClickListener(clickEvent -> {
//...
onLocationChanged(location);
});
}