MeeGo 1.2 Harmattan Developer Documentation Develop for the Nokia N9

Part 3 - Listening to satellites

Another useful part of the Location API is the ability to receive updates of the user's present geographic location from methods such as GPS or network positioning. We're going to add support to our MapsDemo for using these methods to update the "my location" marker we've already added in parts 1 and 2 of this tutorial.

But first we need an attractive way to present status messages to the user while they are busy looking at the map. We're going to do this using an animated translucent rectangle at the bottom of the display.

Animated status bar

First, set up the map to resize automatically:

 class MapsWidget : public QWidget
 {
     ...
 private:
     void resizeEvent(QResizeEvent *event);
     void showEvent(QShowEvent *event);
 };

 void MapsWidget::resizeEvent(QResizeEvent *event)
 {
     if (d->view && d->map) {
         d->view->resize(size());
         d->map->resize(size());
         d->view->centerOn(d->map);
     }
 }

 void MapsWidget::showEvent(QShowEvent *event)
 {
     if (d->view && d->map) {
         d->view->resize(size());
         d->map->resize(size());
         d->view->centerOn(d->map);
     }
 }

And now we add our new StatusBarItem class:

 class StatusBarItemPrivate;
 class StatusBarItem : public QObject, public QGraphicsRectItem
 {
     Q_OBJECT
     Q_PROPERTY(int offset READ offset WRITE setOffset)

 public:
     StatusBarItem();
     ~StatusBarItem();

     int offset() const;
     void setRect(qreal x, qreal y, qreal w, qreal h);

 public slots:
     void setText(QString text);

     void showText(QString text, quint32 timeout=3000);
     void show();
     void hide();

     void setOffset(int offset);

 private:
     StatusBarItemPrivate *d;
 };

Note that the order of base classes here is very important: QObject and then QGraphicsRectItem. Re-ordering the base classes will cause the code not to compile, as QGraphicsRectItem does not have a meta-object (for more details consult the documentation in Qt).

The offset property here is added so that when we come to animating our status bar, we can handle the case where the bar is sliding in and the window is being resized simultaneously. If we simply animated the y property of the GraphicsItem instead we would have difficulty handling this case.

Now add a pointer to one of these in MapsWidgetPrivate (and matching accessor methods):

 class MapsWidgetPrivate
 {
 public:
     ...
     StatusBarItem *statusBarItem;
 };

And we're ready for the implementation. The constructor is not terribly exciting, but sets the defaults for everything:

 class StatusBarItemPrivate
 {
 public:
     int offset;
     QGraphicsSimpleTextItem *textItem;
 };

 StatusBarItem::StatusBarItem() :
     d(new StatusBarItemPrivate)
 {
     d->offset = 0;

     setPen(QPen(QBrush(), 0));
     setBrush(QBrush(QColor(0,0,0,120)));

     d->textItem = new QGraphicsSimpleTextItem(this);
     d->textItem->setBrush(QBrush(Qt::white));

     setText("");
 }

The setText function, however, is more interesting;

 void StatusBarItem::setText(QString text)
 {
     d->textItem->setText(text);
     QRectF rect = d->textItem->boundingRect();
     QPointF delta = this->rect().center() - rect.center();
     d->textItem->setPos(delta.x(), delta.y());
 }

This re-centers the textItem inside its parent (the StatusBarItem) every time the text changes.

Also, the setRect method is used to update the size and position of the status bar:

 void StatusBarItem::setRect(qreal x, qreal y, qreal w, qreal h)
 {
     QGraphicsRectItem::setRect(x, y + d->offset, w, h);
     setText(d->textItem->text());
 }

Here we see the use of the offset property for the first time. The idea is to call setRect to specify a rectangle that is below the bottom of the visible area in the QGraphicsView. Then offset is used to bump the status bar up into the visible area when needed.

