blog

Using map in Qt mobile application with OpenStreetMap map data with your own tileserver – basics

decoration

Using a map in your app

Many applications need a map. There is a multitude of possibilities of how the map can be used in an application, especially in a mobile app. Some apps need map to show real-world information for their users, like some kind of Points Of Interest (POIs) or the shortest path to them. Other apps may need map as a playing field in many kinds of games, from AR games like Pokemon, to a LARP games or Treasure Hunt-like games and many others. Yet another apps can use map to allow some social interactions between users or to organize sport activities.

Qt, as of version 5.0, comes with Qt Location API, including QML Map object. It is ready solution to display map in your application. You can just select your map plugin, type some plugin parameters and it is done… well, not so fast. It would be great if it would be as simple as that, but in reality you need one more thing: a data source for your map.

Map data

Map data is generally available in two formats. First format is a vector data, stored usually as objects in some database. This format is great for pathfinding, navigating, geolocation and other similar services. But it is much harder to display it as a map.

Second format are map tiles. Map tiles are prerendered fragments of a map with each tile having specific known coordinates. These prerendered tiles come in sets for many zoom levels – each zoom level has its own set of tiles, giving you flexibility which zoom levels include in your app and which not.

There are many sources of tiles, especially when you are developing free app. But when you develop commercial app, your choice is limited to:
– using paid options (the most popular are Google Maps and HereWeGo),
– creating your own tile server, using open source software and open source data.

Using paid options is actually almost as simple as using QML Map object and setting up some parameters (after buying and paying for this service), but payments for using external map data source can be high, especially for a startup or when creating an app with large map traffic. The other reason to not use commercial maps is when you simply want to have a control on your data and do not depend on external services. This is where open source software comes to help.

Pros and cons for using your own tileserver

Having your own tileserver can be a great solution for many startups, but it comes at a price.

Pros:
– makes you independent of third party solutions,
– gives you control of your data (for example – Google Maps have some advertisements rendered directly on maps, you cannot get rid of them),
– gives you control on how your map will look like,
– it is free (but you will still have to pay for infrastructure: server(s) and connection).

Cons:
– you need to have some good hardware: for serving a map of the whole world it is recommended to have a machine with at least 32Gb RAM and 2TB SSD storage, with at least 8 cores, but it won’t be enough for larger traffic,
– you need to maintain your tileserver infrastructure yourself,
– you need to regularly update your maps,
– when your traffic grows, you need to scale up your simple server – for example by caching generated tiles on many fileservers and using some load balancing,
– serving tiles generates large network traffic and depending on your pricing plan, this network load can also be very costly,
– you have to credit OSM with notice “© OpenStreetMap contributors” on your map.

Setting your own tileserver

The most mature open source map data and software projects is OpenStreetMap. This project collects map data as open source input from its volunteers, pretty much like a Wikipedia. It is free to download its map data and to use it in your apps as you want. The problem is that this data is a vector data. Fortunately, there is a full compatible open source software stack that can convert this data into a tileset. This stack includes:
– Ubuntu server (you could use other Linux distribution as well, but Ubuntu has most of other tools already included),
– Postgres GIS database extension – to deal with vector map data,
– osm2pgsql – scripts to convert data from OSM format to GIS database,
– Mapnik – app that renders some chunk of map data as a tile, using predefined stylesheet,
– Renderd – daemon to supervise rendering map tiles,
– mod_tile – an Apache module converting requests for a tile into renderd calls and manages cache of prerendered tiles.

If you want exact instructions of how to install and configure each of these components, there are excellent step-by-step tutorials for different Ubuntu versions at Switch2Osm project page: https://switch2osm.org/serving-tiles/. You simply need to follow these instructions (beware, it can be a veeeeryyyy looooong process, and sometimes needs some tweaking and/or troubleshooting).

It is worth noting that, contrary to paid map tiles, you can have complete control of how your map will look:
– you can use one of ready-to-use stylesheets downloaded from any source mentioned here: https://wiki.openstreetmap.org/wiki/Stylesheets,
– you can modify any of these stylesheets changing the lookout of any element, using different fonts for inscriptions, etc.,
– you can even create your own stylesheet from scratch (only for advanced users, it is a long and tedious work, but gives the most complete option).

How does it work

Generating tiles is a time-consuming operation. Moreover, tileservers don’t generate all tiles at once, but rather the tiles are generated on demand. When some tile is requested, mod_tile looks for it in the cache. If it doesn’t find the tile, it simply asks renderd to render it. If it finds the tile, it checks when the tile was rendered. Files that are not too old are served from cache, but files old enough to consider them as expired are deleted and generated again. This way tiles of most of the areas that are frequently visited are served mostly from cache and regenerated occasionally only when necessary. Tiles for areas not visited at all (for example lakes or forests on maximum zoom level) are not generated at all.

You can use this mechanism to set up some prerendering script, which will force prerendering of tiles of selected areas (for example, cities) when server load is low, for example in the middle of the night and, even further, it can cache them on load-balanced fileservers. Given that tile counts rise exponentially with zoom level (count of tiles = 4zoom level), it is reasonable to prerender all tiles up to zoom level 12 when you need to cover whole world, up to zoom level 15 or 16 for cities, and higher zoom levels only on demand (or exclude them at all – it depends on your use case). When prerendering map of a smaller area of the world you can prerender larger part of tiles – you can estimate how much storage you need for your tileset with this calculator: http://tools.geofabrik.de/calc/.

So, now you have a working tileserver, what’s next?

