Working with spatial data and displaying maps is an inalienable part of many business apps. It can be city or regional information systems, oil-and-gas industry applications, transport infrastructure management systems, as well as delivery services and many more. Here, in CUBA Platform, besides basic the out-of-the-box functionality for creating such apps, we have a rich ecosystem of additional components. One of those components is Charts and Maps, which can both display charts and enable integrating Google Maps into the visual part of application. Last year Google updated the Maps terms of service which have caused the cost increase, and implied a requirement to have a billing account for using the API. These changes made most of our clients look towards alternative maps providers and encouraged us to develop a new maps add-on.
Now we are happy to introduce a completely new CUBA Maps add-on.
CUBA Maps is an easy way to enrich your application with visual representation and intuitive manipulation for spatial data. The component operates both with raster and vector data. You are free to use any preferable map provider compatible with the Web Map Service protocol as well as XYZ tile services as raster data. For working with vector data the add-on uses geometry data types (point, polyline, polygon) from JTS Topology Suite (JTS) — the most popular Java library for working with spatial data. The add-on provides all required features for building a comprehensive geographical information system on CUBA.
This article describes the new features that the new Maps add-on brings and compares it to our previous map component.
Layer-based structure
The add-on supports the traditional multi-layer structure commonly used in professional GIS systems. Basically, layers can be of raster and vector types. Raster layers consist of raster images, while vector layers consist of vector geometries.
The add-on supports the following types of layers:
- Tile layer is used to display tiles provided by XYZ tile services.
- Web Map Service (WMS) layer is used to display images from WMS services.
- Vector layer contains geo-objects (entities with geometry attributes).
These layers are represented as structural units of maps. For example, one layer serves as a tiled basemap, the second layer contains polygons describing districts, the third layer can consist of geographical points (locations of various objects like customers, shops and so on). Сombining these layers, you build a complete map:
This approach gives you full freedom to create well-structured maps with any content.
CUBA Maps adds to your toolset a new visual component — GeoMap
. You can define the general map parameters along with the layers in the component’s XML descriptor as in the example below:
<maps:geoMap id="map" height="600px" width="100%" center="2.348, 48.853" zoom="8">
<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>
Such an approach increases flexibility which the old Charts and Maps component lacked. New layers-based approach gives you:
- Multiple layers. For example, you can combine tiles provided by different services.
- Layers provide an abstraction that unites related objects. In the previous Charts and Maps component all map content (like points, polygons, etc) was piled up in the UI component. To structure these objects, project teams had to implement additional logic that handled it.
- Declarative way of defining the layers. As it was shown above, you can describe your map structure by defining all layers in the XML descriptor. In most cases it is enough to avoid writing any additional code in the screen controller. In Charts and Maps it was almost inevitable to implement some logic in the screen controller.
With Tile layers/WMS layers you are free to use the tile provider that meets your needs. You are not bound to a particular vendor as it was in Charts and Maps.
Vector layers enable simple displaying, interactive editing and drawing geo-objects on a map.
It should be mentioned that by default GeoMap
UI component has a special utility layer — Canvas. Canvas provides a straightforward API to display and draw geometries on a map. We will see the examples of using Canvas further in this article.
Geo-objects
Imagine an entity having a property associated with a geometry (point, polyline, polygon). That’s what we mean by a geo-object. The component simplifies working with geo-objects.
For example, consider the Address geo-object:
@Entity
public class Address extends StandardEntity {
...
@Column(name = "LOCATION")
@Geometry
@MetaProperty(datatype = "GeoPoint")
@Convert(converter = CubaPointWKTConverter.class)
protected Point location;
...
}
It has a property location
of the org.locationtech.jts.geom.Point
type that comes from the JTS Topology Suite (JTS) library. The add-on supports the following JTS geometry types:
org.locationtech.jts.geom.Point
— point.org.locationtech.jts.geom.LineString
— polyline.org.locationtech.jts.geom.Polygon
— polygon.
The location
property is annotated with @Geometry
annotation. This annotation marks the property to be used when displaying the entity on a map. The geometry property also has the following annotations:
@MetaProperty
— specifies the corresponding datatype for the attribute. Datatypes are used by CUBA framework for converting values to and from strings.@Convert
— specifies a JPA converter for a persistent attribute. A JPA converter performs type conversion between the database and Java representation of an attribute.
The component comes with a set of spatial datatypes and JPA converters. For more information please refer to the component’s documentation. It is possible to specify your own implementation of the JPA converter, which means that you can work with various providers of spatial data (for example, PostGIS).
Thus, to turn your entity into a geo-object, you should declare a meta property of a JTS geometry type and annotate it with @Geometry
. Another option is to create a non-persistent geometry property by exposing getter/setter methods. It can be helpful if you don't want to change your model and regenerate DDL scripts.
For example, consider the Address entity having latitude/longitude in separate fields:
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);
}
...
}
If you take the second option, make sure to invoke propertyChanged
method in the setter, because the component automatically updates the geometry on a map after this event was fired.
Now that we have prepared our geo-object class, we can add instances of this class to the vector layer. Vector layer is a data-aware component acting as a connector between data (geo-objects) and a map. To bind geo-objects to the layer you need to pass a datacontainer (or datasource in case of using in legacy screens) to the vector layer. This can be accomplished in the XML descriptor:
<maps:geoMap id="map">
<maps:layers>
...
<maps:vector id="addressesLayer" dataContainer="addressesDc"/>
</maps:layers>
</maps:geoMap>
As a result, the instances of the Address
class located in the addressesDc
data container will be displayed on the map.
Let’s consider a basic task of creating an editor screen for a geo-object with the map, where you can edit a geometry. To achieve this, you need to declare the GeoMap
UI component in the screen XML descriptor and define a vector layer connected with the editing object.
<maps:geoMap id="map" height="600px" width="100%" center="2.348, 48.853" zoom="8">
<maps:layers selectedLayer="addressLayer">
<maps:tile ..."/>
<maps:vector id="addressLayer" dataContainer="addressDc" editable="true"/>
</maps:layers>
</maps:geoMap>
By marking the layer as editable you enable interactive geo-object editing. If geometry property of the editing geo-object has the empty value, then the map will be automatically turned into the drawing mode. As you can see, the only thing you need to do is to declare a vector layer and provide a datacontainer to it.
And this is it. If we had used Charts and Maps for solving this problem, it would be necessary to write a pretty big amount of code in the screen controller in order to achieve this functionality. The new Maps component provides a really straightforward way of solving such tasks.
Canvas
Sometimes you do not want to work with entities. Instead, you want a simple API for easy adding and drawing geometries on a map like it was in Charts and Maps. For this purpose, the GeoMap
UI component has a special layer called Canvas. It is a utility layer that a map has by default and which provides a straightforward API for adding and drawing geometries on a map. You can get the Canvas by calling the map.getCanvas() method
.
Next, we will overview some basic tasks, how they were solved in Charts and Maps and how we can do the same using the Canvas.
Displaying geometries on a map
In Charts and Maps geometry objects were created using the map UI component as a factory and then added to the map:
Marker marker = map.createMarker();
GeoPoint position = map.createGeoPoint(lat, lon);
marker.setPosition(position);
map.addMarker(marker);
New Maps add-on works with geometry classes from the JTS library:
CanvasLayer canvasLayer = map.getCanvas();
Point point = address.getLocation();
canvasLayer.addPoint(point);
Editing geometries
In Charts and Maps you were able to mark geometry objects as editable. When these geometries were modified via UI, corresponding events were fired:
Marker marker = map.createMarker();
GeoPoint position = map.createGeoPoint(lat, lon);
marker.setPosition(position);
marker.setDraggable(true);
map.addMarker(marker);
map.addMarkerDragListener(event -> {
// do something
});
In Maps, when you add a JTS geometry on the Canvas, the method returns you a special object that represents this geometry on a map: CanvasLayer.Point
, CanvasLayer.Polyline
or CanvasLayer.Polygon
. Using this object you can set multiple geometry parameters using fluent API, subscribe to geometry-related events, or use this object when you want to remove the geometry from the Canvas.
CanvasLayer canvasLayer = map.getCanvas();
CanvasLayer.Point location = canvasLayer.addPoint(address.getLocation());
location.setEditable(true)
.setPopupContent(address.getName())
.addModifiedListener(modifiedEvent ->
address.setLocation(modifiedEvent.getGeometry()));
Drawing geometries
In Charts and Maps there was an auxiliary drawing component — DrawingOptions
. It was used to enable drawing capabilities in a map. After a geometry was drawn the corresponding event was fired:
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
});
In Maps, Canvas provides a set of methods for drawing geometries. For example, to draw a polygon invoke canvas.drawPolygon()
method. After this method is called the map will turn into the drawing mode. The method accepts Consumer<CanvasLayer.Polygon>
function, in which you can perform additional actions with the drawn polygon.
canvasLayer.drawPolygon(polygon -> {
territory.setPolygon(polygon.getGeometry());
});
Features for geoanalysis
Clustering
Another useful feature that comes with the new Maps add-on is clustering of points. When a layer contains a large number of points you can enable clustering to group nearby points into clusters and make the map more good-looking:
Clustering can be enabled by adding the cluster
element inside the vector
in the XML descriptor:
<maps:vector id="locations" dataContainer="locationsDc" >
<maps:cluster/>
</maps:vector>
You can also enable clustering based on the point's weight specified by geo-object's property:
<maps:vector id="orders" dataContainer="ordersDc" >
<maps:cluster weightProperty="amount"/>
</maps:vector>
Heatmaps
Heatmaps provide a visual representation of data density across a set of geographical points. GeoMap
UI component provides a method for adding a heatmap overlay to a map: addHeatMap(Map<Point, Double> intensityMap)
.
Conclusion
Working with spatial data is essential for many business applications. CUBA Maps provides your CUBA application with all required features to achieve this functionality.
The layer-based structure lets you easily create maps with any content. With Tile layers/WMS layers you can use any preferable provider for your basemap. Vector layers help to effectively work with a group of related geo-objects. Canvas layer provides a straightforward API to display and draw geometries on a map.
The add-on integrates spatial types from the JTS library, which makes it compatible with many other frameworks (for example, GeoTools) to solve a wide range of GIS tasks.
We hope you enjoy the add-on and we are looking forward to your feedback!