Whenever we change the offset we should re-calculate our own y value using the rect and the offset together:

 void StatusBarItem::setOffset(int offset)
 {
     this->setY(this->y() - d->offset + offset);
     d->offset = offset;
 }

And now finally, the animations:

 void StatusBarItem::show()
 {
     QPropertyAnimation *anim = new QPropertyAnimation(this, "offset");
     anim->setStartValue(0);
     anim->setEndValue(-1 * rect().height());
     anim->setDuration(500);
     anim->start(QAbstractAnimation::DeleteWhenStopped);
 }

 void StatusBarItem::hide()
 {
     QPropertyAnimation *anim = new QPropertyAnimation(this, "offset");
     anim->setStartValue(d->offset);
     anim->setEndValue(0);
     anim->setDuration(500);
     anim->start(QAbstractAnimation::DeleteWhenStopped);
 }

You can see here that we simply use QPropertyAnimations on the offset property we just defined. This produces a nice linear slide in and out whenever show() or hide() are called.

Lastly, one convenience method:

 void StatusBarItem::showText(QString text, quint32 timeout)
 {
     setText(text);
     show();
     QTimer::singleShot(timeout, this, SLOT(hide()));
 }

This lets us more easily display a status message when we only want it to appear and disappear soon afterwards.

Then we have only to add this into our MapsWidget:

 void MapsWidget::initialize(QGeoMappingManager *manager)
 {
     QGraphicsScene *sc;
     ...
     d->statusBarItem = new StatusBarItem;
     sc->addItem(d->statusBarItem);
 }

 void MapsWidget::resizeEvent(QResizeEvent *event)
 {
     if (d->view && d->map) {
         ...
         d->statusBarItem->setRect(0, height(), width(), 20);
     }
 }

 // and similarly in MapsWidget::showEvent()

Getting GPS data

Now we move on to the focus of this section: GPS data and how to get it. The QGeoPositionInfoSource class gives a convenient interface to receive position updates. We're going to add one to our MainWindow:

 class MainWindow : public QMainWindow
 {
 private:
     QGeoPositionInfoSource *positionSource;

 private slots:
     // slot to receive updates
     void updateMyPosition(QGeoPositionInfo info);
 };

And in initialize() we'll set it up. We're just using whatever the default position source for the platform happens to be, at an update interval of 1000ms, which is plenty for a basic maps application. Once set up, we call the source's startUpdates() method to begin receiving position updates.

 void MainWindow::initialize()
 {
     ...
     if (positionSource)
         delete positionSource;

     positionSource = QGeoPositionInfoSource::createDefaultSource(this);

     if (!positionSource) {
         mapsWidget->statusBar()->showText("Could not open GPS", 5000);
         mapsWidget->setMyLocation(QGeoCoordinate(-27.5796, 153.1));
     } else {
         positionSource->setUpdateInterval(1000);
         connect(positionSource, SIGNAL(positionUpdated(QGeoPositionInfo)),
                 this, SLOT(updateMyPosition(QGeoPositionInfo)));
         positionSource->startUpdates();
         mapsWidget->statusBar()->showText("Opening GPS...");
     }
 }

Here we also make use of the StatusBarItem to display a message when we are able or unable to open the QGeoPositionInfoSource.

And then in the slot updateMyPosition, we use this to set the myLocation marker.

 void MainWindow::updateMyPosition(QGeoPositionInfo info)
 {
     if (mapsWidget) {
         mapsWidget->setMyLocation(info.coordinate());
     }
 }

So, running the code as is, we have a moving marker for "My Location" that follows our actual GPS or network-sourced location. If you start driving your car with this app running however, you'll quickly notice the fact that the viewport does not pan to follow you as you leave the map area.

We could simply add a call to setCenter() on the map object in the updateMyPosition slot, but in the interests of prettiness, we are going to make a nice smoothly animated transition instead.

Following and animated panning