When you have a working tileserver you can use it in any way you want: you can use tiles in a Qt app, you can embed a map (using the same tiles) on your website using Leaflet library, or even start serving these tiles commercially.

Using a map in QML application

Setting up a QML Map object

Integrating a basic map in QML application is relatively easy. We need to set up a Plugin object, where we have to define what type of map tiles we are using (OSM, HereWeGo etc.), set up address, authorization data, copyrights and other plugin properties. Then we need to add Map object, where we have to define start coordinates and zoom level, and it is ready – we have a working browsable map in our application.

Minimal working example:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtLocation 5.12
import QtPositioning 5.12
Window
{
visible: true
    width: 640
    height: 480
    title: qsTr(“Hello World”)
    Plugin
    {
        id: mapPlugin
        name: “osm”
        PluginParameter
        {
            name: “osm.mapping.custom.host”
            value: “YOUR_TILESERVER_ADDRESS”
        }
    }
    Map
    {
        id: map
        plugin: mapPlugin
        anchors.fill: parent
      activeMapType: supportedMapTypes[supportedMapTypes.length – 1]
        zoomLevel: 12
        minimumZoomLevel: 5
        maximumZoomLevel: 17
        center: QtPositioning.coordinate(54.2,16.2)
    }
}

Interacting with a map

Basic map allows only basic operations: browsing, zooming and tilting. To make map more usable, we need to add some other data to it. All items that you can add to map are collectively called overlays.

Adding overlays to a map

There are some simple types of overlays, such as:
– MapCircle – draws a circle on a map, for example to show range,
– MapRectangle – draws a rectangle on a map, for example to mark some area,
– MapPolygon – draws a polygon on a map, for example to precisely show borders and area of a city,
– MapIconObject – draws an icon on a map, for example to mark some point of interest,
– MapPolyline – draws a polyline on a map,
– MapRoute – special case of a polyline, where line data is a result of route query and shows route from start to destination,
– MapQuickObject – which gives us a possibility to add any Qt Quick object to a map, offering almost limitless possibilities to decorate our map with data.

All overlay items can be made clickable, by adding MouseArea to them.

There is also a couple of complex types for a map: such as MapItemView or MapObjectView, giving us possibility to populate map with items and objects generated from a model. These types are really helpful when showing a list of points of interest on a map.

Showing user’s position

When GPS position is available (for example when building app for mobiles), you can add user’s current position to your map with PositionSource object. It updates its position property regularly, giving you opportunity to set some marker on current user position. It is really convenient because it allows us to find the closest items to user’s current position or to find a route from user’s position to some other point.

Example:

file Map.qml:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtLocation 5.12
import QtPositioning 5.12
Window
{
    visible: true
    width: 640
    height: 480
    title: qsTr(“Hello World”)
    id: top
    property real marker_base_height: 60
    Plugin
    {
        id: mapPlugin
        name: “osm”
        PluginParameter
        {
            name: “osm.mapping.custom.host”
            // INSERT YOUR HOST HERE, OpenStreetMap tileserver is for limited testing only
            value: “https://c.tile.openstreetmap.org/”
        }
    }
    ListModel
    {
        id:dataModel
        ListElement { lat: 54.196; lon: 16.234; name: “test1”; }
        ListElement { lat: 54.209; lon: 16.192; name: “test2”; }
        ListElement { lat: 54.229; lon: 16.215; name: “test3”; }
    }
    Map
    {
        id: map
        plugin: mapPlugin
        anchors.fill: parent
        zoomLevel: 12
        activeMapType: supportedMapTypes[supportedMapTypes.length – 1]
        minimumZoomLevel: 5
        maximumZoomLevel: 17
        center: QtPositioning.coordinate(54.2,16.2)
        MapItemView
        {
            id: mapItemView
            model: dataModel
            delegate: Component
            {
                Marker
                {
                    id: m
                    visible: true
                    coordinate: QtPositioning.coordinate(lat, lon)
                    z: 90 – lat
                    marker_height: marker_base_height
                    marker_mouse.propagateComposedEvents: true
                    marker_mouse.onPressed:
                    {
                        // output name to console
                        console.log(name)
                    }
                }
            }
        }
        MapQuickItem
        {
            id: pos_marker
            anchorPoint.x: marker_base_height / 6
            anchorPoint.y: marker_base_height / 6
            z: 91
            sourceItem: Image
            {
                height: marker_base_height / 2.5
                width: marker_base_height / 2.5
                fillMode: Image.PreserveAspectFit
                source: “qrc:/qml/img/pos.svg”
                sourceSize.height: height
                sourceSize.width: width
            }
        }
    }
    PositionSource
    {
        id: pos_src
        updateInterval: 1000
        active: true
        onPositionChanged:
        {
            pos_marker.coordinate=position.coordinate
        }
    }
}

file Marker.qml:

import QtQuick 2.6
import QtLocation 5.11
import QtPositioning 5.11MapQuickItem
{
    property alias marker_mouse: marker_mouse_area
    property real marker_height: parent.width * 0.1156
    property string src
    id: marker
    anchorPoint.x: marker_height / 4
    anchorPoint.y: marker_height
    sourceItem: Image
    {
        id: marker_image
        height: marker_height
        width: marker_height / 2
        fillMode: Image.PreserveAspectFit
        source: “qrc:/markerGray.svg”
        sourceSize.height: height
        sourceSize.width: width
        MouseArea
        {
            id: marker_mouse_area
            anchors.fill: parent
        }
    }
}

Stay tuned as in the next post we will introduce more advanced use of maps in Qt!

There are no comments so far

Leave a Comment

Don't worry. We never use your email for spam

Don't worry. We never use your email for spam