Display a map
A lot of VIKTOR applications (especially in the infra sector) are strongly related to locations on a map. In those cases it can be helpful to display locations in a map visualization. This guide explains how to add an OpenStreetMap view to a VIKTOR entity. There are 2 different VIKTOR objects to realize this:
- If you want to build map elements using simple map geometry objects use the
MapView
object - If you want to use exports from external software to generate a geojson format (e.g. GIS), please see if the
GeoJSONView
is a better fit.
Map view
All features in the MapView
and GeoJSONView
expect coordinates in the WGS system. In case you are using the
RD-system you can make use of the RDWGSConverter
to obtain the
corresponding WGS coordinates:
lat, lon = RDWGSConverter.from_rd_to_wgs((x, y))
MapView
In the MapView
map primitives (features) can be used to construct markers, (poly)lines and surfaces on this map, to
add user relevant information.
Example implementation
import viktor as vkt
class Controller(vkt.Controller):
@vkt.MapView('Map view')
def get_map_view(self, params, **kwargs):
# Create some points using coordinates
markers = [
vkt.MapPoint(25.7617, -80.1918, description='Miami'),
vkt.MapPoint(18.4655, -66.1057, description='Puerto Rico'),
vkt.MapPoint(32.3078, -64.7505, description='Bermudas')
]
# Create a polygon
polygon = vkt.MapPolygon(markers)
# Visualize map
features = markers + [polygon]
return vkt.MapResult(features)
A map view can be combined with data using a MapAndDataView
Styling the map elements
VIKTOR supports the following arguments for styling the map elements:
- title: Title of a clickable map feature.
- description: Description of a clickable map feature. Supports styling with Markdown.
- color: Specifies the color of the map feature.
- entity_links: When clicking on the map feature, links towards multiple entities can be shown.
- icon: Icon to be shown (
MapPoint
only). See MapPoint for all available icons. - size: Marker size (
MapPoint
only). (>= v14.5.0)
import viktor as vkt
link_1 = vkt.MapEntityLink('First entity', first_entity_id)
link_2 = vkt.MapEntityLink('Second entity', second_entity_id)
vkt.MapPoint(
51.99311570849245, 4.385752379894256,
title='Location',
description='I am blue',
icon="pin",
size="small",
color=vkt.Color(90, 148, 230),
entity_links=[link_1, link_2]
)
Linking geo-fields to the view
When geo-fields (GeoPointField
, GeoPolylineField
, etc.) are used in the parametrization of an entity, corresponding
geo-objects (GeoPoint
, GeoPolyline
, etc.) will be directly available in the params
. These can easily be used in a
MapView
, by converting to the corresponding map feature (e.g. GeoPointField
-> GeoPoint
-> MapPoint
).
Assume the following params
:
params = {
'geo_point': <GeoPoint>,
'geo_polyline': <GeoPolyline>,
'geo_polygon': <GeoPolygon>
}
@vkt.MapView(...)
def get_map_view(self, params, **kwargs):
features = []
if params.geo_point:
features.append(vkt.MapPoint.from_geo_point(params.geo_point))
if params.geo_polyline:
features.append(vkt.MapPolyline.from_geo_polyline(params.geo_polyline))
if params.geo_polygon:
features.append(vkt.MapPolygon.from_geo_polygon(params.geo_polygon))
return vkt.MapResult(features)
Additional styling as explained in the previous section can be added via the class methods as well:
vkt.MapPoint.from_geo_point(params.geo_point, color=vkt.Color(90, 148, 230))
GeoJSONView
In the GeoJSONView
GeoJSON primitives can be used to construct markers, lines and surfaces on this map, to add user
relevant information. Another possibility is an export from an existing GIS program which you want to show to the user.
GeoJSON is a standard format for encoding geographic data structures such as points, lines and polygons (geojson.org). geojson.io is a good tool for quick experiments with geojson.
Example implementation
import viktor as vkt
class Controller(vkt.Controller):
@vkt.GeoJSONView('GeoJSON view')
def get_geojson_view(self, params, **kwargs):
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
4.385747015476227,
51.993107450558156
]
}
}
]
}
return vkt.GeoJSONResult(geojson)
A GeoJSON view can be combined with data using a GeoJSONAndDataView
Styling the map elements - simplestyle-spec GeoJSON properties
GeoJSON defines the geometrical properties of the map elements and not the styling. It does however allow to convey such information under the "properties" key of the respective element. Roughly following the simplestyle-spec, VIKTOR supports the following arguments for styling the map elements:
- icon (geometry type 'Point' only): icon to be shown (default: "pin"). See MapPoint for all available icons.
- marker-symbol (geometry type 'Point' only): same as icon
- marker-color: the color of a marker *
- marker-size: size of the marker ("small", "medium", "large")
- title: A title to show when this item is clicked
- description: text to show when this item is clicked. Supports styling with Markdown
- stroke: the color of a line as part of a polygon, polyline, or multigeometry *
- fill: the color of the interior of a polygon *
- stroke-opacity: the opacity of the line component of a polygon, polyline, or multigeometry **
- fill-opacity: the opacity of the interior of a polygon **
- stroke-width: the width of the line component of a polygon, polyline, or multigeometry (value must be a floating point number greater than or equal to 0)
- dash-size: size of the dashes of a dashed line, relative to the width of the path
- gap-size: size of the gaps of a dashed line, relative to the width of the path
- color rules; Colors can be in short form "#ace" or long form "#aaccee", and should contain the "#" prefix. Colors are interpreted the same as in CSS, in #RRGGBB and #RGB order
** opacity rules: Opacity value must be a floating point number greater than or equal to zero and less or equal to than one
Additional map elements
Apart from geometrical features, the map can be enriched by passing the following VIKTOR specific elements in the
MapResult
or GeoJSONResult
:
import viktor as vkt
@vkt.MapView(...)
def get_map_view(...):
...
legend = vkt.MapLegend(...)
labels = [
vkt.MapLabel(...),
...
]
return vkt.MapResult(features, labels=labels, legend=legend) # or GeoJSONResult
Interaction with map: selecting objects
Map view interaction enables the user to provide extra input by selecting features within a MapView
,
GeoJSONView
, MapAndDataView
or GeoJSONAndDataView
.
The interaction can be bound to all action buttons.
Implementation
To setup an interaction on an map view, the following parts need to be implemented:
- create an action button in the parametrization and assign an
interaction
- add unique
identifier
s on the map features that you want to be selectable - an
InteractionEvent
is inserted in the action method under theevent
argument. This event can be used to process the selection. The selection is not automatically stored (since you can also combine this with a DownloadButton and don't store anything on the entity). Choose the approriate button for processing the selection.
For example selecting an option in an OptionField
by means of selection in a MapView
is achieved by:
- adding a
SetParamsButton
withinteraction=MapSelectInteraction
pointing to the correct view - add unique identifiers on
MapFeature
s which are selectable - adding a controller method which processes the interaction event and returns a
SetParamsResult
. This makes sure the selection persists on the params.
import viktor as vkt
class Parametrization(vkt.Parametrization):
city = vkt.OptionField('City', ['Miami', 'Puerto Rico', 'Bermudas'])
select_button = vkt.SetParamsButton('Select city from map',
method='set_city_from_selection',
interaction=vkt.MapSelectInteraction('get_map_view', max_select=1))
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.MapView('Map view')
def get_map_view(self, params, **kwargs):
# Create some points using coordinates
miami_point = vkt.MapPoint(25.7617, -80.1918, description='Miami', identifier='Miami')
puerto_rico_point = vkt.MapPoint(18.4655, -66.1057, description='Puerto Rico', identifier='Puerto Rico')
bermudas_point = vkt.MapPoint(32.3078, -64.7505, description='Bermudas', identifier='Bermudas')
# Create a polygon
polygon = vkt.MapPolygon([miami_point, puerto_rico_point, bermudas_point])
# Add label of selected location
labels = []
if params.city == 'Miami':
labels = [vkt.MapLabel(miami_point.lat, miami_point.lon, "Miami", scale=3)]
elif params.city == 'Puerto Rico':
labels = [vkt.MapLabel(puerto_rico_point.lat, puerto_rico_point.lon, "Puerto Rico", scale=3)]
# Visualize map
features = [miami_point, puerto_rico_point, bermudas_point, polygon]
return vkt.MapResult(features, labels)
def set_city_from_selection(self, params, event, **kwargs):
selected_city = event.value[0]
return vkt.SetParamsResult({'city': selected_city})
MapSelectInteraction settings
The following settings are available:
min_select
: minimum number of selected objects by user (default 1)max_select
: maximum number of selected objects by user (optional)
Interaction groups
Sometimes you want to select certain features for one action and other features for another action in the same view. This can be achieved by defining interaction_groups
on the view result and define a selection
on the action button. When selection
is used, only the objects in the chosen interaction_group
are considered. Objects which only have an identifier will not be selectable.
An interaction_group
is a dictionary with the following structure:
- key (
str
), matching theselection
filter as defined on the button - value: sequence of
MapFeature
s which are part of the group. They can be referenced using theidentifier
or you can use the variable itself
import viktor as vkt
class Parametrization(vkt.Parametrization):
btn1 = vkt.SetParamsButton('Select from European cities',
method='set_city_from_selection',
interaction=vkt.MapSelectInteraction('visualize_map', selection=['eu_cities'], max_select=1))
btn2 = vkt.SetParamsButton('Select from US cities',
method='set_city_from_selection',
interaction=vkt.MapSelectInteraction('visualize_map', selection=['us_cities'], max_select=1))
class Controller(vkt.Controller):
parametrization = Parametrization
@vkt.MapView("Cities", 2)
def visualize_map(self, params, **kwargs):
...
amsterdam = vkt.MapPoint(52.377956, 4.897070, identifier='Amsterdam')
rotterdam = vkt.MapPoint(51.924419, 4.477733)
berlin = vkt.MapPoint(52.520008, 13.404954, identifier='Berlin')
new_york = vkt.MapPoint(40.712776, -74.005974, identifier='New York')
...
my_interaction_groups = {
'eu_cities': [amsterdam, berlin], # or point to the identifier: ['Amsterdam', 'Berlin']
'us_cities': [new_york], # or point to the identifier: ['New York']
}
return vkt.MapResult(..., interaction_groups=my_interaction_groups)
Testing
mock_View
decorator for easier testing of view methods
Methods decorated with @MapView
, @MapAndDataView
, @GeoJSONView
, or @GeoJSONAndDataView
need to be
mocked within the context of (automated) testing.
import unittest
from viktor.testing import mock_View
from app.my_entity_type.controller import MyEntityTypeController
class TestMyEntityTypeController(unittest.TestCase):
@mock_View(MyEntityTypeController)
def test_map_view(self):
params = ...
result = MyEntityTypeController().map_view(params=params)
self.assertEqual(result.features, ...)
self.assertEqual(result.labels, ...)