Skip to content

03 Current Position

Overview

You will learn how to use a location service to acquire the current car position and strategies to display it on the map.

Base

Use Tutorial02 as a starting point for this project.

Prerequisites:

For better usability, we created two helper classes to handle GPS data and to set the current position on the map. To use them, please copy the following files from the "Tutorial03" folder to your "Tutorial" directory:

  • GPSManager.cpp
  • GPSManager.h
  • NavigationLoop.cpp
  • NavigationLoop.h

Replace the following files with the ones of Tutorial03:

  • MapWidget.cpp
  • MapWidget.h

Also, copy the folder "res" to your Tutorial directory. This folder holds resources like bitmaps and other files used in our tutorial. We use the Qt resource handling, so we add a file called main.qrc to our Tutorial directory with the following content:

<RCC>
  <qresource>
    <file>res/ccp.png</file>
  </qresource>
</RCC>

For now, we only have a bitmap for the custom rendering of the current car position (see Extended CCP drawing for details).

Add the files to the CMakeLists.txt file in your "Tutorial" folder:

set(Tutorial_RES
    ...
    main.qrc
)

set(Tutorial_SRCS
    ...
    NavigationLoop.cpp
    GPSManager.cpp
)

set(Tutorial_HEADER    
    ...
    NavigationLoop.h
    GPSManager.h
    ...
)

set(Tutorial_LIBS
    ...
    Qt5::Positioning
    ...
)

# Find the QtWidgets library
...
find_package(Qt5Positioning)

Add the GPSManager and the NavigationLoop files to the correspondent sections. Also, add the Qt5::Positioning to the LIBS section and to the find_package section to later use the Qt location service.

Call cmake ../ in the build directory or build the ALL_BUILD project of the solution to update it.
Also, always trigger a rebuild after copying to ensure that replaced files will be newly built.

The SDK offers two possibilities to get location data:

  • Using the system location service. In this case, we have to push the location data to the SDK
  • Reading out a GPS device directly (Windows only)

Because the second method works only under Windows (using COM-Ports), we choose the first one. See the documentation on how to acquire GPS-Data from a GPS device under Windows if needed.

Generally, the GPS handling in the SDK is done by the following:

  • Open the GPS device by calling SDK_OpenGPSDevice()
  • If using a location service, feed the location data from the location service to the SDK by calling SDK_SetLocation()
  • Get GPS information from the SDK by calling SDK_QueryGPSDevice()
  • If GPS is not needed anymore, close the GPS device by calling SDK_CloseGPSDevice().
  • If you want to change GPS settings, close the GPS device, set the new parameters and reopen it.

As we mentioned, the SDK can use location data directly. But it can also use a simulation file as a GPS source. It even can create such a file. An example for this is to simulate a calculated route (this functionality will be shown in "Tutorial06").

The GPSManager class is a wrapper around the SDK functions. To use it in our application, we have to

add the following code to our MainWindow class:

MainWindow.h:

...
#include "GPSManager.h"
#include <QGeoPositionInfo>

class MainWindow : public QMainWindow
{
    ...
protected:
    void startLocationUpdater();

protected slots: 
    ...
    void onPositionUpdated(QGeoPositionInfo);
    ...
private:
    ...
    GPSManager mGPSManager;

};

MainWindow.cpp:

...
#include <QtPositioning/QGeoPositionInfoSource>

MainWindow::MainWindow(QMainWindow * parent) : QMainWindow(parent)
{
    ...
    mGPSManager.openGPS(0);
    startLocationUpdater();
}

void MainWindow::startLocationUpdater()
{
    QGeoPositionInfoSource *source = QGeoPositionInfoSource::createSource("serialnmea", this);
    if (source)
    {
        source->setUpdateInterval(1000);
        source->setPreferredPositioningMethods(QGeoPositionInfoSource::AllPositioningMethods);
        connect(source, SIGNAL(positionUpdated(QGeoPositionInfo)),
            this, SLOT(onPositionUpdated(QGeoPositionInfo)));
        source->startUpdates();
    }
}

void MainWindow::onPositionUpdated(QGeoPositionInfo info)
{
    double lat = info.coordinate().latitude();
    double lon = info.coordinate().longitude();

    SDK_GPSData gpsData;
    SDK_InitGPSData(&gpsData);
    gpsData.GPSPosition.latitude = lat * SDK_GEOCONVERSION_FACTOR;
    gpsData.GPSPosition.longitude = lon * SDK_GEOCONVERSION_FACTOR;
    gpsData.altitude = 100;
    gpsData.course = info.attribute(QGeoPositionInfo::Direction);
    gpsData.fix = true;

    GPSManager::getInstance()->positionUpdated(gpsData);
}

Open the GPS device

In our MainWindow constructor, we first open the GPS device (we set the sim file name to 0 because we want to handle real location data).
Let's take a look at the GPSManager::openGPS() function:

