Qt And Google Maps

QML & Google Maps 


Introduction

Google Maps is the  standard for consumer maps.  For Qt developers this presents a dilemma as there are no C++ API's available for Google Maps.  Enter Qt's WebEngineView  which provides a Chromium based embedded web browser window for QML. WebEngineView allows one to seamlessly integrate a Google Map into a QML application.  With Google Maps comes Google's services such for directions and search and info services.  What follows is a description on how to integrate Google Maps into QML.

 Motivation

If you want to integrate maps into a Qt application there are many native solutions including using Qt's Location module and map plugin system. Available maplugins shipping with Qt are: Mapbox, Open Street Maps, Here, and ESRI.  One could argue that these plugins are superior as they are done in  C++  and all share a common Qt/QML API, and some allow caching of maps.  Qt map plugins also use less computer resources then the WebEngineView  approach used here to display a  Google map. But there are those applications, with reasons such as quality, features, or just cache value that demand Google Maps.

Requirements

Your going to need to register with Google and obtain a API Key to use Google Maps in your application.  For commercial users there is a cost associated with this key, but a development key maybe obtained for free.  If your developing an in-house tool you might able to get away with using just a developers key.

Also keep in mind that Qt is released under two licenses, commercial and GPL.  You should check with both Google and the Qt Company to determine what license to use and the restrictions and obligations associated with these licenses.

The Code

I will not cover the code in detail as the entire code for this demo app is is available at https://bitbucket.org/dboosalis/qmlandgooglemaps

Instead I will cover some of the basic concepts on using Google Maps inside a QML application.


To use Qt's webengine you need to add the module to our project file with the following line

QT += qml quick webengine

 This demo app has two controllers.  One which interfaces between the C++ and our QML and the other between our QML and our Google Maps web page.   The former controller is written as a class.  In our simple example we use this C++ class to save and restore map zoom settings. The latter controller is used to communicate between the QML and the Web browser.   This controller is based on Qt's  QML Webchannel,  and is used to communicate between your native QML layer and the Web rendered Google Map layer.  Qt also allows one to use a WebChannel to communicate from C++ to a webview directly, but in this demo we will focus on how to communicate with our embedded web browser  to/from our QML code. An outline of our communication flow is shown below:

 Communication


C++

To use a QML Webview the first thing we must do in our main.cpp is to initialize the WebEngine.   We then declare our C++ controller class and register it so the QML can make use of it, and finally we load our html page into the browser.  This html page is loaded as a resource and is  is described later.  Here is what the main.cpp file looks like:

 int main(int argc, char *argv[])

