📜 ⬆️ ⬇️

Development of cartographic mobile applications in C ++ / Qt, using Qt Mobility

This post participates in the competition " Smart phones for smart posts "


image In the previous article, we learned how to write a cartographic application in QML, and today we will consider developing an application based on the Qt Graphics View architecture of graphical representations, using the QtLocation API module. The article can be divided into two parts: theoretical and practical. The theoretical part deals with the architecture of the Graphics View and the main points of using the QtLocation module. In the practical part, I will not describe creating a project from scratch, but will provide the code of the most interesting functional from the project, such as inertial map movement, animated transition to a given coordinate, GPS positioning , elements for scaling and displaying textual information.
If you collected additional plugins from the previous article, you can use them in this project.


Table of contents

  1. Graphics View Architecture
  2. Introduction to graphic elements
  3. QtLocation module
  4. Maps and Services
  5. Getting GPS coordinates
  6. Project Parsing
  7. Tips & Tricks
  8. Conclusion
  9. Links



Graphics View Architecture

The graphics view architecture is similar to the model / view architecture in the sense that it has a class for storing QGraphicsScene data and a class for visualizing this QGraphicsView data. The same scene can be visualized with different views, if necessary. The graphic scene contains elements — objects of classes derived from the abstract QGraphicsItem class.
From the very beginning, a lot of effort was put into the development of the architecture of graphical representations aimed at improving performance and expanding capabilities. Scenes can be scaled, rotated and printed, and to display them, use both the Qt engine and the OpenGL library. The architecture also supports animation and drag and drop. Graphic scenes are suitable for presentation from a few to tens of thousands of elements, an example of 40,000 chips can serve as a good illustration. You can also place widgets that are derived from QWidget on the scene. To do this, you must transfer the widget to the QGraphicsProxyWidget class constructor and place the proxy widget on the scene. The proxy widget (or the QWidget objects themselves) is slow, but whether this slowdown is noticeable depends on the application (for more details, see QtLabs [En]).
')

Introduction to graphic elements

The QGraphicsItem class is basic for all graphic elements. You cannot create instances of it, since it has two purely virtual methods: boundingRect () and paint () . The paint () method corresponds to the QWidget :: paintEvent () method, its implementation should draw an element. The boundingRect () method informs the infrastructure about the bounding rectangle of an element — it is used to detect collisions and not to redraw an element that is not visible. This class provides support for mouse and keyboard events, drag & drop, grouping items. Event processing works as follows, the view receives mouse and keyboard events, then it translates them into events for the scene, changing the coordinates in accordance with the coordinates of the scene, and then the event is passed to the desired element.


QtLocation module



The QtLocation module provides the developer with a convenient interface for accessing positional information. The API allows you to abstract from a source of information, which can be a list of satellites or data from other sources.
QtLocation , comes with a set of classes related to various aspects of navigation. It includes hardware-related classes, such as QGeoPositionInfoSource , which provides user position information using the global positioning system (GPS) or other location sources, and QGeoSatelliteInfoSource , which is used to obtain satellite positioning information.
The module also includes a number of classes with the main purpose, which is the description of the location information, for example, QGeoCoordinate , which contains latitude, longitude and height above sea level. QtLocation also provides a way to represent geographic areas, either in an abstract form, like QGeoBoundingArea or more precisely, as in QGeoBoundingBox and QGeoBoundingCircle .


Maps and Services