void GPSManager::openGPS(const SDK_WCHAR_T * simFileName)
{   
    SDK_InitGPSConfig(&mGPSConfig);

    mGPSConfig.pDeviceName = L"PASSIVEMODE";
    mGPSConfig.shortTimeExtrapolator = SDK_TRUE;

    if (simFileName)
    {
        mGPSConfig.pSimFileName = simFileName;
        mGPSConfig.LogFileFormat = 3;
    }
    else
    {
        mGPSConfig.useLocationDirectly = SDK_TRUE;
    }

    SDK_ERROR rc = SDK_OpenGPSDevice(&mGPSConfig, 0);
}

To initialize the GPS functionality, we do the following:

  • Set device name to L"PASSIVEMODE". If you want to use a real GPS device under Windows, the device name would be the COM port of the device. Because we use a location service, we do not need this, so we set the name to L"PASSIVEMODE". The shortTimeExtrapolator is used for getting extrapolated GPS results within one second when not getting any GPS data. Set this to SDK_TRUE.
  • If we have a sim file name given, we set the pSimFilename field to the name and the LogFileFormat to 3 that means we use gzip files. In this case, the SDK uses the sim file instead of the pushed location data as GPS source.
  • Also, we set the useLocationDirectly to SDK_TRUE because we want to push locations to the SDK.

Call SDK_OpenGPSDevice(&mGPSConfig, 0) to open the device with the given configuration.

Requesting Location Updates

The MainWindow::startLocationUpdater() function opens the Qt location service, sets the update interval (we would like to check every 1000ms for new location updates) and connects the service to a slot called MainWindow::onPositionChanged() which will be called when a new location is available.

Notify the SDK

This would be enough to get the current position and show it on the MapWidget. But we also need to notify the SDK about the location updates so that we can use the position in a possible navigation for example. For this, we convert the QGeoPositionInfo in the MainWindow::onPositionChanged() method to the SDK struct SDK_GPSData. Take a look at the above code for the MainWindow::onPositionUpdated() method. We fill the SDK_GPSData struct with the coordinates (the longitude and latitude values from the location service are double values, but the SDK needs SDK_INT4 values, so we multiply the original values by SDK_GEOCONVERSION_FACTOR, which is defined in SDKDef.h) and we set the course, altitude and fix values. Then we call GPSManager::positionUpdated() to push the data to the SDK. This will push the data by calling SDK_SetLocation():

void GPSManager::positionUpdated(const SDK_GPSData& gpsData)
{
    SDK_ERROR rc = SDK_SetLocation(&gpsData);
}

Closing the GPS device

Closing is done in the GPSManager::closeGPS() method. It only calls SDK_CloseGPSDevice().

Getting the current position

Getting the current position can be achieved by calling GPSManager::getCurrentPosition():

SDK_ERROR GPSManager::getCurrentPosition(SDK_GPSData& gpsData)
{
    SDK_InitGPSData(&gpsData);
    SDK_ERROR rc = SDK_QueryGPSDevice(&gpsData, 0);
    if (rc == SDK_ERROR_NoPositionData)
    {
        gpsData.fix = 0;
        gpsData.GPSPositionMerc = mLastValidPos;
        return SDK_ERROR_Ok;
    }

    if (rc == SDK_ERROR_Ok)
    {
        mLastValidPos = gpsData.GPSPositionMerc;
    }

    return rc;
}

We get the current position data by calling SDK_QueryGPSDevice(). If the returned error code is SDK_NoPositionData, we will set the position to the last valid position we received. If there was no valid position before, we return the position of Karlsruhe/Germany ;-)

Changing GPS configuration

If you want to change the GPS configuration, like switching from real locations to a simulation file, you have to first close the GPS device, set the GPSConfig parameters new and reopen the device.

The NavigationLoop

To update our MapWidget with the current position, we implement one of the core mechanisms of this tutorial, the NavigationLoop. This is a runnable which is called frequently by a timer to update our navigation state. For this, we created a class NavigationLoop which is extended from QRunnable. The class get's the MapWidget from our MainWindow as a parameter, so that we can manipulate the map in the runnable. The runnable has one function called run() which will be called frequently by a timer that we start in our MainWindow.

Take a look at the NavigationLoop class:

void NavigationLoop::run()
{
    SDK_GPSData gps;
    SDK_ERROR rc = GPSManager::getInstance()->getCurrentPosition(gps);
    if (rc == SDK_ERROR_Ok || rc == SDK_ERROR_SamePosition)
    {       
        if (gps.fix > 0)
        {
            mMap->setCenter(gps.GPSPositionMerc);
            mMap->setMapMarker(gps.GPSPositionMerc, gps.course, false, SDK_mm_Directed);
            //mMap->setCCPPos(gps.GPSPositionMerc, gps.course); 
        }
        else
            mMap->setMapMarker(gps.GPSPositionMerc, gps.course, false, SDK_mm_None);

        mMap->update();
    }
}