{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);
    QtWebEngine::initialize();

    QQmlApplicationEngine engine;
    QQmlContext *context = engine.rootContext();

    // Controller is our Qt interface to save and retrieve zoom settings of map
    Controller *m_controller = new Controller();
    m_controller->readSettings();
    context->setContextProperty("controller", m_controller);
           context>setContextProperty(QStringLiteral("googleMapsUrl"),
              QUrl("qrc:/html/googleMaps.html"));
    engine.load(QUrl(QLatin1String("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;
    return app.exec();
}

The Controller class shown here provides our communication  interface to the QML. In this example, our communication is limited to saving and restoring map zoom events.

class Controller : public QObject{
    Q_OBJECT

    Q_DISABLE_COPY(Controller)

    Q_PROPERTY(int zoomLevel READ zoomLevel WRITE setZoomLevel NOTIFY zoomLevel\
Changed)
public:
    Controller();

   ~Controller();
     int   zoomLevel();
    Q_INVOKABLE void setZoomLevel(int level);
    void readSettings();
signals:
    void zoomLevelChanged(int zoomValue);
private:
    qint32 m_zoomLevel;

};

The methods for saving and restoring our zoom attribute are shown below from the controller.cpp file


void Controller::setZoomLevel(int level)
{
    m_zoomLevel = level;
    QSettings  settings ("QtApp","GoogleMap");
    settings.setValue("zoomLevel",m_zoomLevel);
}
void Controller::readSettings()
{
    QSettings  settings ("QtApp","GoogleMap");
    m_zoomLevel = settings.value("zoomLevel",10).toInt();

}

QML

The mapController class is defined in our main.qml as follows:
 QtObject{
        id: mapController
        WebChannel.id: "mapController"
        property int zoomLevel:controller.zoomLevel
        signal searchForRestaurants(bool nearMe)
        signal callItem(variant searchResult)
        signal displayRouteToSearchItem(variant searchResult)
        function setZoomLevel(z) {
            // notify C++ to save new zoomLevel
            controller.setZoomLevel(z)
        }

        // put into our search model the results received from Google
        function processSearchResults(searchResults)
        {
            for(var i=0;i< searchResults.length; i++) {
                searchModel.append({"name": searchResults[i].name, "address":se\
archResults[i].vicinity, "distance": searchResults[i].distance.toString() + " m\
i", "searchResult": searchResults[i]});
            }

        }

Please see Qt's WebChannel documentation for a more complete description.  


In the same file we setup the WebEngineViewer with our html page and also register the above webchannel.



WebEngineView {
            id: mapView
            objectName: "webView"
            anchors.left: resultsDialog.right
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            url: googleMapsUrl
            webChannel: wc
}
  

The url value of 'googleMapsURL' was defined in our main.cpp with:
     context>setContextProperty(QStringLiteral("googleMapsUrl"),
     QUrl("qrc:/html/googleMaps.html"));

 HTML

Next we turn our attention to what gets rendered in our QMl Webview, this html page is part of our Qt project and is loaded as a Qt resource.

In our header we load the Qt provided qwebchannel.js file as follows
     

<script type="text/javascript" src="qwebchannel.js"></script>

 We then define our map object, and out global variables that will interact with Google Maps.


Our HTML page includes the following script to shake hands and register our service with Google's map service. Be sure to enter a valid Google Map key here else your application will not work :
<script async defer
            src="https://maps.googleapis.com/maps/api/js?key=<your key here>=3&callback=initMap&libraries=weather,places">
       </script>



The "initMap" callback function initiates what we want our map to look like, where we want to  center it and at what zoom level. The initMap function also creates are WebChanel, and initiates Google Maps Direction and Place services.


function initMap() {
           myPosition =  new google.maps.LatLng(37.38556, -121.99223)
           map = new google.maps.Map(document.getElementById('map'), {
center: myPosition,
streetViewControl: false,
draggable: true,
scrollwheel: true,
panControl: true,
zoom: 13,
            })

        createChannel()
        directionsDisplay = new google.maps.DirectionsRenderer()
        directionsService = new google.maps.DirectionsService()
        services = new google.maps.places.PlacesService(map)
        directionsDisplay.setMap(map)
        }

Inside the createChannel function we create a Web channel and also bind to our MapContoller.  This mapContoller will be our conduit between our browser  and our QML code.   The code below is more pseudo code so please refer to the actual code which also includes comments.

 function createChannel() {

          new QWebChannel(qt.webChannelTransport, function (channel) {

            mapController = channel.objects.mapController
            mapController.searchForRestaurants.connect(function(nearMe) {
                  services.nearbySearch({
                    location: myPosition,
                    radius: 2000, // meters
                    type: ['cafe']   // https://developers.google.com/places/supported_types
                  }, callback)
                function callback(results, status) {
                  if (status === google.maps.places.PlacesServiceStatus.OK) {
                  for(var i=0;i< results.length;i++ )
                  {
                      var d =  distance(myPosition,results[i].geometry.location)
                      results[i]["distance"] = Number(d*0.000621371).toFixed(2) // convert to miles
                  }
                    mapController.processSearchResults(results)
                  }
                }
            })
            mapController.displayRouteToSearchItem.connect(function(searchResult) {
                var request = {
                  origin: myPosition,
                  destination: {placeId:searchResult.place_id},
                  travelMode: 'DRIVING'
                }
                directionsService.route(request, function(result, status) {
                 if (status == 'OK') {
                    directionsDisplay.setDirections(result)
                  }
                })
            })
            mapController.callItem.connect(function(place) {
                services.getDetails({placeId: place.place_id}, function(place, status) {
                if (status === 'OK') {
                        mapController.phoneItem(place)
                    }
                })
           })
           // Retrieve last used zoom setting from Qt
           map.setZoom(mapController.zoomLevel)
           google.maps.event.addListener(map, 'zoom_changed', function() {
                var zoom =  map.getZoom()
                mapController.setZoomLevel(zoom) // see main.qml
           })

        })

       }

 Google Map Services

 From the UI's hamburger menu which is written in QML we have an one item in the pull down labeled "Coffee Search".  This QML signal associated with this menu item uses the webchannel to notify our browser app that we want to search for Coffee shops near us. 


MenuItem {
             id: coffeeSearch
             text: "Coffee ..."
             onTriggered: {
                   mapController.searchForRestaurants(true);
                   workArea.state = "search"
             }
}

The Webchannel receives this info and then using the Google Map Place services sends this search request to the Google Server. The response from the Place server is handled in a callback in our web pages JavaScript code which then uses the mapController object to send the info back to our QML code.


mapController.searchForRestaurants.connect(function(nearMe) {
                  services.nearbySearch({
                    location: myPosition,
                    radius: 2000, // meters
                    type: ['cafe']
                  }, callback)
                function callback(results, status) {
                  if (status === PlacesServiceStatus.OK) {
                  for(var i=0;i< results.length;i++
                  {
                      var d = distance(...)
                      results[i]["distance"] =Number(d*0.000621371).toFixed(2)
                      ...
                  }
                mapController.processSearchResults(results)               
      }
}


The function processSearchResults() located in main.qml takes the results and populates our QML model:

function processSearchResults(searchResults)
        {
            for(var i=0;i< searchResults.length; i++) {
                searchModel.append({"name": searchResults[i].name, "address":searchResults[i].vicinity, "distance": searchResults[i].distance.toString() + " mi", "searchResult": searchResults[i]});               
            }
        }


With the model populated with the results, a user can then select an item which will use Google Maps directions platform to show us on the map how to get there from our given location. Here is the code in our HTML page that makes the request for directions and draws the response on our map

 mapController.displayRouteToSearchItem.connect(function(searchResult) {
                var request = {
                  origin: myPosition,
                  destination: {placeId:searchResult.place_id},
                  travelMode: 'DRIVING'
                }
                directionsService.route(request, function(result, status) {
                  if (status == 'OK') {
                    directionsDisplay.setDirections(result)
                  }
                })
            })

Final Thoughts

While one can easily put Google Maps into a Qt/QML application one must be aware that there are trade-offs to using a more native approach such as Qt's own Mapbox plugins which I enumerate below:

Size and Performance.  

When you utilize Qt's Webview you are essentially loading a fully blown browser into your application which can cause large increases in memory  and CPU usage.  On a modern day desktop or laptop computer you will hardly notice it and results when running on a Raspberrypi 3 were better then expected.



Network Connectivity Required.

Google does not allow you to control the caching of the maps, so you will need a network connection to run your application.




Comments