Skip to main content

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

note

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

from viktor.views import MapPolygon, MapPoint, MapResult, MapView


class Controller(ViktorController):
...

@MapView('Map view', duration_guess=1)
def get_map_view(self, params, **kwargs):
# Create some points using coordinates
markers = [
MapPoint(25.7617, -80.1918, description='Miami'),
MapPoint(18.4655, -66.1057, description='Puerto Rico'),
MapPoint(32.3078, -64.7505, description='Bermudas')
]
# Create a polygon
polygon = MapPolygon(markers)

# Visualize map
features = markers + [polygon]
return 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)
from viktor import Color
from viktor.views import MapEntityLink

link_1 = MapEntityLink('First entity', first_entity_id)
link_2 = MapEntityLink('Second entity', second_entity_id)
MapPoint(
51.99311570849245, 4.385752379894256,
title='Location',
description='I am blue',
icon="pin",
size="small",
color=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>
}
@MapView(...)
def get_map_view(self, params, **kwargs):
features = []

if params.geo_point:
features.append(MapPoint.from_geo_point(params.geo_point))
if params.geo_polyline:
features.append(MapPolyline.from_geo_polyline(params.geo_polyline))
if params.geo_polygon:
features.append(MapPolygon.from_geo_polygon(params.geo_polygon))

return MapResult(features)

Additional styling as explained in the previous section can be added via the class methods as well:

MapPoint.from_geo_point(params.geo_point, color=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.

note

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

from viktor.views import GeoJSONResult, GeoJSONView


class Controller(ViktorController):
...

@GeoJSONView('GeoJSON view', duration_guess=1)
def get_geojson_view(self, params, **kwargs):
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
4.385747015476227,
51.993107450558156
]
}
}
]
}
return 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:

...
from viktor.views import MapLabel, MapLegend


@MapView(...)
def get_map_view(...):
...

legend = MapLegend(...)
labels = [
MapLabel(...),
...
]

return MapResult(features, labels=labels, legend=legend) # or GeoJSONResult

Interaction with map: selecting objects

New in v13.2.0

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 identifiers on the map features that you want to be selectable
  • an InteractionEvent is inserted in the action method under the event 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 with interaction=MapSelectInteraction pointing to the correct view
  • add unique identifiers on MapFeatures 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.
...
from viktor.parametrization import MapSelectInteraction, SetParamsButton
from viktor.result import SetParamsResult
from viktor.views import MapLabel


class Parametrization(ViktorParametrization):
city = OptionField('City', ['Miami', 'Puerto Rico', 'Bermudas'])
select_button = SetParamsButton('Select city from map',
method='set_city_from_selection',
interaction=MapSelectInteraction('get_map_view', max_select=1))


class Controller(ViktorController):

label = 'My Entity Type'
parametrization = Parametrization

@MapView('Map view', duration_guess=1)
def get_map_view(self, params, **kwargs):
# Create some points using coordinates

miami_point = MapPoint(25.7617, -80.1918, description='Miami', identifier='Miami')
puerto_rico_point = MapPoint(18.4655, -66.1057, description='Puerto Rico', identifier='Puerto Rico')
bermudas_point = MapPoint(32.3078, -64.7505, description='Bermudas', identifier='Bermudas')

# Create a polygon
polygon = MapPolygon([miami_point, puerto_rico_point, bermudas_point])

selected_city = params.city

# add label of selected location # TODO: consider color change
labels = []
if selected_city == 'Miami':
labels = [MapLabel(miami_point.lat, miami_point.lon, "Miami", scale=3)]
elif selected_city == 'Puerto Rico':
labels = [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 MapResult(features, labels)

def set_city_from_selection(self, params, event, **kwargs):
selected_city = event.value[0]
return 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 the selection filter as defined on the button
  • value: sequence of MapFeatures which are part of the group. They can be referenced using the identifier or you can use the variable itself
class MyParametrization(ViktorParametrization):
btn1 = SetParamsButton('Select from European cities',
method='set_city_from_selection',
interaction=MapSelectInteraction('visualize_map', selection=['eu_cities'], max_select=1))
btn2 = SetParamsButton('Select from US cities',
method='set_city_from_selection',
interaction=MapSelectInteraction('visualize_map', selection=['us_cities'], max_select=1))


class MyController(ViktorController):
...
parametrization = MyParametrization

@MapView("Cities", 2)
def visualize_map(self, params, **kwargs):
...
amsterdam = MapPoint(52.377956, 4.897070, identifier='Amsterdam')
rotterdam = MapPoint(51.924419, 4.477733)
berlin = MapPoint(52.520008, 13.404954, identifier='Berlin')
new_york = 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 MapResult(..., interaction_groups=my_interaction_groups)

Testing

New in v13.3.0

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, ...)