Every time the timer runs out, the NavigationLoop::run() function gets called. It retrieves the current position from the GPSManager and then sets the map center to the current GPS position. To avoid setting the map to a bogus position, we check if an error was returned and if we have a valid fix. If the data is valid, we set the center of the map to the current position.

To get this to work in the tutorial,

add the following to the MainWindow class:

MainWindow.h:

#pragma once
...
#include "NavigationLoop.h"

class MainWindow : public QMainWindow
{
protected slots:     
    void updateTimer();
    ...
private:
    ...
    NavigationLoop * mNavigationLoop;
    QTimer * mTimer;
};

Include the QTimer header to the includes of the MainWindow.cpp:

MainWindow.cpp:

#include <QTimer>

The NavigationLoop will be started in the constructor of our MainWindow, so add the following to it:

    ...
    mNavigationLoop = new NavigationLoop(ui.map);
    mTimer = new QTimer(this);
    connect(mTimer, SIGNAL(timeout()), this, SLOT(updateTimer()));
    mTimer->start(200);

We create a NavigationLoop and a new timer, set the timer interval to 200ms and connect the timeout of this timer to the updateTimer() function.
Add the updateTimer() function to the MainWindow class:

void MainWindow::updateTimer()
{
    mNavigationLoop->run();
}

Now we have a running timer.

When destroying the MainWindow, we stop the timer and delete it. Also, the NavigationLoop will be deleted. The destructor should now look like this:

MainWindow:: ~MainWindow()
{
    if (mTimer)
    {
        mTimer->stop();
        delete mTimer;
    }

    if(mNavigationLoop)
        delete mNavigationLoop;

    mGPSManager.closeGPS();
}

Drawing a CCP

We added a new function called setMapMarker() to our MapWidget class to draw the current car position (CCP) on the map:

void MapWidget::setMapMarker(const SDK_Position& pos, SDK_INT4 course, bool northenAdjusted, SDK_INT4 mapMarkerStyle)
{
    SDK_ERROR rc = SDK_SetMapMarker(mMapId, pos.x,
        pos.y, course,
        northenAdjusted, mapMarkerStyle);
}

MapWidget::setMapMarker() calls SDK_SetMapMarker() which displays a marker on the map. The style of the marker can be one of these:

MapMarkerStyles | ---------------------------------------------------------- | -------------------------------------------------------------------- round map marker | directed map marker SDK_mm_Round | SDK_mm_Directed

To remove the marker, use SDK_mm_None as style.

In our NavigationLoop::run() method, we set the MapMarker to the mercator position, the orientation (course) and the desired style. If no fix is available, we remove the map marker.

!!! note: If you do not have a gps receiver installed, set gpsData.fix == 1 in GPSManager::getCurrentPosition() to show the marker for testing purposes.

Extended CCP drawing

You can use SDK_TransformCoordinates() to convert a mercator position to a pixel coordinate on the MapWidget and then draw a custom bitmap onto the canvas of your view at the given pixel position (rotated by the course).

In the MapWidget class, we added the following code:

MapWidget.cpp:

void MapWidget::renderCustomCCP(QPainter * painter)
{   
    QTransform rotating;
    rotating.translate(mCCP.width() / 2.0, mCCP.height() / 2.0);
    rotating.rotate(getOrientation() - mCCPCourse + 90);
    rotating.translate(-mCCP.width() / 2, -mCCP.height() / 2);
    QPixmap newCCP = mCCP.transformed(rotating, Qt::SmoothTransformation);
    painter->drawPixmap(mCCPPos.x-mCCP.width()/2.0, mCCPPos.y- mCCP.height()/2.0, newCCP);
}

void MapWidget::setCCPPos(const SDK_Position& pos, SDK_INT4 course)
{
    SDK_INT4 destX;
    SDK_INT4 destY;
    SDK_TransformCoordinates(mMapId, SDK_cf_MERCATOR, pos.x, pos.y, SDK_cf_PIXEL, &destX, &destY);
    mCCPPos.x = destX;
    mCCPPos.y = destY;
    mCCPCourse = course;
}

For details on how to rotate a given bitmap, please see the documentation of your used API.
Have in mind, that the coordinate system of the SDK is counter clockwise, head east. To transform this to a standard system with heading north, clockwise, we substract the current course from the current orientation of the map and add 90 degrees:

rotating.rotate(getOrientation() - mCCPCourse + 90);

The setCCPPos() function will be called in the NavigationLoop instead of the setMapMarker() function. In the MapWidget::paintEvent() function, we add the following to render the custom ccp:

...
renderCustomCCP(&painter);