map-shapes
Before we can start working with maps, scenes and views, first of all we need to get access to a source of geographic data. A common way to do this is to use a QGeoServiceProvider . To find out which services are available, you can use QGeoServiceProvider :: availableServiceProviders () , which returns an empty QStringList if there are no available services, so the developer should check this feature:
QStringList services = QGeoServiceProvider::availableServiceProviders(); if (services.isEmpty()) { //      } 

Once the service provider has been received, we can get a QGeoMappingManager , which will allow us to get an image of the card.
  QGeoMappingManager *manager = service.mappingManager(); 
In order to query the manager to search for objects on the map or route, you need to call searchManager () or routingManager () .
To draw a map for a user, QtLocation provides us with a QGraphicsGeoMap class to display data from QGeoMappingManager . All we have to do is create the view and scene in the usual way, and add a map to the scene:
  QGraphicsScene scene; QGraphicsView view; view.setScene(&scene); QGraphicsGeoMap *geoMap = new QGraphicsGeoMap(manager); scene.addItem(geoMap); 



Getting GPS coordinates

image
To get GPS coordinates, you need to create a QGeoPositionInfoSource by calling QGeoPositionInfoSource :: createDefaultSource () , which gives access to various QGeoPositionInfo information . Clients requiring location data can connect to the positionUpdated () signal so that QGeoPositionInfoSource to start sending this signal must be called startUpdates () or requestUpdate () .
An example of a client that receives location data:
  class MyClass : public QObject { Q_OBJECT public: MyClass(QObject *parent = 0) : QObject(parent) { QGeoPositionInfoSource *source = QGeoPositionInfoSource::createDefaultSource(this); if (source) { connect(source, SIGNAL(positionUpdated(QGeoPositionInfo)), this, SLOT(positionUpdated(QGeoPositionInfo))); source->startUpdates(); } } private slots: void positionUpdated(const QGeoPositionInfo &info) { qDebug() << "Position updated:" << info; } }; 

If regular position updates are required, setUpdateInterval () can be used to determine how often these updates should occur. If the interval is not specified, updates come when they are available. For example, if the client application requires updating every 30 seconds:
  // Emit updates every 30 seconds if available QGeoPositionInfoSource *source = QGeoPositionInfoSource::createDefaultSource(someParent); if (source) source->setUpdateInterval(30000); 



Animated transition to the specified coordinate

So, we got a general idea of ​​how everything works, now you can go to the code. To create a smooth movement of the map to a given coordinate, we need to expand the functionality provided by the QGraphicsGeoMap class:
 class GeoMap : public QGraphicsGeoMap { Q_OBJECT Q_PROPERTY(double centerLatitude READ centerLatitude WRITE setCenterLatitude) Q_PROPERTY(double centerLongitude READ centerLongitude WRITE setCenterLongitude) public: GeoMap(QGeoMappingManager *manager, QGraphicsItem *parent = 0); void animatedPanTo(const QGeoCoordinate& center); double centerLatitude() const { return center().latitude(); } void setCenterLatitude(double lat); double centerLongitude() const { return center().longitude(); } void setCenterLongitude(double lon); //... 

and implementation:
 void GeoMap::animatedPanTo(const QtMobility::QGeoCoordinate ¢er) { QGeoCoordinate curStart(this->center()); if (curStart == center) return; //        setStatusBarText(QString("Panning to %1").arg(center.toString(QGeoCoordinate::Degrees))); QPropertyAnimation *latAnim = new QPropertyAnimation(this, "centerLatitude"); latAnim->setEndValue(center.latitude()); latAnim->setDuration(300); QPropertyAnimation *lonAnim = new QPropertyAnimation(this, "centerLongitude"); lonAnim->setEndValue(center.longitude()); lonAnim->setDuration(300); QParallelAnimationGroup *group = new QParallelAnimationGroup; group->addAnimation(latAnim); group->addAnimation(lonAnim); group->start(QAbstractAnimation::DeleteWhenStopped); } void GeoMap::setCenterLatitude(double lat) { QGeoCoordinate c = center(); c.setLatitude(lat); setCenter( c); } void GeoMap::setCenterLongitude(double lon) { QGeoCoordinate c = center(); c.setLongitude(lon); setCenter( c); } 
Now, when you call animatedPanTo (), the map will smoothly move to the specified coordinate and at the right angle, i.e. if the new coordinate is higher relative to the current center, then the map will move up and so on. Since QPropertyAnimation does not work with QGeoCoordinate by default , I added two properties to the map that the animation can work with ( list of supported types). Of course, you could register your interpolator and register QGeoCoordinate for QVariant , but with the properties it seems to me much clearer and more elegant.


Pinch gesture

image
Qt includes a gesture programming framework that generates them from a series of events, regardless of the input method. The gesture can be a mouse movement, a touch on the touch screen, or a series of events from other sources. In Qt, gesture processing is represented by the following classes: QPanGesture , QPinchGesture, and QSwipeGesture . "Chips" is used to increase or decrease the image, in our case it will change the map scale. To implement it in a class, you need to enable processing of this gesture by calling the grabGesture method (Qt :: PinchGesture) and process it in the sceneEvent () of our map:
 bool GeoMap::sceneEvent(QEvent *event) { switch (event->type()) { case QEvent::Gesture: { if (QGestureEvent *gesture = static_cast<QGestureEvent *>(event)) { if (QPinchGesture *pinch = static_cast<QPinchGesture *>(gesture->gesture(Qt::PinchGesture))) { qreal scale = qLn(pinch->scaleFactor())/qLn(2); qreal zoom = 0; zoom = scale > 0 ? 1 : -1; setZoomLevel(zoomLevel() + zoom); return true; } } } default: break; } return QGraphicsGeoMap::sceneEvent(event); } 

Qt allows you to handle touch events on a par with gestures . To accept them for a widget, you need to set the Qt :: WA_AcceptTouchEvents attribute , and for graphic elements, call acceptTouchEvents (true) .


Kinetic scroll


Kinetic Scroll is an inertial movement of the map, not knowing how to more accurately convey this functionality in words, I put a video, it’s better to see once than to read a hundred times. We are implementing this functionality. So, the rest of our header file:
 protected: void mousePressEvent(QGraphicsSceneMouseEvent *event); void mouseMoveEvent(QGraphicsSceneMouseEvent *event); void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event); void wheelEvent(QGraphicsSceneWheelEvent *event); void keyPressEvent(QKeyEvent * event); void keyReleaseEvent(QKeyEvent * event); bool sceneEvent(QEvent *event); private slots: void kineticTimerEvent(); private: void panFloatWrapper(const QPointF & delta); void applyPan(const Qt::KeyboardModifiers & modifiers); void setStatusBarText(const QString &text); private: bool panActive; bool panDecellerate; // Fractional pan, used by panFloatWrapper QPointF remainingPan; // current kinetic panning speed, in pixel/msec QPointF kineticPanSpeed; QPoint panDir; QTimer kineticTimer; QTime lastMoveTime; // An entry in the mouse history. first=speed, second=time typedef QPair<QPointF, QTime> MouseHistoryEntry; // A history of the last (currently 5) mouse move events is stored in order to smooth out // movement detection for kinetic panning QList<MouseHistoryEntry> mouseHistory; StatusBarItem *m_statusBar; }; 

The most interesting, of course, in the implementation, first consider the settings for motion control:
 //////////////////////////////////////////////////////////////////////////////// // TODO: Some of these could be exposed in a GUI and should probably be put elsewhere in that case // (and made non-const) // Kinect annimation properties static const bool enableKineticPanning = true; // time until kinetic panning speed slows down to 50%, in msec static const qreal kineticPanningHalflife = 160.0; // keyboard panning speed without modifiers, in pixels/msec static const qreal panSpeedNormal = 0.3; // keyboard panning speed with shift, in pixels/msec static const qreal panSpeedFast = 1.0; // minimum panning speed, in pixels/msec static const qreal kineticPanSpeedThreshold = 0.005; // temporal resolution. Smaller values take more CPU but improve visual quality static const int kineticPanningResolution = 20; // maximum time between last mouse move and mouse release for kinetic panning to kick in static const int holdTimeThreshold = 100; //////////////////////////////////////////////////////////////////////////////// 
I think nothing needs to be explained, everything is clear from the comments.
The rest of the implementation:
 void GeoMap::mousePressEvent(QGraphicsSceneMouseEvent *event) { setFocus(); if (event->button() == Qt::LeftButton) { panActive = true; // When pressing, stop the timer and stop all current kinetic panning kineticTimer.stop(); kineticPanSpeed = QPointF(); lastMoveTime = QTime::currentTime(); } event->accept(); } void GeoMap::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (panActive) { setCursor(Qt::ClosedHandCursor); // Calculate time delta QTime currentTime = QTime::currentTime(); int deltaTime = lastMoveTime.msecsTo(currentTime); lastMoveTime = currentTime; // Calculate position delta QPointF delta = event->lastPos() - event->pos(); // Calculate and set speed if (deltaTime > 0) { kineticPanSpeed = delta / deltaTime; mouseHistory.push_back(MouseHistoryEntry(kineticPanSpeed, currentTime)); mouseHistory.pop_front(); } // Pan map panFloatWrapper(delta); } event->accept(); } void GeoMap::mouseReleaseEvent(QGraphicsSceneMouseEvent * event) { if (event->button() == Qt::LeftButton && panActive) { panActive = false; setCursor(Qt::OpenHandCursor); if (!enableKineticPanning || lastMoveTime.msecsTo(QTime::currentTime()) > holdTimeThreshold) { return; } kineticPanSpeed = QPointF(); int entries_considered = 0; QTime currentTime = QTime::currentTime(); foreach(MouseHistoryEntry entry, mouseHistory) { // first=speed, second=time int deltaTime = entry.second.msecsTo(currentTime); if (deltaTime < holdTimeThreshold) { kineticPanSpeed += entry.first; entries_considered++; } } if (entries_considered > 0) kineticPanSpeed /= entries_considered; lastMoveTime = currentTime; // When releasing the mouse button/finger while moving, // start the kinetic panning timer kineticTimer.start(); panDecellerate = true; } event->accept(); } void GeoMap::mouseDoubleClickEvent(QGraphicsSceneMouseEvent * event) { setFocus(); animatedPanTo(screenPositionToCoordinate(event->pos())); event->accept(); } // ... void GeoMap::kineticTimerEvent() { QTime currentTime = QTime::currentTime(); int deltaTime = lastMoveTime.msecsTo(currentTime); lastMoveTime = currentTime; if (panDecellerate) kineticPanSpeed *= qPow(qreal(0.5), qreal(deltaTime / kineticPanningHalflife)); QPointF scaledSpeed = kineticPanSpeed * deltaTime; if (kineticPanSpeed.manhattanLength() < kineticPanSpeedThreshold) { // Kinetic panning is almost halted -> stop it. kineticTimer.stop(); return; } panFloatWrapper(scaledSpeed); } // Wraps the pan(int, int) method to achieve floating point accuracy, // which is needed to scroll smoothly. void GeoMap::panFloatWrapper(const QPointF & delta) { // Add to previously stored panning distance remainingPan += delta; // Convert to integers QPoint move = remainingPan.toPoint(); // Commit mouse movement pan(move.x(), move.y()); // Store committed mouse movement remainingPan -= move; } 
I dropped the implementation of the keyboard processing, you can get acquainted with it by downloading the entire project . The implementation of the inertial movement is taken slightly less than completely from this example, thanks to the developers for the excellent documentation and excellent examples.


Item for status bar


Now let's consider an element for displaying various textual information in the status bar.
When displaying text, an element first appears smoothly, is delayed for a specified time and smoothly disappears. Let's start with the announcement:
 // An animated status bar item that appears at the bottom // of the map class StatusBarItem : public QObject, public QGraphicsRectItem { Q_OBJECT Q_PROPERTY(int offset READ offset WRITE setOffset) public: explicit 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: int m_offset; QGraphicsSimpleTextItem *m_textItem; }; 
First, the item itself is a rectangle that contains the QGraphicsSimpleTextItem and simply controls it. Let's look at the implementation of this class:
 StatusBarItem::StatusBarItem() { m_offset = 0; setPen(QPen(QBrush(), 0)); setBrush(QBrush(QColor(0,0,0,120))); m_textItem = new QGraphicsSimpleTextItem(this); m_textItem->setBrush(QBrush(Qt::white)); setText(""); } StatusBarItem::~StatusBarItem() { } void StatusBarItem::setText(QString text) { m_textItem->setText(text); QRectF rect = m_textItem->boundingRect(); QPointF delta = this->rect().center() - rect.center(); m_textItem->setPos(delta.x(), delta.y()); } int StatusBarItem::offset() const { return m_offset; } void StatusBarItem::setRect(qreal x, qreal y, qreal w, qreal h) { QGraphicsRectItem::setRect(x, y + m_offset, w, h); QFont f; f.setFixedPitch(true); f.setPixelSize(h/1.1); m_textItem->setFont(f); setText(m_textItem->text()); } void StatusBarItem::setOffset(int offset) { this->setY(this->y() - m_offset + offset); m_offset = offset; } void StatusBarItem::showText(QString text, quint32 timeout) { setText(text); show(); QTimer::singleShot(timeout, this, SLOT(hide())); } 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(m_offset); anim->setEndValue(0); anim->setDuration(500); anim->start(QAbstractAnimation::DeleteWhenStopped); } 
The animation controls the position of the element and, depending on the method, either pulls the element up or down. Qt 's motto is “Code Less. Create More. " Works in full.


Element to scale


Here I wanted to consider the implementation of buttons for scaling, but at the last moment I changed my mind (seeing the volume of the resulting article) and only describe the main principle of its work. So, this is a regular QGraphicsRectItem , which contains two text elements for displaying + and - , and QGraphicsLineItem for visual separation. The element reacts to user actions and by pressing it either increases or decreases the scale of the map:
  //       //  ,      0      m_geoMap->setZoomLevel(m_geoMap->zoomLevel() + zoomLevel(event->pos())); 

In principle, the element could be implemented by a single inheritance from QGraphicsItem and simply draw the information we need, but I thought that working with various graphical elements was more intuitive. Who wants, can change this class, after it will consume a little less memory.


Tips & Tricks

In order for QGraphicsView to use OpenGL for drawing, you just need to install the QGLWidget as follows:
 #ifndef QT_NO_OPENGL setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); viewport()->setMouseTracking(true); setRenderHint(QPainter::HighQualityAntialiasing, true); #endif 
And set the flag for QGraphicsView , since QGLWidget does not support partial screen updates:
  graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); 
Also, if you have a lot of animated or moving elements in the scene, then for better performance, you can disable indexing of elements:
  graphicsScene->setItemIndexMethod(QGraphicsScene::NoIndex); 
Indexing is great for static scene elements, it enhances the search for scene elements in functions such as items () and itemAt () . You may also consider optimizing flags for QGraphicsView .


Conclusion

The article turned out to be large, and it seems to me that it can be expanded indefinitely. We did not consider the API for finding objects, laying a route and working with the Landmark API (if anyone is interested, see Example 1 and 2 ). In the article we got acquainted with the Graphics View Framework , the main points when using the QtLocation API, I hope, we learned how to work with the Animation Framework and looked at the implementation of inertial movement of the map, various elements for managing the map and displaying textual information. In general, it turned out a good component for displaying the map, which can be expanded to a full-fledged navigator. To try the application in action, you need to install the Qt SDK and build the project for the Qt emulator. Finally, some useful links.


Links

  1. Qt Videos
  2. Scene Graph: A Different Approach to Graphics in Qt
  3. Performance: Do Graphics the Right Way
  4. Qt GraphicsView in Depth
  5. Training Materials - Graphics View
  6. All project on gitorius


PS Comments on the article are welcome, it will be very useful for such an inexperienced author as me. Thank you in advance.

Source: https://habr.com/ru/post/134289/


All Articles