/*************************************************************************** qgstemporalcontrollerwidget.cpp ------------------------------ begin : February 2020 copyright : (C) 2020 by Samweli Mwakisambwe email : samweli at kartoza dot com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgstemporalcontrollerwidget.h" #include "qgsgui.h" #include "qgsproject.h" #include "qgsprojecttimesettings.h" #include "qgstemporalmapsettingswidget.h" #include "qgstemporalutils.h" #include "qgsmaplayertemporalproperties.h" QgsTemporalControllerWidget::QgsTemporalControllerWidget( QWidget *parent ) : QgsPanelWidget( parent ) { setupUi( this ); mNavigationObject = new QgsTemporalNavigationObject( this ); connect( mForwardButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePlayForward ); connect( mBackButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePlayBackward ); connect( mStopButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePause ); connect( mNextButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::next ); connect( mPreviousButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::previous ); connect( mFastForwardButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::skipToEnd ); connect( mRewindButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::rewindToStart ); connect( mLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); } ); setWidgetStateFromNavigationMode( mNavigationObject->navigationMode() ); connect( mNavigationObject, &QgsTemporalNavigationObject::navigationModeChanged, this, &QgsTemporalControllerWidget::setWidgetStateFromNavigationMode ); connect( mNavigationOff, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationOff_clicked ); connect( mNavigationFixedRange, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationFixedRange_clicked ); connect( mNavigationAnimated, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationAnimated_clicked ); connect( mNavigationObject, &QgsTemporalNavigationObject::stateChanged, this, [ = ]( QgsTemporalNavigationObject::AnimationState state ) { mForwardButton->setChecked( state == QgsTemporalNavigationObject::Forward ); mBackButton->setChecked( state == QgsTemporalNavigationObject::Reverse ); mStopButton->setChecked( state == QgsTemporalNavigationObject::Idle ); } ); connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed ); connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed ); connect( mFixedRangeStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed ); connect( mFixedRangeEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed ); connect( mStepSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration ); connect( mTimeStepsComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration ); connect( mSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged ); mStepSpinBox->setClearValue( 1 ); connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider ); connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked ); connect( mSetToProjectTimeButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mSetToProjectTimeButton_clicked ); connect( mFixedRangeSetToProjectTimeButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mSetToProjectTimeButton_clicked ); connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation ); QgsDateTimeRange range; if ( QgsProject::instance()->timeSettings() ) range = QgsProject::instance()->timeSettings()->temporalRange(); mStartDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); mEndDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); mFixedRangeStartDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); mFixedRangeEndDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); if ( range.begin().isValid() && range.end().isValid() ) { whileBlocking( mStartDateTime )->setDateTime( range.begin() ); whileBlocking( mEndDateTime )->setDateTime( range.end() ); whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() ); whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() ); } QString setToProjectTimeButtonToolTip = tr( "Match time range to project. \n" "If a project has no explicit time range set, \n" "then the range will be calculated based on the \n" "minimum and maximum dates from any temporal-enabled layers." ); mSetToProjectTimeButton->setToolTip( setToProjectTimeButtonToolTip ); mFixedRangeSetToProjectTimeButton->setToolTip( setToProjectTimeButtonToolTip ); for ( QgsUnitTypes::TemporalUnit u : { QgsUnitTypes::TemporalMilliseconds, QgsUnitTypes::TemporalSeconds, QgsUnitTypes::TemporalMinutes, QgsUnitTypes::TemporalHours, QgsUnitTypes::TemporalDays, QgsUnitTypes::TemporalWeeks, QgsUnitTypes::TemporalMonths, QgsUnitTypes::TemporalYears, QgsUnitTypes::TemporalDecades, QgsUnitTypes::TemporalCenturies } ) { mTimeStepsComboBox->addItem( QgsUnitTypes::toString( u ), u ); } // TODO: might want to choose an appropriate default unit based on the range mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsUnitTypes::TemporalHours ) ); mStepSpinBox->setMinimum( 0.0000001 ); mStepSpinBox->setMaximum( std::numeric_limits::max() ); mStepSpinBox->setSingleStep( 1 ); mStepSpinBox->setValue( 1 ); mForwardButton->setToolTip( tr( "Play" ) ); mBackButton->setToolTip( tr( "Reverse" ) ); mNextButton->setToolTip( tr( "Go to next frame" ) ); mPreviousButton->setToolTip( tr( "Go to previous frame" ) ); mStopButton->setToolTip( tr( "Pause" ) ); mRewindButton->setToolTip( tr( "Rewind to start" ) ); mFastForwardButton->setToolTip( tr( "Fast forward to end" ) ); updateFrameDuration(); connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject ); connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded ); connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared ); } void QgsTemporalControllerWidget::keyPressEvent( QKeyEvent *e ) { if ( mSlider->hasFocus() && e->key() == Qt::Key_Space ) { togglePause(); } QWidget::keyPressEvent( e ); } void QgsTemporalControllerWidget::togglePlayForward() { mPlayingForward = true; if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Forward ) { mStopButton->setChecked( false ); mBackButton->setChecked( false ); mForwardButton->setChecked( true ); mNavigationObject->playForward(); } else { mBackButton->setChecked( true ); mForwardButton->setChecked( false ); mNavigationObject->pause(); } } void QgsTemporalControllerWidget::togglePlayBackward() { mPlayingForward = false; if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Reverse ) { mStopButton->setChecked( false ); mBackButton->setChecked( true ); mForwardButton->setChecked( false ); mNavigationObject->playBackward(); } else { mBackButton->setChecked( true ); mBackButton->setChecked( false ); mNavigationObject->pause(); } } void QgsTemporalControllerWidget::togglePause() { if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Idle ) { mStopButton->setChecked( true ); mBackButton->setChecked( false ); mForwardButton->setChecked( false ); mNavigationObject->pause(); } else { mBackButton->setChecked( mPlayingForward ? false : true ); mForwardButton->setChecked( mPlayingForward ? false : true ); if ( mPlayingForward ) { mNavigationObject->playForward(); } else { mNavigationObject->playBackward(); } } } void QgsTemporalControllerWidget::updateTemporalExtent() { QgsDateTimeRange temporalExtent = QgsDateTimeRange( mStartDateTime->dateTime(), mEndDateTime->dateTime() ); mNavigationObject->setTemporalExtents( temporalExtent ); mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 ); mSlider->setValue( 0 ); } void QgsTemporalControllerWidget::updateFrameDuration() { if ( mBlockSettingUpdates ) return; // save new settings into project QgsProject::instance()->timeSettings()->setTimeStepUnit( static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) ); QgsProject::instance()->timeSettings()->setTimeStep( mStepSpinBox->value() ); mNavigationObject->setFrameDuration( QgsInterval( QgsProject::instance()->timeSettings()->timeStep(), QgsProject::instance()->timeSettings()->timeStepUnit() ) ); mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 ); } void QgsTemporalControllerWidget::setWidgetStateFromProject() { mBlockSettingUpdates++; mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsProject::instance()->timeSettings()->timeStepUnit() ) ); mStepSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() ); mBlockSettingUpdates--; bool ok = false; QgsTemporalNavigationObject::NavigationMode mode = static_cast< QgsTemporalNavigationObject::NavigationMode>( QgsProject::instance()->readNumEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ), 0, &ok ) ); if ( ok ) { mNavigationObject->setNavigationMode( mode ); setWidgetStateFromNavigationMode( mode ); } else { mNavigationObject->setNavigationMode( QgsTemporalNavigationObject::NavigationOff ); setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff ); } const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) ); const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) ); if ( !startString.isEmpty() && !endString.isEmpty() ) { whileBlocking( mStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODate ) ); whileBlocking( mEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODate ) ); whileBlocking( mFixedRangeStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODate ) ); whileBlocking( mFixedRangeEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODate ) ); } else { setDatesToProjectTime(); } updateTemporalExtent(); updateFrameDuration(); mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() ); mNavigationObject->setTemporalRangeCumulative( QgsProject::instance()->timeSettings()->isTemporalRangeCumulative() ); } void QgsTemporalControllerWidget::mNavigationOff_clicked() { QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ), static_cast( QgsTemporalNavigationObject::NavigationOff ) ); mNavigationObject->setNavigationMode( QgsTemporalNavigationObject::NavigationOff ); setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff ); } void QgsTemporalControllerWidget::mNavigationFixedRange_clicked() { QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ), static_cast( QgsTemporalNavigationObject::FixedRange ) ); mNavigationObject->setNavigationMode( QgsTemporalNavigationObject::FixedRange ); setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::FixedRange ); } void QgsTemporalControllerWidget::mNavigationAnimated_clicked() { QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ), static_cast( QgsTemporalNavigationObject::Animated ) ); mNavigationObject->setNavigationMode( QgsTemporalNavigationObject::Animated ); setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::Animated ); } void QgsTemporalControllerWidget::setWidgetStateFromNavigationMode( const QgsTemporalNavigationObject::NavigationMode mode ) { mNavigationOff->setChecked( mode == QgsTemporalNavigationObject::NavigationOff ); mNavigationFixedRange->setChecked( mode == QgsTemporalNavigationObject::FixedRange ); mNavigationAnimated->setChecked( mode == QgsTemporalNavigationObject::Animated ); switch ( mode ) { case QgsTemporalNavigationObject::NavigationOff: mNavigationModeStackedWidget->setCurrentIndex( 0 ); break; case QgsTemporalNavigationObject::FixedRange: mNavigationModeStackedWidget->setCurrentIndex( 1 ); break; case QgsTemporalNavigationObject::Animated: mNavigationModeStackedWidget->setCurrentIndex( 2 ); break; } } void QgsTemporalControllerWidget::onLayersAdded( const QList &layers ) { if ( !mHasTemporalLayersLoaded ) { for ( QgsMapLayer *layer : layers ) { if ( layer->temporalProperties() ) { mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive(); if ( !mHasTemporalLayersLoaded ) { connect( layer, &QgsMapLayer::dataSourceChanged, this, [ this, layer ] { if ( layer->isValid() && layer->temporalProperties()->isActive() && !mHasTemporalLayersLoaded ) { mHasTemporalLayersLoaded = true; // if we are moving from zero temporal layers to non-zero temporal layers, let's set temporal extent this->setDatesToProjectTime(); } } ); } } } if ( mHasTemporalLayersLoaded ) setDatesToProjectTime(); } } void QgsTemporalControllerWidget::onProjectCleared() { mHasTemporalLayersLoaded = false; mNavigationObject->setNavigationMode( QgsTemporalNavigationObject::NavigationOff ); setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff ); whileBlocking( mStartDateTime )->setDateTime( QDateTime( QDate::currentDate(), QTime( 0, 0, 0, Qt::UTC ) ) ); whileBlocking( mEndDateTime )->setDateTime( mStartDateTime->dateTime() ); whileBlocking( mFixedRangeStartDateTime )->setDateTime( QDateTime( QDate::currentDate(), QTime( 0, 0, 0, Qt::UTC ) ) ); whileBlocking( mFixedRangeEndDateTime )->setDateTime( mStartDateTime->dateTime() ); updateTemporalExtent(); } void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range ) { whileBlocking( mSlider )->setValue( mNavigationObject->currentFrameNumber() ); updateRangeLabel( range ); } void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range ) { switch ( mNavigationObject->navigationMode() ) { case QgsTemporalNavigationObject::Animated: mCurrentRangeLabel->setText( tr( "Frame: %1 to %2" ).arg( range.begin().toString( "yyyy-MM-dd HH:mm:ss" ), range.end().toString( "yyyy-MM-dd HH:mm:ss" ) ) ); break; case QgsTemporalNavigationObject::FixedRange: mCurrentRangeLabel->setText( tr( "Range: %1 to %2" ).arg( range.begin().toString( "yyyy-MM-dd HH:mm:ss" ), range.end().toString( "yyyy-MM-dd HH:mm:ss" ) ) ); break; case QgsTemporalNavigationObject::NavigationOff: mCurrentRangeLabel->setText( tr( "Temporal navigation disabled" ) ); break; } } QgsTemporalController *QgsTemporalControllerWidget::temporalController() { return mNavigationObject; } void QgsTemporalControllerWidget::settings_clicked() { QgsTemporalMapSettingsWidget *settingsWidget = new QgsTemporalMapSettingsWidget( this ); settingsWidget->setFrameRateValue( mNavigationObject->framesPerSecond() ); settingsWidget->setIsTemporalRangeCumulative( mNavigationObject->temporalRangeCumulative() ); connect( settingsWidget, &QgsTemporalMapSettingsWidget::frameRateChanged, this, [ = ]( double rate ) { // save new settings into project QgsProject::instance()->timeSettings()->setFramesPerSecond( rate ); mNavigationObject->setFramesPerSecond( rate ); } ); connect( settingsWidget, &QgsTemporalMapSettingsWidget::temporalRangeCumulativeChanged, this, [ = ]( bool state ) { // save new settings into project QgsProject::instance()->timeSettings()->setIsTemporalRangeCumulative( state ); mNavigationObject->setTemporalRangeCumulative( state ); } ); openPanel( settingsWidget ); } void QgsTemporalControllerWidget::timeSlider_valueChanged( int value ) { mNavigationObject->setCurrentFrameNumber( value ); } void QgsTemporalControllerWidget::startEndDateTime_changed() { whileBlocking( mFixedRangeStartDateTime )->setDateTime( mStartDateTime->dateTime() ); whileBlocking( mFixedRangeEndDateTime )->setDateTime( mEndDateTime->dateTime() ); updateTemporalExtent(); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); } void QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed() { whileBlocking( mStartDateTime )->setDateTime( mFixedRangeStartDateTime->dateTime() ); whileBlocking( mEndDateTime )->setDateTime( mFixedRangeEndDateTime->dateTime() ); updateTemporalExtent(); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); } void QgsTemporalControllerWidget::mSetToProjectTimeButton_clicked() { setDatesToProjectTime(); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); } void QgsTemporalControllerWidget::setDatesToProjectTime() { QgsDateTimeRange range; // by default try taking the project's fixed temporal extent if ( QgsProject::instance()->timeSettings() ) range = QgsProject::instance()->timeSettings()->temporalRange(); // if that's not set, calculate the extent from the project's layers if ( !range.begin().isValid() || !range.end().isValid() ) { range = QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject::instance() ); } if ( range.begin().isValid() && range.end().isValid() ) { whileBlocking( mStartDateTime )->setDateTime( range.begin() ); whileBlocking( mEndDateTime )->setDateTime( range.end() ); whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() ); whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() ); updateTemporalExtent(); } } void QgsTemporalControllerWidget::setDateInputsEnable( bool enabled ) { mStartDateTime->setEnabled( enabled ); mEndDateTime->setEnabled( enabled ); } void QgsTemporalControllerWidget::updateButtonsEnable( bool enabled ) { mPreviousButton->setEnabled( enabled ); mNextButton->setEnabled( enabled ); mBackButton->setEnabled( enabled ); mForwardButton->setEnabled( enabled ); }