First, add a new boolean member variable to MainWindow, called tracking, to keep track of whether the viewport is currently following the My Location marker:

 class MainWindow : public QMainWindow
 {
 private:
     bool tracking;
     ...
 };

Our intended design is that initially, the viewport will be in tracking mode. It will continue this way until the view is manually panned by the user, at which point tracking will stop. Then, if the user clicks the "My Location" menu option to re-center the map, we resume tracking once again.

So we will need a way to notify the MainWindow that the user has panned the view. Add a new signal mapPanned() to MapsWidget, and a corresponding signal panned() to GeoMap, as we did for clicked().

 class MapsWidget : public QWidget
 {
 signals:
     void mapPanned();
     ...
 };

 class GeoMap : public QGraphicsGeoMap
 {
 signals:
     void panned();
     ...
 };

 void MapsWidget::initialize(QGeoMappingManager *manager)
 {
     ...
     connect(geoMap, SIGNAL(panned()),
             this, SIGNAL(mapPanned()));
     ...
 }

And now we simply emit it when a user pan takes place:

 void GeoMap::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
 {
     if (panActive) {
         ...
         emit panned();
     }
     ...
 }

Back up in MainWindow, we create a slot disableTracking and hook up the new signal to it:

 class MainWindow : public QMainWindow
 {
     ...
 private slots:
     ...
     void disableTracking();
     ...
 };

 void MainWindow::initialize()
 {
     ...
     connect(mapsWidget, SIGNAL(mapPanned()),
             this, SLOT(disableTracking()));
     ...
 }

And finally in the slot itself we simply set the flag we created earlier:

 void MainWindow::disableTracking()
 {
     tracking = false;
 }

Next we want animated panning to be available. Add a new method on MapsWidget:

 class MapsWidget : public QWidget
 {
 public:
     ...
     void animatedPanTo(QGeoCoordinate center);
     ...
 };

To do animations in Qt, it's always easiest if we can make use of a QPropertyAnimation, and to do this you need a Q_PROPERTY to act upon. We'll use two animations in parallel, one moving latitude and one moving longitude, so we need two Q_PROPERTIES:

 class GeoMap : public QGraphicsGeoMap
 {
     Q_OBJECT

     Q_PROPERTY(double centerLatitude READ centerLatitude WRITE setCenterLatitude)
     Q_PROPERTY(double centerLongitude READ centerLongitude WRITE setCenterLongitude)

 public:
     ...
     double centerLatitude() const;
     void setCenterLatitude(double lat);
     double centerLongitude() const;
     void setCenterLongitude(double lon);
     ...
 };

These functions simply adjust the corresponding value on center() and then call setCenter() with the new QGeoCoordinate.

Now we can implement our animatedPanTo() method:

 void MapsWidget::animatedPanTo(QGeoCoordinate center)
 {
     if (!d->map)
         return;

     QPropertyAnimation *latAnim = new QPropertyAnimation(d->map, "centerLatitude");
     latAnim->setEndValue(center.latitude());
     latAnim->setDuration(200);
     QPropertyAnimation *lonAnim = new QPropertyAnimation(d->map, "centerLongitude");
     lonAnim->setEndValue(center.longitude());
     lonAnim->setDuration(200);

     QParallelAnimationGroup *group = new QParallelAnimationGroup;
     group->addAnimation(latAnim);
     group->addAnimation(lonAnim);
     group->start(QAbstractAnimation::DeleteWhenStopped);
 }

To bring it all together, we make the last few changes in MainWindow:

 void MainWindow::goToMyLocation()
 {
     mapsWidget->animatedPanTo(markerManager->myLocation());
     tracking = true;
 }

 void MainWindow::updateMyPosition(QGeoPositionInfo info)
 {
     if (mapsWidget) {
         mapsWidget->setMyLocation(info.coordinate());
         if (tracking)
             mapsWidget->animatedPanTo(info.coordinate());
     }
 }

And now we have the simple location tracking functionality we set out to implement.