GeoJson Viewer (QML)

The GeoJson viewer example demonstrates how to manipulate MapItems, handle user input and I/O to and from a GeoJson file.

The example displays a map with various MapItems. The MapItems are either imported from a GeoJson file, using the GeoJsonData API of QtLocation or drawn by the user using TapHandlers.

Examples for GeoJson files can be found in the directory data within the example directory.

To draw a MapItem, right click on an empty part of the map and select an item type of your choice in the appearing menu. The next clicks will define the chosen item. The example allows to draw MapCircles, MapRectangles, MapPolygons and MapPolylines. Items that are fully defined by two points, i.e. circles and rectangles, are drawn with two clicks of the left mouse button. Items that are defined by multiple points, i.e. polygons and polylines, are created by an arbitrary amount of left button clicks and completed with the right mouse button. Items drawn this way are saved as points, polygons and polylines to fit the GeoJson specification, see https://geojson.org/.

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.

Creating a MapView

First we create a base map on which all items can be placed on. We take advantage of a MapView element that combines a basic Map with input handling (mouse wheel, drag, etc.). The underlying Map can be accessed with map property. If you miss a property in MapView it can be most likely accessed with MapView.map.

 MapView {
     id: view
     anchors.fill: parent
     map.plugin: Plugin { name: "osm" }
     map.zoomLevel: 4
     map.center: QtPositioning.coordinate(3, 8)
 }

Setting up the GeoJson Model / Display MapItems

In order to display file contents on the map we will use a design pattern known as Model/View Programming. First we need to set up a suitable view, in this example a MapItemView element. Its parent must be set to the underlying map of the MapView to correctly display all items placed in it.

 MapItemView {
     id: miv
     parent: view.map
 }

Next we need a suitable model, representing a GeoJSON document. For this purpose QtLocation offers the GeoJsonData element that can read and write GeoJSON files. It can be easily instantiated

 GeoJsonData {
     id: geoDatabase
     sourceUrl: ":/data/11-full.json"
 }

and assigned to the MapItemView.

 model: geoDatabase.model

The file 11-full.json is loaded on start-up as an example.

Finally we need a delegate, translating the model data into a representation of items, filling the MapItemView.

 delegate: GeoJsonDelegate {}

The GeoJsonDelegate element is declared in the file GeoJsonDelegate.qml. It is a DelegateChooser element, to take into account the varying properties of different geometry types.

 DelegateChooser {
     id: dc
     role: "type"
 }

The DelegateChooser contains a DelegateChoice for each geometry type that can be found in a GeoJson file. The property role will be matched with DelegateChoice.roleValue to determine the correct delegate.

As an example, a point, described with "type":"Point" in GeoJson, is represented by a MapCircle on the MapItemView:

 DelegateChoice {
     roleValue: "Point"
     delegate: MapCircle {
         property string geojsonType: "Point"
         property var props: modelData.properties
         geoShape: modelData.data
         radius: (props && props.radius) || 20*1000
         border.width: 2
         border.color: hh.hovered ? "magenta" : Qt.darker(color)
         opacity: dc.defaultOpacity
         color: (props && props.color) || (parent && parent.props && parent.props.color) || dc.defaultColor
     }
 }

Properties of the MapCircle, such as color or radius are attempted to be read from the GeoJson file that is available in form of the modelData property. However, this is not a strict standard of GeoJson and fallback values are set for all properties.

Writing MapItems to GeoJson

To write MapItems to a GeoJson file we can simply call the GeoJsonData::saveAs function with the designated filename. This writes all items in the current model to the designated file. Any other items that should be written to the file have to be added to the model first using the function GeoJsonData::addItem or GeoJsonData::setModelToMapContents.

 geoDatabase.saveAs(fileWriteDialog.selectedFile)

User Interaction with MapItems

To handle user interactions we will use PointHandlers. They are especially well suited for the task as they conform to the exact shape of the underlying item, in contrast to MouseArea, which always covers a square shape. MapItems that are imported from a GeoJson file get their own HoverHandler and TapHandler directly in the delegate:

 TapHandler {
     onTapped: {
         if (props !== undefined)
             console.log(props.name)
         else if (parent.parent.geojsonType == "MultiPoint")
             console.log(parent.parent.props.name)
         else
             console.log("NO NAME!", props)
     }
 }
 HoverHandler {
     id: hh
 }

The TapHandler is used to write some information about the item on the console when the item is tapped. The HoverHandler is used to highlight items that lie beneath the mouse pointer. This is implemented by describing the property border.color depending on the property / state hovered of the HoverHandler.

Adding new Items

A combination of HoverHandler and TapHandler for the MapView allows us to react to mouse movements and clicks by the user.

If the TapHandler emits a singleTapped signal, we will create or modify a new MapItem on LeftButton and finish the MapItem on RightButton. If there is no item to finish then the RightButton will open a menu.

 onSingleTapped: (eventPoint, button) => {
     lastCoordinate = view.map.toCoordinate(tapHandler.point.position)
     if (button === Qt.RightButton) {
         if (view.unfinishedItem !== undefined) {
             view.finishGeoItem()
         } else
             mapPopupMenu.show(lastCoordinate)
     } else if (button === Qt.LeftButton) {
         if (view.unfinishedItem !== undefined) {
             if (view.unfinishedItem.addGeometry(view.map.toCoordinate(tapHandler.point.position), false)) {
                 view.finishGeoItem()
             }
         }
     }
 }

The pointChanged signal is used to temporarily update a MapItem, giving the user a preview.

 HoverHandler {
     id: hoverHandler
     property variant currentCoordinate
     grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType

     onPointChanged: {
         currentCoordinate = view.map.toCoordinate(hoverHandler.point.position)
         if (view.unfinishedItem !== undefined)
             view.unfinishedItem.addGeometry(view.map.toCoordinate(hoverHandler.point.position), true)
     }
 }

Mapitems are generated from prototypes that are defined in separate qml files. They are created using the createComponent function and added to the map with addMapItem. A reference to the new item is stored for further manipulation by the user.

 function addGeoItem(item)
 {
     var co = Qt.createComponent('mapitems/'+item+'.qml')
     if (co.status === Component.Ready) {
         unfinishedItem = co.createObject(map)
         unfinishedItem.setGeometry(tapHandler.lastCoordinate)
         unfinishedItem.addGeometry(hoverHandler.currentCoordinate, false)
         view.map.addMapItem(unfinishedItem)
     } else {
         console.log(item + " is not supported right now, please call us later.")
     }
 }

Adding the item to the Map is sufficient to display the item. However, in order to further use the item (e.g. saving it to a file), it has to be added to the model. This is done after editing is finished:

 function finishGeoItem()
 {
     unfinishedItem.finishAddGeometry()
     geoDatabase.addItem(unfinishedItem)
     map.removeMapItem(unfinishedItem)
     unfinishedItem = undefined
 }

Removing Items

To remove all items from the map, we simply call the reset function of the GeoJsonData object

 function clearAllItems()
 {
     geoDatabase.clear();
 }

Example project @ code.qt.io