1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2011-2012 Florian Eßer <f.esser@rwth-aachen.de>
4 // SPDX-FileCopyrightText: 2012 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
5 // SPDX-FileCopyrightText: 2013 Roman Karlstetter <roman.karlstetter@googlemail.com>
6 //
7 
8 #include "ElevationProfileFloatItem.h"
9 
10 #include "ElevationProfileContextMenu.h"
11 #include "ui_ElevationProfileConfigWidget.h"
12 
13 #include "MarbleModel.h"
14 #include "MarbleWidget.h"
15 #include "GeoDataPlacemark.h"
16 #include "GeoDataTreeModel.h"
17 #include "ViewportParams.h"
18 #include "MarbleColors.h"
19 #include "MarbleDirs.h"
20 #include "ElevationModel.h"
21 #include "MarbleGraphicsGridLayout.h"
22 #include "MarbleDebug.h"
23 #include "routing/RoutingManager.h"
24 #include "routing/RoutingModel.h"
25 
26 #include <QContextMenuEvent>
27 #include <QRect>
28 #include <QPainter>
29 #include <QPainterPath>
30 #include <QPushButton>
31 #include <QMenu>
32 #include <QMouseEvent>
33 
34 namespace Marble
35 {
36 
ElevationProfileFloatItem(const MarbleModel * marbleModel)37 ElevationProfileFloatItem::ElevationProfileFloatItem( const MarbleModel *marbleModel )
38         : AbstractFloatItem( marbleModel, QPointF( 220, 10.5 ), QSizeF( 0.0, 50.0 ) ),
39         m_activeDataSource(nullptr),
40         m_routeDataSource( marbleModel ? marbleModel->routingManager()->routingModel() : nullptr, marbleModel ? marbleModel->elevationModel() : nullptr, this ),
41         m_trackDataSource( marbleModel ? marbleModel->treeModel() : nullptr, this ),
42         m_configDialog( nullptr ),
43         ui_configWidget( nullptr ),
44         m_leftGraphMargin( 0 ),
45         m_eleGraphWidth( 0 ),
46         m_viewportWidth( 0 ),
47         m_shrinkFactorY( 1.2 ),
48         m_fontHeight( 10 ),
49         m_markerPlacemark( new GeoDataPlacemark ),
50         m_documentIndex( -1 ),
51         m_cursorPositionX( 0 ),
52         m_isInitialized( false ),
53         m_contextMenu( nullptr ),
54         m_marbleWidget( nullptr ),
55         m_firstVisiblePoint( 0 ),
56         m_lastVisiblePoint( 0 ),
57         m_zoomToViewport( false )
58 {
59     setVisible( false );
60     bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
61     if ( smallScreen ) {
62         setPosition( QPointF( 10.5, 10.5 ) );
63     }
64     bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution;
65     m_eleGraphHeight = highRes ? 100 : 50; /// TODO make configurable
66 
67     setPadding( 1 );
68 
69     m_markerDocument.setDocumentRole( UnknownDocument );
70     m_markerDocument.setName(QStringLiteral("Elevation Profile"));
71 
72     m_markerPlacemark->setName(QStringLiteral("Elevation Marker"));
73     m_markerPlacemark->setVisible( false );
74     m_markerDocument.append( m_markerPlacemark );
75 
76     m_contextMenu = new ElevationProfileContextMenu(this);
77     connect( &m_trackDataSource, SIGNAL(sourceCountChanged()), m_contextMenu, SLOT(updateContextMenuEntries()) );
78     connect( &m_routeDataSource, SIGNAL(sourceCountChanged()), m_contextMenu, SLOT(updateContextMenuEntries()) );
79 }
80 
~ElevationProfileFloatItem()81 ElevationProfileFloatItem::~ElevationProfileFloatItem()
82 {
83 }
84 
backendTypes() const85 QStringList ElevationProfileFloatItem::backendTypes() const
86 {
87     return QStringList(QStringLiteral("elevationprofile"));
88 }
89 
zValue() const90 qreal ElevationProfileFloatItem::zValue() const
91 {
92     return 3.0;
93 }
94 
name() const95 QString ElevationProfileFloatItem::name() const
96 {
97     return tr("Elevation Profile");
98 }
99 
guiString() const100 QString ElevationProfileFloatItem::guiString() const
101 {
102     return tr("&Elevation Profile");
103 }
104 
nameId() const105 QString ElevationProfileFloatItem::nameId() const
106 {
107     return QStringLiteral("elevationprofile");
108 }
109 
version() const110 QString ElevationProfileFloatItem::version() const
111 {
112     return QStringLiteral("1.2"); // TODO: increase to 1.3 ?
113 }
114 
description() const115 QString ElevationProfileFloatItem::description() const
116 {
117     return tr( "A float item that shows the elevation profile of the current route." );
118 }
119 
copyrightYears() const120 QString ElevationProfileFloatItem::copyrightYears() const
121 {
122     return QStringLiteral("2011, 2012, 2013");
123 }
124 
pluginAuthors() const125 QVector<PluginAuthor> ElevationProfileFloatItem::pluginAuthors() const
126 {
127     return QVector<PluginAuthor>()
128             << PluginAuthor(QStringLiteral("Florian Eßer"),QStringLiteral("f.esser@rwth-aachen.de"))
129             << PluginAuthor(QStringLiteral("Bernhard Beschow"), QStringLiteral("bbeschow@cs.tu-berlin.de"))
130             << PluginAuthor(QStringLiteral("Roman Karlstetter"), QStringLiteral("roman.karlstetter@googlemail.com"));
131 }
132 
icon() const133 QIcon ElevationProfileFloatItem::icon () const
134 {
135     return QIcon(QStringLiteral(":/icons/elevationprofile.png"));
136 }
137 
initialize()138 void ElevationProfileFloatItem::initialize ()
139 {
140     connect( marbleModel()->elevationModel(), SIGNAL(updateAvailable()), &m_routeDataSource, SLOT(requestUpdate()) );
141     connect( marbleModel()->routingManager()->routingModel(), SIGNAL(currentRouteChanged()), &m_routeDataSource, SLOT(requestUpdate()) );
142     connect( this, SIGNAL(dataUpdated()), SLOT(forceRepaint()) );
143     switchDataSource(&m_routeDataSource);
144 
145     m_fontHeight = QFontMetricsF( font() ).ascent() + 1;
146     m_leftGraphMargin = QFontMetricsF( font() ).width( "0000 m" ); /// TODO make this dynamic according to actual need
147 
148     m_isInitialized = true;
149 }
150 
isInitialized() const151 bool ElevationProfileFloatItem::isInitialized () const
152 {
153     return m_isInitialized;
154 }
155 
setProjection(const ViewportParams * viewport)156 void ElevationProfileFloatItem::setProjection( const ViewportParams *viewport )
157 {
158     if ( !( viewport->width() == m_viewportWidth && m_isInitialized ) ) {
159         bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution;
160         int const widthRatio = highRes ? 2 : 3;
161         setContentSize( QSizeF( viewport->width() / widthRatio,
162                                 m_eleGraphHeight + m_fontHeight * 2.5 ) );
163         m_eleGraphWidth = contentSize().width() - m_leftGraphMargin;
164         m_axisX.setLength( m_eleGraphWidth );
165         m_axisY.setLength( m_eleGraphHeight );
166         m_axisX.setTickCount( 3, m_eleGraphWidth / ( m_leftGraphMargin * 1.5 ) );
167         m_axisY.setTickCount( 2, m_eleGraphHeight / m_fontHeight );
168         m_viewportWidth = viewport->width();
169         bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
170         if ( !m_isInitialized && !smallScreen ) {
171             setPosition( QPointF( (viewport->width() - contentSize().width()) / 2 , 10.5 ) );
172         }
173     }
174 
175     update();
176 
177     AbstractFloatItem::setProjection( viewport );
178 }
179 
paintContent(QPainter * painter)180 void ElevationProfileFloatItem::paintContent( QPainter *painter )
181 {
182     // do not try to draw if not initialized
183     if(!isInitialized()) {
184         return;
185     }
186     painter->save();
187     painter->setRenderHint( QPainter::Antialiasing, true );
188     painter->setFont( font() );
189 
190     if ( ! ( m_activeDataSource->isDataAvailable() && m_eleData.size() > 0 ) ) {
191         painter->setPen( QColor( Qt::black ) );
192         QString text = tr( "Create a route or load a track from file to view its elevation profile." );
193         painter->drawText( contentRect().toRect(), Qt::TextWordWrap | Qt::AlignCenter, text );
194         painter->restore();
195         return;
196     }
197     if ( m_zoomToViewport && ( m_lastVisiblePoint - m_firstVisiblePoint < 5 ) ) {
198         painter->setPen( QColor( Qt::black ) );
199         QString text = tr( "Not enough points in the current viewport.\nTry to disable 'Zoom to viewport'." );
200         painter->drawText( contentRect().toRect(), Qt::TextWordWrap | Qt::AlignCenter, text );
201         painter->restore();
202         return;
203     }
204 
205     QString intervalStr;
206     int lastStringEnds;
207 
208     // draw viewport bounds
209     if ( ! m_zoomToViewport && ( m_firstVisiblePoint > 0 || m_lastVisiblePoint < m_eleData.size() - 1 ) ) {
210         QColor color( Qt::black );
211         color.setAlpha( 64 );
212         QRect rect;
213         rect.setLeft( m_leftGraphMargin + m_eleData.value( m_firstVisiblePoint ).x() * m_eleGraphWidth / m_axisX.range() );
214         rect.setTop( 0 );
215         rect.setWidth( ( m_eleData.value( m_lastVisiblePoint ).x() - m_eleData.value( m_firstVisiblePoint ).x() ) * m_eleGraphWidth / m_axisX.range() );
216         rect.setHeight( m_eleGraphHeight );
217         painter->fillRect( rect, color );
218     }
219 
220     // draw X and Y axis
221     painter->setPen( Oxygen::aluminumGray4 );
222     painter->drawLine( m_leftGraphMargin, m_eleGraphHeight, contentSize().width(), m_eleGraphHeight );
223     painter->drawLine( m_leftGraphMargin, m_eleGraphHeight, m_leftGraphMargin, 0 );
224 
225     // draw Y grid and labels
226     painter->setPen( QColor( Qt::black ) );
227     QPen dashedPen( Qt::DashLine );
228     dashedPen.setColor( Oxygen::aluminumGray4 );
229     QRect labelRect( 0, 0, m_leftGraphMargin - 1, m_fontHeight + 2 );
230     lastStringEnds = m_eleGraphHeight + m_fontHeight;
231 //     painter->drawText(m_leftGraphMargin + 1, m_fontHeight, QLatin1Char('[') + m_axisY.unit() + QLatin1Char(']'));
232     for ( const AxisTick &tick: m_axisY.ticks() ) {
233         const int posY = m_eleGraphHeight - tick.position;
234         painter->setPen( dashedPen );
235         painter->drawLine( m_leftGraphMargin, posY, contentSize().width(), posY );
236 
237         labelRect.moveCenter( QPoint( labelRect.center().x(), posY ) );
238         if ( labelRect.top() < 0 ) {
239             // don't cut off uppermost label
240             labelRect.moveTop( 0 );
241         }
242         if ( labelRect.bottom() >= lastStringEnds ) {
243             // Don't print overlapping labels
244             continue;
245         }
246         lastStringEnds = labelRect.top();
247         painter->setPen( QColor( Qt::black ) );
248         intervalStr.setNum( tick.value * m_axisY.scale() );
249         painter->drawText( labelRect, Qt::AlignRight, intervalStr );
250     }
251 
252     // draw X grid and labels
253     painter->setPen( QColor( Qt::black ) );
254     labelRect.moveTop( m_eleGraphHeight + 1 );
255     lastStringEnds = 0;
256     for ( const AxisTick &tick: m_axisX.ticks() ) {
257         const int posX = m_leftGraphMargin + tick.position;
258         painter->setPen( dashedPen );
259         painter->drawLine( posX, 0, posX, m_eleGraphHeight );
260 
261         intervalStr.setNum( tick.value * m_axisX.scale() );
262         if ( tick.position == m_axisX.ticks().last().position ) {
263             intervalStr += QLatin1Char(' ') + m_axisX.unit();
264         }
265         labelRect.setWidth( QFontMetricsF( font() ).width( intervalStr ) * 1.5 );
266         labelRect.moveCenter( QPoint( posX, labelRect.center().y() ) );
267         if ( labelRect.right() > m_leftGraphMargin + m_eleGraphWidth ) {
268             // don't cut off rightmost label
269             labelRect.moveRight( m_leftGraphMargin + m_eleGraphWidth );
270         }
271         if ( labelRect.left() <= lastStringEnds ) {
272             // Don't print overlapping labels
273             continue;
274         }
275         lastStringEnds = labelRect.right();
276         painter->setPen( QColor( Qt::black ) );
277         painter->drawText( labelRect, Qt::AlignCenter, intervalStr );
278     }
279 
280     // display elevation gain/loss data
281     painter->setPen( QColor( Qt::black ) );
282     intervalStr = tr( "Difference: %1 %2" )
283                    .arg( QString::number( m_gain - m_loss, 'f', 0 ) )
284                    .arg( m_axisY.unit() );
285     intervalStr += QString::fromUtf8( "  (↗ %1 %3  ↘ %2 %3)" )
286                    .arg( QString::number( m_gain, 'f', 0 ) )
287                    .arg( QString::number( m_loss, 'f', 0 ) )
288                    .arg( m_axisY.unit() );
289     painter->drawText( contentRect().toRect(), Qt::AlignBottom | Qt::AlignCenter, intervalStr );
290 
291     // draw elevation profile
292     painter->setPen( QColor( Qt::black ) );
293     bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution;
294     QPen pen = painter->pen();
295     pen.setWidth( highRes ? 2 : 1 );
296     painter->setPen( pen );
297 
298     QLinearGradient fillGradient( 0, 0, 0, m_eleGraphHeight );
299     QColor startColor = Oxygen::forestGreen4;
300     QColor endColor = Oxygen::hotOrange4;
301     startColor.setAlpha( 200 );
302     endColor.setAlpha( 32 );
303     fillGradient.setColorAt( 0.0, startColor );
304     fillGradient.setColorAt( 1.0, endColor );
305     QBrush brush = QBrush( fillGradient );
306     painter->setBrush( brush );
307 
308     QPoint oldPos;
309     oldPos.setX( m_leftGraphMargin );
310     oldPos.setY( ( m_axisY.minValue() - m_axisY.minValue() )
311                  * m_eleGraphHeight / ( m_axisY.range() / m_shrinkFactorY ) );
312     oldPos.setY( m_eleGraphHeight - oldPos.y() );
313     QPainterPath path;
314     path.moveTo( oldPos.x(), m_eleGraphHeight );
315     path.lineTo( oldPos.x(), oldPos.y() );
316 
317     const int start = m_zoomToViewport ? m_firstVisiblePoint : 0;
318     const int end = m_zoomToViewport ? m_lastVisiblePoint : m_eleData.size() - 1;
319     for ( int i = start; i <= end; ++i ) {
320         QPoint newPos;
321         if ( i == start ) {
322             // make sure the plot always starts at the y-axis
323             newPos.setX( 0 );
324         } else {
325             newPos.setX( ( m_eleData.value(i).x() - m_axisX.minValue() ) * m_eleGraphWidth / m_axisX.range() );
326         }
327         newPos.rx() += m_leftGraphMargin;
328         if ( newPos.x() != oldPos.x() || newPos.y() != oldPos.y()  ) {
329             newPos.setY( ( m_eleData.value(i).y() - m_axisY.minValue() )
330                          * m_eleGraphHeight / ( m_axisY.range() * m_shrinkFactorY ) );
331             newPos.setY( m_eleGraphHeight - newPos.y() );
332             path.lineTo( newPos.x(), newPos.y() );
333             oldPos = newPos;
334         }
335     }
336     path.lineTo( oldPos.x(), m_eleGraphHeight );
337     // fill
338     painter->setPen( QPen( Qt::NoPen ) );
339     painter->drawPath( path );
340     // contour
341     // "remove" the first and last path element first, they are only used to fill down to the bottom
342     painter->setBrush( QBrush( Qt::NoBrush ) );
343     path.setElementPositionAt( 0, path.elementAt( 1 ).x,  path.elementAt( 1 ).y );
344     path.setElementPositionAt( path.elementCount()-1,
345                                path.elementAt( path.elementCount()-2 ).x,
346                                path.elementAt( path.elementCount()-2 ).y );
347     painter->setPen( pen );
348     painter->drawPath( path );
349 
350     pen.setWidth( 1 );
351     painter->setPen( pen );
352 
353     // draw interactive cursor
354     const GeoDataCoordinates currentPoint = m_markerPlacemark->coordinate();
355     if ( currentPoint.isValid() ) {
356         painter->setPen( QColor( Qt::white ) );
357         painter->drawLine( m_leftGraphMargin + m_cursorPositionX, 0,
358                            m_leftGraphMargin + m_cursorPositionX, m_eleGraphHeight );
359         qreal xpos = m_axisX.minValue() + ( m_cursorPositionX / m_eleGraphWidth ) * m_axisX.range();
360         qreal ypos = m_eleGraphHeight - ( ( currentPoint.altitude() - m_axisY.minValue() ) / ( qMax<qreal>( 1.0, m_axisY.range() ) * m_shrinkFactorY ) ) * m_eleGraphHeight;
361 
362         painter->drawLine( m_leftGraphMargin + m_cursorPositionX - 5, ypos,
363                            m_leftGraphMargin + m_cursorPositionX + 5, ypos );
364         intervalStr.setNum( xpos * m_axisX.scale(), 'f', 2 );
365         intervalStr += QLatin1Char(' ') + m_axisX.unit();
366         int currentStringBegin = m_leftGraphMargin + m_cursorPositionX
367                              - QFontMetricsF( font() ).width( intervalStr ) / 2;
368         painter->drawText( currentStringBegin, contentSize().height() - 1.5 * m_fontHeight, intervalStr );
369 
370         intervalStr.setNum( currentPoint.altitude(), 'f', 1 );
371         intervalStr += QLatin1Char(' ') + m_axisY.unit();
372         if ( m_cursorPositionX + QFontMetricsF( font() ).width( intervalStr ) + m_leftGraphMargin
373                 < m_eleGraphWidth ) {
374             currentStringBegin = ( m_leftGraphMargin + m_cursorPositionX + 5 + 2 );
375         } else {
376             currentStringBegin = m_leftGraphMargin + m_cursorPositionX - 5
377                                  - QFontMetricsF( font() ).width( intervalStr ) * 1.5;
378         }
379         // Make sure the text still fits into the window
380         while ( ypos < m_fontHeight ) {
381             ypos++;
382         }
383         painter->drawText( currentStringBegin, ypos + m_fontHeight / 2, intervalStr );
384     }
385 
386     painter->restore();
387 }
388 
configDialog()389 QDialog *ElevationProfileFloatItem::configDialog() //FIXME TODO Make a config dialog? /// TODO what is this comment?
390 {
391     if ( !m_configDialog ) {
392         // Initializing configuration dialog
393         m_configDialog = new QDialog();
394         ui_configWidget = new Ui::ElevationProfileConfigWidget;
395         ui_configWidget->setupUi( m_configDialog );
396 
397         readSettings();
398 
399         connect( ui_configWidget->m_buttonBox, SIGNAL(accepted()), SLOT(writeSettings()) );
400         connect( ui_configWidget->m_buttonBox, SIGNAL(rejected()), SLOT(readSettings()) );
401         QPushButton *applyButton = ui_configWidget->m_buttonBox->button( QDialogButtonBox::Apply );
402         connect( applyButton, SIGNAL(clicked()), this, SLOT(writeSettings()) );
403     }
404     return m_configDialog;
405 }
406 
contextMenuEvent(QWidget * w,QContextMenuEvent * e)407 void ElevationProfileFloatItem::contextMenuEvent( QWidget *w, QContextMenuEvent *e )
408 {
409     Q_ASSERT( m_contextMenu );
410     m_contextMenu->getMenu()->exec( w->mapToGlobal( e->pos() ) );
411 }
412 
eventFilter(QObject * object,QEvent * e)413 bool ElevationProfileFloatItem::eventFilter( QObject *object, QEvent *e )
414 {
415     if ( !enabled() || !visible() ) {
416         return false;
417     }
418 
419     MarbleWidget *widget = dynamic_cast<MarbleWidget*>( object );
420     if ( !widget ) {
421         return AbstractFloatItem::eventFilter(object,e);
422     }
423 
424     if ( widget && !m_marbleWidget ) {
425         m_marbleWidget = widget;
426         connect( this, SIGNAL(dataUpdated()), this, SLOT(updateVisiblePoints()) );
427         connect( m_marbleWidget, SIGNAL(visibleLatLonAltBoxChanged(GeoDataLatLonAltBox)),
428                  this, SLOT(updateVisiblePoints()) );
429         connect( this, SIGNAL(settingsChanged(QString)), this, SLOT(updateVisiblePoints()) );
430     }
431 
432     if ( e->type() == QEvent::MouseButtonDblClick || e->type() == QEvent::MouseMove ) {
433         GeoDataTreeModel *const treeModel = const_cast<MarbleModel *>( marbleModel() )->treeModel();
434 
435         QMouseEvent *event = static_cast<QMouseEvent*>( e  );
436         QRectF plotRect = QRectF ( m_leftGraphMargin, 0, m_eleGraphWidth, contentSize().height() );
437         plotRect.translate( positivePosition() );
438         plotRect.translate( padding(), padding() );
439 
440         // for antialiasing: increase size by 1 px to each side
441         plotRect.translate(-1, -1);
442         plotRect.setSize(plotRect.size() + QSize(2, 2) );
443 
444         const bool cursorAboveFloatItem = plotRect.contains(event->pos());
445 
446         if ( cursorAboveFloatItem ) {
447             const int start = m_zoomToViewport ? m_firstVisiblePoint : 0;
448             const int end = m_zoomToViewport ? m_lastVisiblePoint : m_eleData.size();
449 
450             // Double click triggers recentering the map at the specified position
451             if ( e->type() == QEvent::MouseButtonDblClick ) {
452                 const QPointF mousePosition = event->pos() - plotRect.topLeft();
453                 const int xPos = mousePosition.x();
454                 for ( int i = start; i < end; ++i) {
455                     const int plotPos = ( m_eleData.value(i).x() - m_axisX.minValue() ) * m_eleGraphWidth / m_axisX.range();
456                     if ( plotPos >= xPos ) {
457                         widget->centerOn( m_points[i], true );
458                         break;
459                     }
460                 }
461                 return true;
462             }
463 
464             if ( e->type() == QEvent::MouseMove && !(event->buttons() & Qt::LeftButton) ) {
465                 // Cross hair cursor when moving above the float item
466                 // and mark the position on the graph
467                 widget->setCursor(QCursor(Qt::CrossCursor));
468                 if ( m_cursorPositionX != event->pos().x() - plotRect.left() ) {
469                     m_cursorPositionX = event->pos().x() - plotRect.left();
470                     const qreal xpos = m_axisX.minValue() + ( m_cursorPositionX / m_eleGraphWidth ) * m_axisX.range();
471                     GeoDataCoordinates currentPoint; // invalid coordinates
472                     for ( int i = start; i < end; ++i) {
473                         if ( m_eleData.value(i).x() >= xpos ) {
474                             currentPoint = m_points[i];
475                             currentPoint.setAltitude( m_eleData.value(i).y() );
476                             break;
477                         }
478                     }
479                     m_markerPlacemark->setCoordinate( currentPoint );
480                     if ( m_documentIndex < 0 ) {
481                         m_documentIndex = treeModel->addDocument( &m_markerDocument );
482                     }
483                     emit repaintNeeded();
484                 }
485 
486                 return true;
487             }
488         }
489         else {
490             if ( m_documentIndex >= 0 ) {
491                 m_markerPlacemark->setCoordinate( GeoDataCoordinates() ); // set to invalid
492                 treeModel->removeDocument( &m_markerDocument );
493                 m_documentIndex = -1;
494                 emit repaintNeeded();
495             }
496         }
497     }
498 
499     return AbstractFloatItem::eventFilter(object,e);
500 }
501 
handleDataUpdate(const GeoDataLineString & points,const QVector<QPointF> & eleData)502 void ElevationProfileFloatItem::handleDataUpdate(const GeoDataLineString &points, const QVector<QPointF> &eleData)
503 {
504     m_eleData = eleData;
505     m_points = points;
506     calculateStatistics( m_eleData );
507     if ( m_eleData.length() >= 2 ) {
508         m_axisX.setRange( m_eleData.first().x(), m_eleData.last().x() );
509         m_axisY.setRange( qMin( m_minElevation, qreal( 0.0 ) ), m_maxElevation );
510     }
511 
512     emit dataUpdated();
513 }
514 
updateVisiblePoints()515 void ElevationProfileFloatItem::updateVisiblePoints()
516 {
517     if ( ! m_activeDataSource->isDataAvailable() || m_points.size() < 2 ) {
518         return;
519     }
520 
521     // find the longest visible route section on screen
522     QList<QList<int> > routeSegments;
523     QList<int> currentRouteSegment;
524     for ( int i = 0; i < m_eleData.count(); i++ ) {
525         qreal lon = m_points[i].longitude(GeoDataCoordinates::Degree);
526         qreal lat = m_points[i].latitude (GeoDataCoordinates::Degree);
527         qreal x = 0;
528         qreal y = 0;
529 
530         if ( m_marbleWidget->screenCoordinates(lon, lat, x, y) ) {
531             // on screen --> add point to list
532             currentRouteSegment.append(i);
533         } else {
534             // off screen --> start new list
535             if ( !currentRouteSegment.isEmpty() ) {
536                 routeSegments.append( currentRouteSegment );
537                 currentRouteSegment.clear();
538             }
539         }
540     }
541     routeSegments.append( currentRouteSegment ); // in case the route ends on screen
542 
543     int maxLenght = 0;
544     for ( const QList<int> &currentRouteSegment: routeSegments ) {
545         if ( currentRouteSegment.size() > maxLenght ) {
546             maxLenght = currentRouteSegment.size() ;
547             m_firstVisiblePoint = currentRouteSegment.first();
548             m_lastVisiblePoint  = currentRouteSegment.last();
549         }
550     }
551     if ( m_firstVisiblePoint < 0 ) {
552         m_firstVisiblePoint = 0;
553     }
554     if ( m_lastVisiblePoint < 0 || m_lastVisiblePoint >= m_eleData.count() ) {
555         m_lastVisiblePoint = m_eleData.count() - 1;
556     }
557 
558     // include setting range to statistics and test for m_zoomToViewport in calculateStatistics();
559     if ( m_zoomToViewport ) {
560         calculateStatistics( m_eleData );
561         m_axisX.setRange( m_eleData.value( m_firstVisiblePoint ).x(),
562                           m_eleData.value( m_lastVisiblePoint  ).x() );
563         m_axisY.setRange( m_minElevation, m_maxElevation );
564     }
565 
566     return;
567 }
568 
calculateStatistics(const QVector<QPointF> & eleData)569 void ElevationProfileFloatItem::calculateStatistics(const QVector<QPointF> &eleData)
570 {
571     // This basically calculates the important peaks of the moving average filtered elevation and
572     // calculates the elevation data based on this points.
573     // This is done by always placing the averaging window in a way that it starts or ends at an
574     // original data point. This should ensure that all minima/maxima of the moving average
575     // filtered data are covered.
576     const qreal averageDistance = 200.0;
577 
578     m_maxElevation = 0.0;
579     m_minElevation = invalidElevationData;
580     m_gain = 0.0;
581     m_loss = 0.0;
582     const int start = m_zoomToViewport ? m_firstVisiblePoint : 0;
583     const int end = m_zoomToViewport ? m_lastVisiblePoint + 1 : eleData.size();
584 
585     if( start < end ) {
586         qreal lastX = eleData.value( start ).x();
587         qreal lastY = eleData.value( start ).y();
588         qreal nextX = eleData.value( start + 1 ).x();
589         qreal nextY = eleData.value( start + 1 ).y();
590 
591         m_maxElevation = qMax( lastY, nextY );
592         m_minElevation = qMin( lastY, nextY );
593 
594         int averageStart = start;
595         if(lastX + averageDistance < eleData.value( start + 2 ).x())
596             ++averageStart;
597 
598         for ( int index = start + 2; index <= end; ++index ) {
599             qreal indexX = index < end ? eleData.value( index ).x() : eleData.value( end - 1 ).x() + averageDistance;
600             qreal indexY = eleData.value( qMin( index, end - 1 ) ).y();
601             m_maxElevation = qMax( m_maxElevation, indexY );
602             m_minElevation = qMin( m_minElevation, indexY );
603 
604             // Low-pass filtering (moving average) of the elevation profile to calculate gain and loss values
605             // not always the best method, see for example
606             // http://www.ikg.uni-hannover.de/fileadmin/ikg/staff/thesis/finished/documents/StudArb_Schulze.pdf
607             // (German), chapter 4.2
608 
609             // Average over the part ending with the previous point.
610             // Do complete recalculation to avoid accumulation of floating point artifacts.
611             nextY = 0;
612             qreal averageX = nextX - averageDistance;
613             for( int averageIndex = averageStart; averageIndex < index; ++averageIndex ) {
614                 qreal nextAverageX = eleData.value( averageIndex ).x();
615                 qreal ratio = ( nextAverageX - averageX ) / averageDistance; // Weighting of original data based on covered distance
616                 nextY += eleData.value( qMax( averageIndex - 1, 0 ) ).y() * ratio;
617                 averageX = nextAverageX;
618             }
619 
620             while( averageStart < index ) {
621                 // This handles the part ending with the previous point on the first iteration and the parts starting with averageStart afterwards
622                 if ( nextY > lastY ) {
623                     m_gain += nextY - lastY;
624                 } else {
625                     m_loss += lastY - nextY;
626                 }
627 
628                 // Here we split the data into parts that average over the same data points
629                 // As soon as the end of the averaging window reaches the current point we reached the end of the current part
630                 lastX = nextX;
631                 lastY = nextY;
632                 nextX = eleData.value( averageStart ).x() + averageDistance;
633                 if( nextX >= indexX ) {
634                     break;
635                 }
636 
637                 // We don't need to recalculate the average completely, just remove the reached point
638                 qreal ratio = (nextX - lastX) / averageDistance;
639                 nextY += ( eleData.value( index - 1 ).y() - eleData.value( qMax( averageStart - 1, 0 ) ).y() ) * ratio;
640                 ++averageStart;
641             }
642 
643             // This is for the next part already, the end of the averaging window is at the following point
644             nextX = indexX;
645         }
646 
647         // Also include the last point
648         nextY = eleData.value( end - 1 ).y();
649         if ( nextY > lastY ) {
650             m_gain += nextY - lastY;
651         } else {
652             m_loss += lastY - nextY;
653         }
654     }
655 }
656 
forceRepaint()657 void ElevationProfileFloatItem::forceRepaint()
658 {
659     // We add one pixel as antialiasing could result into painting on these pixels to.
660     QRectF floatItemRect = QRectF( positivePosition() - QPoint( 1, 1 ),
661                                    size() + QSize( 2, 2 ) );
662     update();
663     emit repaintNeeded( floatItemRect.toRect() );
664 }
665 
readSettings()666 void ElevationProfileFloatItem::readSettings()
667 {
668     if ( !m_configDialog )
669         return;
670 
671     if ( m_zoomToViewport ) {
672         ui_configWidget->m_zoomToViewportCheckBox->setCheckState( Qt::Checked );
673     }
674     else {
675         ui_configWidget->m_zoomToViewportCheckBox->setCheckState( Qt::Unchecked );
676     }
677 }
678 
writeSettings()679 void ElevationProfileFloatItem::writeSettings()
680 {
681     if ( ui_configWidget->m_zoomToViewportCheckBox->checkState() == Qt::Checked ) {
682         m_zoomToViewport = true;
683     } else {
684         m_zoomToViewport = false;
685     }
686 
687     emit settingsChanged( nameId() );
688 }
689 
toggleZoomToViewport()690 void ElevationProfileFloatItem::toggleZoomToViewport()
691 {
692     m_zoomToViewport = ! m_zoomToViewport;
693     calculateStatistics( m_eleData );
694     if ( ! m_zoomToViewport ) {
695         m_axisX.setRange( m_eleData.first().x(), m_eleData.last().x() );
696         m_axisY.setRange( qMin( m_minElevation, qreal( 0.0 ) ), m_maxElevation );
697     }
698     readSettings();
699     emit settingsChanged( nameId() );
700 }
701 
switchToRouteDataSource()702 void ElevationProfileFloatItem::switchToRouteDataSource()
703 {
704     switchDataSource(&m_routeDataSource);
705 }
706 
switchToTrackDataSource(int index)707 void ElevationProfileFloatItem::switchToTrackDataSource(int index)
708 {
709     m_trackDataSource.setSourceIndex(index);
710     switchDataSource(&m_trackDataSource);
711 }
712 
switchDataSource(ElevationProfileDataSource * source)713 void ElevationProfileFloatItem::switchDataSource(ElevationProfileDataSource* source)
714 {
715     if (m_activeDataSource) {
716         disconnect(m_activeDataSource, SIGNAL(dataUpdated(GeoDataLineString,QVector<QPointF>)),nullptr,nullptr);
717     }
718     m_activeDataSource = source;
719     connect(m_activeDataSource, SIGNAL(dataUpdated(GeoDataLineString,QVector<QPointF>)), this, SLOT(handleDataUpdate(GeoDataLineString,QVector<QPointF>)));
720     m_activeDataSource->requestUpdate();
721 }
722 
723 }
724 
725 #include "moc_ElevationProfileFloatItem.cpp"
726