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 those 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 this 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 corresponding sections. To use the Qt location service later, add the Qt5::Positioning to the LIBS and find_package section

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 a GPS device directly (Windows only)

Because the second method works for Windows systems (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 works like this:

  • 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. You even can create a simulation file with th SDK. 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 the device name to L"PASSIVEMODE". If you want to use a real GPS device on a Windows system, the device name has to be 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 there is temporarily no GPS data available. 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, meaning 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.

Notifying the SDK

Up to here we 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 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. Longitude and Latitude from the location service are of type double, the SDK needs SDK_INT4 values, so we multiply the values by SDK_GEOCONVERSION_FACTOR, which is defined in SDKDef.h. Further we set the course, altitude and fix values. Then we call GPSManager::positionUpdated() to push the data to the SDK. This will forward 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 first have to 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 gets the MapWidget from our MainWindow as a parameter, so we can control the map in the runnable. The runnable has a function called run(), which will be called frequently by a timer started 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. We only set the position if the data is valid.

Pleas add the following to the 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:

#include <QTimer>

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

    ...
    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 add 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. This allows you to draw a custom bitmap onto the canvas of your view at the given pixel position, rotated by the course value.

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 bitmap, please see the documentation of your used API.
Please have in mind that the coordinate system of the SDK is counter clockwise, heading east. To transform this to a coordinate system with clockwise heading north, 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);