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> ¤tRouteSegment: 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