File indexing completed on 2024-12-08 06:35:36
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2011-2012 Florian Eßer <f.esser@rwth-aachen.de> 0004 // SPDX-FileCopyrightText: 2012 Bernhard Beschow <bbeschow@cs.tu-berlin.de> 0005 // SPDX-FileCopyrightText: 2013 Roman Karlstetter <roman.karlstetter@googlemail.com> 0006 // 0007 0008 #include "ElevationProfileFloatItem.h" 0009 0010 #include "ElevationProfileContextMenu.h" 0011 #include "ui_ElevationProfileConfigWidget.h" 0012 0013 #include "MarbleModel.h" 0014 #include "MarbleWidget.h" 0015 #include "GeoDataPlacemark.h" 0016 #include "GeoDataTreeModel.h" 0017 #include "ViewportParams.h" 0018 #include "MarbleColors.h" 0019 #include "MarbleDirs.h" 0020 #include "ElevationModel.h" 0021 #include "MarbleGraphicsGridLayout.h" 0022 #include "MarbleDebug.h" 0023 #include "routing/RoutingManager.h" 0024 #include "routing/RoutingModel.h" 0025 0026 #include <QContextMenuEvent> 0027 #include <QRect> 0028 #include <QPainter> 0029 #include <QPainterPath> 0030 #include <QPushButton> 0031 #include <QMenu> 0032 #include <QMouseEvent> 0033 0034 namespace Marble 0035 { 0036 0037 ElevationProfileFloatItem::ElevationProfileFloatItem( const MarbleModel *marbleModel ) 0038 : AbstractFloatItem( marbleModel, QPointF( 220, 10.5 ), QSizeF( 0.0, 50.0 ) ), 0039 m_activeDataSource(nullptr), 0040 m_routeDataSource( marbleModel ? marbleModel->routingManager()->routingModel() : nullptr, marbleModel ? marbleModel->elevationModel() : nullptr, this ), 0041 m_trackDataSource( marbleModel ? marbleModel->treeModel() : nullptr, this ), 0042 m_configDialog( nullptr ), 0043 ui_configWidget( nullptr ), 0044 m_leftGraphMargin( 0 ), 0045 m_eleGraphWidth( 0 ), 0046 m_viewportWidth( 0 ), 0047 m_shrinkFactorY( 1.2 ), 0048 m_fontHeight( 10 ), 0049 m_markerPlacemark( new GeoDataPlacemark ), 0050 m_documentIndex( -1 ), 0051 m_cursorPositionX( 0 ), 0052 m_isInitialized( false ), 0053 m_contextMenu( nullptr ), 0054 m_marbleWidget( nullptr ), 0055 m_firstVisiblePoint( 0 ), 0056 m_lastVisiblePoint( 0 ), 0057 m_zoomToViewport( false ) 0058 { 0059 setVisible( false ); 0060 bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen; 0061 if ( smallScreen ) { 0062 setPosition( QPointF( 10.5, 10.5 ) ); 0063 } 0064 bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution; 0065 m_eleGraphHeight = highRes ? 100 : 50; /// TODO make configurable 0066 0067 setPadding( 1 ); 0068 0069 m_markerDocument.setDocumentRole( UnknownDocument ); 0070 m_markerDocument.setName(QStringLiteral("Elevation Profile")); 0071 0072 m_markerPlacemark->setName(QStringLiteral("Elevation Marker")); 0073 m_markerPlacemark->setVisible( false ); 0074 m_markerDocument.append( m_markerPlacemark ); 0075 0076 m_contextMenu = new ElevationProfileContextMenu(this); 0077 connect( &m_trackDataSource, SIGNAL(sourceCountChanged()), m_contextMenu, SLOT(updateContextMenuEntries()) ); 0078 connect( &m_routeDataSource, SIGNAL(sourceCountChanged()), m_contextMenu, SLOT(updateContextMenuEntries()) ); 0079 } 0080 0081 ElevationProfileFloatItem::~ElevationProfileFloatItem() 0082 { 0083 } 0084 0085 QStringList ElevationProfileFloatItem::backendTypes() const 0086 { 0087 return QStringList(QStringLiteral("elevationprofile")); 0088 } 0089 0090 qreal ElevationProfileFloatItem::zValue() const 0091 { 0092 return 3.0; 0093 } 0094 0095 QString ElevationProfileFloatItem::name() const 0096 { 0097 return tr("Elevation Profile"); 0098 } 0099 0100 QString ElevationProfileFloatItem::guiString() const 0101 { 0102 return tr("&Elevation Profile"); 0103 } 0104 0105 QString ElevationProfileFloatItem::nameId() const 0106 { 0107 return QStringLiteral("elevationprofile"); 0108 } 0109 0110 QString ElevationProfileFloatItem::version() const 0111 { 0112 return QStringLiteral("1.2"); // TODO: increase to 1.3 ? 0113 } 0114 0115 QString ElevationProfileFloatItem::description() const 0116 { 0117 return tr( "A float item that shows the elevation profile of the current route." ); 0118 } 0119 0120 QString ElevationProfileFloatItem::copyrightYears() const 0121 { 0122 return QStringLiteral("2011, 2012, 2013"); 0123 } 0124 0125 QVector<PluginAuthor> ElevationProfileFloatItem::pluginAuthors() const 0126 { 0127 return QVector<PluginAuthor>() 0128 << PluginAuthor(QStringLiteral("Florian Eßer"),QStringLiteral("f.esser@rwth-aachen.de")) 0129 << PluginAuthor(QStringLiteral("Bernhard Beschow"), QStringLiteral("bbeschow@cs.tu-berlin.de")) 0130 << PluginAuthor(QStringLiteral("Roman Karlstetter"), QStringLiteral("roman.karlstetter@googlemail.com")); 0131 } 0132 0133 QIcon ElevationProfileFloatItem::icon () const 0134 { 0135 return QIcon(QStringLiteral(":/icons/elevationprofile.png")); 0136 } 0137 0138 void ElevationProfileFloatItem::initialize () 0139 { 0140 connect( marbleModel()->elevationModel(), SIGNAL(updateAvailable()), &m_routeDataSource, SLOT(requestUpdate()) ); 0141 connect( marbleModel()->routingManager()->routingModel(), SIGNAL(currentRouteChanged()), &m_routeDataSource, SLOT(requestUpdate()) ); 0142 connect( this, SIGNAL(dataUpdated()), SLOT(forceRepaint()) ); 0143 switchDataSource(&m_routeDataSource); 0144 0145 m_fontHeight = QFontMetricsF( font() ).ascent() + 1; 0146 m_leftGraphMargin = QFontMetricsF( font() ).width( "0000 m" ); /// TODO make this dynamic according to actual need 0147 0148 m_isInitialized = true; 0149 } 0150 0151 bool ElevationProfileFloatItem::isInitialized () const 0152 { 0153 return m_isInitialized; 0154 } 0155 0156 void ElevationProfileFloatItem::setProjection( const ViewportParams *viewport ) 0157 { 0158 if ( !( viewport->width() == m_viewportWidth && m_isInitialized ) ) { 0159 bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution; 0160 int const widthRatio = highRes ? 2 : 3; 0161 setContentSize( QSizeF( viewport->width() / widthRatio, 0162 m_eleGraphHeight + m_fontHeight * 2.5 ) ); 0163 m_eleGraphWidth = contentSize().width() - m_leftGraphMargin; 0164 m_axisX.setLength( m_eleGraphWidth ); 0165 m_axisY.setLength( m_eleGraphHeight ); 0166 m_axisX.setTickCount( 3, m_eleGraphWidth / ( m_leftGraphMargin * 1.5 ) ); 0167 m_axisY.setTickCount( 2, m_eleGraphHeight / m_fontHeight ); 0168 m_viewportWidth = viewport->width(); 0169 bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen; 0170 if ( !m_isInitialized && !smallScreen ) { 0171 setPosition( QPointF( (viewport->width() - contentSize().width()) / 2 , 10.5 ) ); 0172 } 0173 } 0174 0175 update(); 0176 0177 AbstractFloatItem::setProjection( viewport ); 0178 } 0179 0180 void ElevationProfileFloatItem::paintContent( QPainter *painter ) 0181 { 0182 // do not try to draw if not initialized 0183 if(!isInitialized()) { 0184 return; 0185 } 0186 painter->save(); 0187 painter->setRenderHint( QPainter::Antialiasing, true ); 0188 painter->setFont( font() ); 0189 0190 if ( ! ( m_activeDataSource->isDataAvailable() && m_eleData.size() > 0 ) ) { 0191 painter->setPen( QColor( Qt::black ) ); 0192 QString text = tr( "Create a route or load a track from file to view its elevation profile." ); 0193 painter->drawText( contentRect().toRect(), Qt::TextWordWrap | Qt::AlignCenter, text ); 0194 painter->restore(); 0195 return; 0196 } 0197 if ( m_zoomToViewport && ( m_lastVisiblePoint - m_firstVisiblePoint < 5 ) ) { 0198 painter->setPen( QColor( Qt::black ) ); 0199 QString text = tr( "Not enough points in the current viewport.\nTry to disable 'Zoom to viewport'." ); 0200 painter->drawText( contentRect().toRect(), Qt::TextWordWrap | Qt::AlignCenter, text ); 0201 painter->restore(); 0202 return; 0203 } 0204 0205 QString intervalStr; 0206 int lastStringEnds; 0207 0208 // draw viewport bounds 0209 if ( ! m_zoomToViewport && ( m_firstVisiblePoint > 0 || m_lastVisiblePoint < m_eleData.size() - 1 ) ) { 0210 QColor color( Qt::black ); 0211 color.setAlpha( 64 ); 0212 QRect rect; 0213 rect.setLeft( m_leftGraphMargin + m_eleData.value( m_firstVisiblePoint ).x() * m_eleGraphWidth / m_axisX.range() ); 0214 rect.setTop( 0 ); 0215 rect.setWidth( ( m_eleData.value( m_lastVisiblePoint ).x() - m_eleData.value( m_firstVisiblePoint ).x() ) * m_eleGraphWidth / m_axisX.range() ); 0216 rect.setHeight( m_eleGraphHeight ); 0217 painter->fillRect( rect, color ); 0218 } 0219 0220 // draw X and Y axis 0221 painter->setPen( Oxygen::aluminumGray4 ); 0222 painter->drawLine( m_leftGraphMargin, m_eleGraphHeight, contentSize().width(), m_eleGraphHeight ); 0223 painter->drawLine( m_leftGraphMargin, m_eleGraphHeight, m_leftGraphMargin, 0 ); 0224 0225 // draw Y grid and labels 0226 painter->setPen( QColor( Qt::black ) ); 0227 QPen dashedPen( Qt::DashLine ); 0228 dashedPen.setColor( Oxygen::aluminumGray4 ); 0229 QRect labelRect( 0, 0, m_leftGraphMargin - 1, m_fontHeight + 2 ); 0230 lastStringEnds = m_eleGraphHeight + m_fontHeight; 0231 // painter->drawText(m_leftGraphMargin + 1, m_fontHeight, QLatin1Char('[') + m_axisY.unit() + QLatin1Char(']')); 0232 for ( const AxisTick &tick: m_axisY.ticks() ) { 0233 const int posY = m_eleGraphHeight - tick.position; 0234 painter->setPen( dashedPen ); 0235 painter->drawLine( m_leftGraphMargin, posY, contentSize().width(), posY ); 0236 0237 labelRect.moveCenter( QPoint( labelRect.center().x(), posY ) ); 0238 if ( labelRect.top() < 0 ) { 0239 // don't cut off uppermost label 0240 labelRect.moveTop( 0 ); 0241 } 0242 if ( labelRect.bottom() >= lastStringEnds ) { 0243 // Don't print overlapping labels 0244 continue; 0245 } 0246 lastStringEnds = labelRect.top(); 0247 painter->setPen( QColor( Qt::black ) ); 0248 intervalStr.setNum( tick.value * m_axisY.scale() ); 0249 painter->drawText( labelRect, Qt::AlignRight, intervalStr ); 0250 } 0251 0252 // draw X grid and labels 0253 painter->setPen( QColor( Qt::black ) ); 0254 labelRect.moveTop( m_eleGraphHeight + 1 ); 0255 lastStringEnds = 0; 0256 for ( const AxisTick &tick: m_axisX.ticks() ) { 0257 const int posX = m_leftGraphMargin + tick.position; 0258 painter->setPen( dashedPen ); 0259 painter->drawLine( posX, 0, posX, m_eleGraphHeight ); 0260 0261 intervalStr.setNum( tick.value * m_axisX.scale() ); 0262 if ( tick.position == m_axisX.ticks().last().position ) { 0263 intervalStr += QLatin1Char(' ') + m_axisX.unit(); 0264 } 0265 labelRect.setWidth( QFontMetricsF( font() ).width( intervalStr ) * 1.5 ); 0266 labelRect.moveCenter( QPoint( posX, labelRect.center().y() ) ); 0267 if ( labelRect.right() > m_leftGraphMargin + m_eleGraphWidth ) { 0268 // don't cut off rightmost label 0269 labelRect.moveRight( m_leftGraphMargin + m_eleGraphWidth ); 0270 } 0271 if ( labelRect.left() <= lastStringEnds ) { 0272 // Don't print overlapping labels 0273 continue; 0274 } 0275 lastStringEnds = labelRect.right(); 0276 painter->setPen( QColor( Qt::black ) ); 0277 painter->drawText( labelRect, Qt::AlignCenter, intervalStr ); 0278 } 0279 0280 // display elevation gain/loss data 0281 painter->setPen( QColor( Qt::black ) ); 0282 intervalStr = tr( "Difference: %1 %2" ) 0283 .arg( QString::number( m_gain - m_loss, 'f', 0 ) ) 0284 .arg( m_axisY.unit() ); 0285 intervalStr += QString::fromUtf8( " (↗ %1 %3 ↘ %2 %3)" ) 0286 .arg( QString::number( m_gain, 'f', 0 ) ) 0287 .arg( QString::number( m_loss, 'f', 0 ) ) 0288 .arg( m_axisY.unit() ); 0289 painter->drawText( contentRect().toRect(), Qt::AlignBottom | Qt::AlignCenter, intervalStr ); 0290 0291 // draw elevation profile 0292 painter->setPen( QColor( Qt::black ) ); 0293 bool const highRes = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::HighResolution; 0294 QPen pen = painter->pen(); 0295 pen.setWidth( highRes ? 2 : 1 ); 0296 painter->setPen( pen ); 0297 0298 QLinearGradient fillGradient( 0, 0, 0, m_eleGraphHeight ); 0299 QColor startColor = Oxygen::forestGreen4; 0300 QColor endColor = Oxygen::hotOrange4; 0301 startColor.setAlpha( 200 ); 0302 endColor.setAlpha( 32 ); 0303 fillGradient.setColorAt( 0.0, startColor ); 0304 fillGradient.setColorAt( 1.0, endColor ); 0305 QBrush brush = QBrush( fillGradient ); 0306 painter->setBrush( brush ); 0307 0308 QPoint oldPos; 0309 oldPos.setX( m_leftGraphMargin ); 0310 oldPos.setY( ( m_axisY.minValue() - m_axisY.minValue() ) 0311 * m_eleGraphHeight / ( m_axisY.range() / m_shrinkFactorY ) ); 0312 oldPos.setY( m_eleGraphHeight - oldPos.y() ); 0313 QPainterPath path; 0314 path.moveTo( oldPos.x(), m_eleGraphHeight ); 0315 path.lineTo( oldPos.x(), oldPos.y() ); 0316 0317 const int start = m_zoomToViewport ? m_firstVisiblePoint : 0; 0318 const int end = m_zoomToViewport ? m_lastVisiblePoint : m_eleData.size() - 1; 0319 for ( int i = start; i <= end; ++i ) { 0320 QPoint newPos; 0321 if ( i == start ) { 0322 // make sure the plot always starts at the y-axis 0323 newPos.setX( 0 ); 0324 } else { 0325 newPos.setX( ( m_eleData.value(i).x() - m_axisX.minValue() ) * m_eleGraphWidth / m_axisX.range() ); 0326 } 0327 newPos.rx() += m_leftGraphMargin; 0328 if ( newPos.x() != oldPos.x() || newPos.y() != oldPos.y() ) { 0329 newPos.setY( ( m_eleData.value(i).y() - m_axisY.minValue() ) 0330 * m_eleGraphHeight / ( m_axisY.range() * m_shrinkFactorY ) ); 0331 newPos.setY( m_eleGraphHeight - newPos.y() ); 0332 path.lineTo( newPos.x(), newPos.y() ); 0333 oldPos = newPos; 0334 } 0335 } 0336 path.lineTo( oldPos.x(), m_eleGraphHeight ); 0337 // fill 0338 painter->setPen( QPen( Qt::NoPen ) ); 0339 painter->drawPath( path ); 0340 // contour 0341 // "remove" the first and last path element first, they are only used to fill down to the bottom 0342 painter->setBrush( QBrush( Qt::NoBrush ) ); 0343 path.setElementPositionAt( 0, path.elementAt( 1 ).x, path.elementAt( 1 ).y ); 0344 path.setElementPositionAt( path.elementCount()-1, 0345 path.elementAt( path.elementCount()-2 ).x, 0346 path.elementAt( path.elementCount()-2 ).y ); 0347 painter->setPen( pen ); 0348 painter->drawPath( path ); 0349 0350 pen.setWidth( 1 ); 0351 painter->setPen( pen ); 0352 0353 // draw interactive cursor 0354 const GeoDataCoordinates currentPoint = m_markerPlacemark->coordinate(); 0355 if ( currentPoint.isValid() ) { 0356 painter->setPen( QColor( Qt::white ) ); 0357 painter->drawLine( m_leftGraphMargin + m_cursorPositionX, 0, 0358 m_leftGraphMargin + m_cursorPositionX, m_eleGraphHeight ); 0359 qreal xpos = m_axisX.minValue() + ( m_cursorPositionX / m_eleGraphWidth ) * m_axisX.range(); 0360 qreal ypos = m_eleGraphHeight - ( ( currentPoint.altitude() - m_axisY.minValue() ) / ( qMax<qreal>( 1.0, m_axisY.range() ) * m_shrinkFactorY ) ) * m_eleGraphHeight; 0361 0362 painter->drawLine( m_leftGraphMargin + m_cursorPositionX - 5, ypos, 0363 m_leftGraphMargin + m_cursorPositionX + 5, ypos ); 0364 intervalStr.setNum( xpos * m_axisX.scale(), 'f', 2 ); 0365 intervalStr += QLatin1Char(' ') + m_axisX.unit(); 0366 int currentStringBegin = m_leftGraphMargin + m_cursorPositionX 0367 - QFontMetricsF( font() ).width( intervalStr ) / 2; 0368 painter->drawText( currentStringBegin, contentSize().height() - 1.5 * m_fontHeight, intervalStr ); 0369 0370 intervalStr.setNum( currentPoint.altitude(), 'f', 1 ); 0371 intervalStr += QLatin1Char(' ') + m_axisY.unit(); 0372 if ( m_cursorPositionX + QFontMetricsF( font() ).width( intervalStr ) + m_leftGraphMargin 0373 < m_eleGraphWidth ) { 0374 currentStringBegin = ( m_leftGraphMargin + m_cursorPositionX + 5 + 2 ); 0375 } else { 0376 currentStringBegin = m_leftGraphMargin + m_cursorPositionX - 5 0377 - QFontMetricsF( font() ).width( intervalStr ) * 1.5; 0378 } 0379 // Make sure the text still fits into the window 0380 while ( ypos < m_fontHeight ) { 0381 ypos++; 0382 } 0383 painter->drawText( currentStringBegin, ypos + m_fontHeight / 2, intervalStr ); 0384 } 0385 0386 painter->restore(); 0387 } 0388 0389 QDialog *ElevationProfileFloatItem::configDialog() //FIXME TODO Make a config dialog? /// TODO what is this comment? 0390 { 0391 if ( !m_configDialog ) { 0392 // Initializing configuration dialog 0393 m_configDialog = new QDialog(); 0394 ui_configWidget = new Ui::ElevationProfileConfigWidget; 0395 ui_configWidget->setupUi( m_configDialog ); 0396 0397 readSettings(); 0398 0399 connect( ui_configWidget->m_buttonBox, SIGNAL(accepted()), SLOT(writeSettings()) ); 0400 connect( ui_configWidget->m_buttonBox, SIGNAL(rejected()), SLOT(readSettings()) ); 0401 QPushButton *applyButton = ui_configWidget->m_buttonBox->button( QDialogButtonBox::Apply ); 0402 connect( applyButton, SIGNAL(clicked()), this, SLOT(writeSettings()) ); 0403 } 0404 return m_configDialog; 0405 } 0406 0407 void ElevationProfileFloatItem::contextMenuEvent( QWidget *w, QContextMenuEvent *e ) 0408 { 0409 Q_ASSERT( m_contextMenu ); 0410 m_contextMenu->getMenu()->exec( w->mapToGlobal( e->pos() ) ); 0411 } 0412 0413 bool ElevationProfileFloatItem::eventFilter( QObject *object, QEvent *e ) 0414 { 0415 if ( !enabled() || !visible() ) { 0416 return false; 0417 } 0418 0419 MarbleWidget *widget = dynamic_cast<MarbleWidget*>( object ); 0420 if ( !widget ) { 0421 return AbstractFloatItem::eventFilter(object,e); 0422 } 0423 0424 if ( widget && !m_marbleWidget ) { 0425 m_marbleWidget = widget; 0426 connect( this, SIGNAL(dataUpdated()), this, SLOT(updateVisiblePoints()) ); 0427 connect( m_marbleWidget, SIGNAL(visibleLatLonAltBoxChanged(GeoDataLatLonAltBox)), 0428 this, SLOT(updateVisiblePoints()) ); 0429 connect( this, SIGNAL(settingsChanged(QString)), this, SLOT(updateVisiblePoints()) ); 0430 } 0431 0432 if ( e->type() == QEvent::MouseButtonDblClick || e->type() == QEvent::MouseMove ) { 0433 GeoDataTreeModel *const treeModel = const_cast<MarbleModel *>( marbleModel() )->treeModel(); 0434 0435 QMouseEvent *event = static_cast<QMouseEvent*>( e ); 0436 QRectF plotRect = QRectF ( m_leftGraphMargin, 0, m_eleGraphWidth, contentSize().height() ); 0437 plotRect.translate( positivePosition() ); 0438 plotRect.translate( padding(), padding() ); 0439 0440 // for antialiasing: increase size by 1 px to each side 0441 plotRect.translate(-1, -1); 0442 plotRect.setSize(plotRect.size() + QSize(2, 2) ); 0443 0444 const bool cursorAboveFloatItem = plotRect.contains(event->pos()); 0445 0446 if ( cursorAboveFloatItem ) { 0447 const int start = m_zoomToViewport ? m_firstVisiblePoint : 0; 0448 const int end = m_zoomToViewport ? m_lastVisiblePoint : m_eleData.size(); 0449 0450 // Double click triggers recentering the map at the specified position 0451 if ( e->type() == QEvent::MouseButtonDblClick ) { 0452 const QPointF mousePosition = event->pos() - plotRect.topLeft(); 0453 const int xPos = mousePosition.x(); 0454 for ( int i = start; i < end; ++i) { 0455 const int plotPos = ( m_eleData.value(i).x() - m_axisX.minValue() ) * m_eleGraphWidth / m_axisX.range(); 0456 if ( plotPos >= xPos ) { 0457 widget->centerOn( m_points[i], true ); 0458 break; 0459 } 0460 } 0461 return true; 0462 } 0463 0464 if ( e->type() == QEvent::MouseMove && !(event->buttons() & Qt::LeftButton) ) { 0465 // Cross hair cursor when moving above the float item 0466 // and mark the position on the graph 0467 widget->setCursor(QCursor(Qt::CrossCursor)); 0468 if ( m_cursorPositionX != event->pos().x() - plotRect.left() ) { 0469 m_cursorPositionX = event->pos().x() - plotRect.left(); 0470 const qreal xpos = m_axisX.minValue() + ( m_cursorPositionX / m_eleGraphWidth ) * m_axisX.range(); 0471 GeoDataCoordinates currentPoint; // invalid coordinates 0472 for ( int i = start; i < end; ++i) { 0473 if ( m_eleData.value(i).x() >= xpos ) { 0474 currentPoint = m_points[i]; 0475 currentPoint.setAltitude( m_eleData.value(i).y() ); 0476 break; 0477 } 0478 } 0479 m_markerPlacemark->setCoordinate( currentPoint ); 0480 if ( m_documentIndex < 0 ) { 0481 m_documentIndex = treeModel->addDocument( &m_markerDocument ); 0482 } 0483 emit repaintNeeded(); 0484 } 0485 0486 return true; 0487 } 0488 } 0489 else { 0490 if ( m_documentIndex >= 0 ) { 0491 m_markerPlacemark->setCoordinate( GeoDataCoordinates() ); // set to invalid 0492 treeModel->removeDocument( &m_markerDocument ); 0493 m_documentIndex = -1; 0494 emit repaintNeeded(); 0495 } 0496 } 0497 } 0498 0499 return AbstractFloatItem::eventFilter(object,e); 0500 } 0501 0502 void ElevationProfileFloatItem::handleDataUpdate(const GeoDataLineString &points, const QVector<QPointF> &eleData) 0503 { 0504 m_eleData = eleData; 0505 m_points = points; 0506 calculateStatistics( m_eleData ); 0507 if ( m_eleData.length() >= 2 ) { 0508 m_axisX.setRange( m_eleData.first().x(), m_eleData.last().x() ); 0509 m_axisY.setRange( qMin( m_minElevation, qreal( 0.0 ) ), m_maxElevation ); 0510 } 0511 0512 emit dataUpdated(); 0513 } 0514 0515 void ElevationProfileFloatItem::updateVisiblePoints() 0516 { 0517 if ( ! m_activeDataSource->isDataAvailable() || m_points.size() < 2 ) { 0518 return; 0519 } 0520 0521 // find the longest visible route section on screen 0522 QList<QList<int> > routeSegments; 0523 QList<int> currentRouteSegment; 0524 for ( int i = 0; i < m_eleData.count(); i++ ) { 0525 qreal lon = m_points[i].longitude(GeoDataCoordinates::Degree); 0526 qreal lat = m_points[i].latitude (GeoDataCoordinates::Degree); 0527 qreal x = 0; 0528 qreal y = 0; 0529 0530 if ( m_marbleWidget->screenCoordinates(lon, lat, x, y) ) { 0531 // on screen --> add point to list 0532 currentRouteSegment.append(i); 0533 } else { 0534 // off screen --> start new list 0535 if ( !currentRouteSegment.isEmpty() ) { 0536 routeSegments.append( currentRouteSegment ); 0537 currentRouteSegment.clear(); 0538 } 0539 } 0540 } 0541 routeSegments.append( currentRouteSegment ); // in case the route ends on screen 0542 0543 int maxLenght = 0; 0544 for ( const QList<int> ¤tRouteSegment: routeSegments ) { 0545 if ( currentRouteSegment.size() > maxLenght ) { 0546 maxLenght = currentRouteSegment.size() ; 0547 m_firstVisiblePoint = currentRouteSegment.first(); 0548 m_lastVisiblePoint = currentRouteSegment.last(); 0549 } 0550 } 0551 if ( m_firstVisiblePoint < 0 ) { 0552 m_firstVisiblePoint = 0; 0553 } 0554 if ( m_lastVisiblePoint < 0 || m_lastVisiblePoint >= m_eleData.count() ) { 0555 m_lastVisiblePoint = m_eleData.count() - 1; 0556 } 0557 0558 // include setting range to statistics and test for m_zoomToViewport in calculateStatistics(); 0559 if ( m_zoomToViewport ) { 0560 calculateStatistics( m_eleData ); 0561 m_axisX.setRange( m_eleData.value( m_firstVisiblePoint ).x(), 0562 m_eleData.value( m_lastVisiblePoint ).x() ); 0563 m_axisY.setRange( m_minElevation, m_maxElevation ); 0564 } 0565 0566 return; 0567 } 0568 0569 void ElevationProfileFloatItem::calculateStatistics(const QVector<QPointF> &eleData) 0570 { 0571 // This basically calculates the important peaks of the moving average filtered elevation and 0572 // calculates the elevation data based on this points. 0573 // This is done by always placing the averaging window in a way that it starts or ends at an 0574 // original data point. This should ensure that all minima/maxima of the moving average 0575 // filtered data are covered. 0576 const qreal averageDistance = 200.0; 0577 0578 m_maxElevation = 0.0; 0579 m_minElevation = invalidElevationData; 0580 m_gain = 0.0; 0581 m_loss = 0.0; 0582 const int start = m_zoomToViewport ? m_firstVisiblePoint : 0; 0583 const int end = m_zoomToViewport ? m_lastVisiblePoint + 1 : eleData.size(); 0584 0585 if( start < end ) { 0586 qreal lastX = eleData.value( start ).x(); 0587 qreal lastY = eleData.value( start ).y(); 0588 qreal nextX = eleData.value( start + 1 ).x(); 0589 qreal nextY = eleData.value( start + 1 ).y(); 0590 0591 m_maxElevation = qMax( lastY, nextY ); 0592 m_minElevation = qMin( lastY, nextY ); 0593 0594 int averageStart = start; 0595 if(lastX + averageDistance < eleData.value( start + 2 ).x()) 0596 ++averageStart; 0597 0598 for ( int index = start + 2; index <= end; ++index ) { 0599 qreal indexX = index < end ? eleData.value( index ).x() : eleData.value( end - 1 ).x() + averageDistance; 0600 qreal indexY = eleData.value( qMin( index, end - 1 ) ).y(); 0601 m_maxElevation = qMax( m_maxElevation, indexY ); 0602 m_minElevation = qMin( m_minElevation, indexY ); 0603 0604 // Low-pass filtering (moving average) of the elevation profile to calculate gain and loss values 0605 // not always the best method, see for example 0606 // http://www.ikg.uni-hannover.de/fileadmin/ikg/staff/thesis/finished/documents/StudArb_Schulze.pdf 0607 // (German), chapter 4.2 0608 0609 // Average over the part ending with the previous point. 0610 // Do complete recalculation to avoid accumulation of floating point artifacts. 0611 nextY = 0; 0612 qreal averageX = nextX - averageDistance; 0613 for( int averageIndex = averageStart; averageIndex < index; ++averageIndex ) { 0614 qreal nextAverageX = eleData.value( averageIndex ).x(); 0615 qreal ratio = ( nextAverageX - averageX ) / averageDistance; // Weighting of original data based on covered distance 0616 nextY += eleData.value( qMax( averageIndex - 1, 0 ) ).y() * ratio; 0617 averageX = nextAverageX; 0618 } 0619 0620 while( averageStart < index ) { 0621 // This handles the part ending with the previous point on the first iteration and the parts starting with averageStart afterwards 0622 if ( nextY > lastY ) { 0623 m_gain += nextY - lastY; 0624 } else { 0625 m_loss += lastY - nextY; 0626 } 0627 0628 // Here we split the data into parts that average over the same data points 0629 // As soon as the end of the averaging window reaches the current point we reached the end of the current part 0630 lastX = nextX; 0631 lastY = nextY; 0632 nextX = eleData.value( averageStart ).x() + averageDistance; 0633 if( nextX >= indexX ) { 0634 break; 0635 } 0636 0637 // We don't need to recalculate the average completely, just remove the reached point 0638 qreal ratio = (nextX - lastX) / averageDistance; 0639 nextY += ( eleData.value( index - 1 ).y() - eleData.value( qMax( averageStart - 1, 0 ) ).y() ) * ratio; 0640 ++averageStart; 0641 } 0642 0643 // This is for the next part already, the end of the averaging window is at the following point 0644 nextX = indexX; 0645 } 0646 0647 // Also include the last point 0648 nextY = eleData.value( end - 1 ).y(); 0649 if ( nextY > lastY ) { 0650 m_gain += nextY - lastY; 0651 } else { 0652 m_loss += lastY - nextY; 0653 } 0654 } 0655 } 0656 0657 void ElevationProfileFloatItem::forceRepaint() 0658 { 0659 // We add one pixel as antialiasing could result into painting on these pixels to. 0660 QRectF floatItemRect = QRectF( positivePosition() - QPoint( 1, 1 ), 0661 size() + QSize( 2, 2 ) ); 0662 update(); 0663 emit repaintNeeded( floatItemRect.toRect() ); 0664 } 0665 0666 void ElevationProfileFloatItem::readSettings() 0667 { 0668 if ( !m_configDialog ) 0669 return; 0670 0671 if ( m_zoomToViewport ) { 0672 ui_configWidget->m_zoomToViewportCheckBox->setCheckState( Qt::Checked ); 0673 } 0674 else { 0675 ui_configWidget->m_zoomToViewportCheckBox->setCheckState( Qt::Unchecked ); 0676 } 0677 } 0678 0679 void ElevationProfileFloatItem::writeSettings() 0680 { 0681 if ( ui_configWidget->m_zoomToViewportCheckBox->checkState() == Qt::Checked ) { 0682 m_zoomToViewport = true; 0683 } else { 0684 m_zoomToViewport = false; 0685 } 0686 0687 emit settingsChanged( nameId() ); 0688 } 0689 0690 void ElevationProfileFloatItem::toggleZoomToViewport() 0691 { 0692 m_zoomToViewport = ! m_zoomToViewport; 0693 calculateStatistics( m_eleData ); 0694 if ( ! m_zoomToViewport ) { 0695 m_axisX.setRange( m_eleData.first().x(), m_eleData.last().x() ); 0696 m_axisY.setRange( qMin( m_minElevation, qreal( 0.0 ) ), m_maxElevation ); 0697 } 0698 readSettings(); 0699 emit settingsChanged( nameId() ); 0700 } 0701 0702 void ElevationProfileFloatItem::switchToRouteDataSource() 0703 { 0704 switchDataSource(&m_routeDataSource); 0705 } 0706 0707 void ElevationProfileFloatItem::switchToTrackDataSource(int index) 0708 { 0709 m_trackDataSource.setSourceIndex(index); 0710 switchDataSource(&m_trackDataSource); 0711 } 0712 0713 void ElevationProfileFloatItem::switchDataSource(ElevationProfileDataSource* source) 0714 { 0715 if (m_activeDataSource) { 0716 disconnect(m_activeDataSource, SIGNAL(dataUpdated(GeoDataLineString,QVector<QPointF>)),nullptr,nullptr); 0717 } 0718 m_activeDataSource = source; 0719 connect(m_activeDataSource, SIGNAL(dataUpdated(GeoDataLineString,QVector<QPointF>)), this, SLOT(handleDataUpdate(GeoDataLineString,QVector<QPointF>))); 0720 m_activeDataSource->requestUpdate(); 0721 } 0722 0723 } 0724 0725 #include "moc_ElevationProfileFloatItem.cpp"