QGIS API Documentation 3.41.0-Master (45a0abf3bec)
Loading...
Searching...
No Matches
qgstemporalcontrollerwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstemporalcontrollerwidget.cpp
3 ------------------------------
4 begin : February 2020
5 copyright : (C) 2020 by Samweli Mwakisambwe
6 email : samweli at kartoza dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsapplication.h"
20#include "moc_qgstemporalcontrollerwidget.cpp"
21#include "qgsmaplayermodel.h"
22#include "qgsproject.h"
25#include "qgstemporalutils.h"
27#include "qgsmeshlayer.h"
28#include "qgsrasterlayer.h"
29#include "qgsunittypes.h"
30
31#include <QAction>
32#include <QMenu>
33#include <QRegularExpression>
34
36 : QgsPanelWidget( parent )
37{
38 setupUi( this );
39
40 mNavigationObject = new QgsTemporalNavigationObject( this );
41
42 mStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
43 mEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
44 mFixedRangeStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
45 mFixedRangeEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
46
47 auto handleOperation = [ = ]( Qgis::PlaybackOperation operation )
48 {
49 switch ( operation )
50 {
52 mNavigationObject->rewindToStart();
53 break;
54
56 mNavigationObject->previous();
57 break;
58
60 mNavigationObject->playBackward();
61 break;
62
64 mNavigationObject->pause();
65 break;
66
68 mNavigationObject->playForward();
69 break;
70
72 mNavigationObject->next();
73 break;
74
76 mNavigationObject->skipToEnd();
77 break;
78 }
79 };
80 connect( mAnimationController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
81 connect( mMovieController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
82
83 connect( mAnimationLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); mMovieLoopingCheckBox->setChecked( state ); } );
84 connect( mMovieLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); mAnimationLoopingCheckBox->setChecked( state ); } );
85
86 setWidgetStateFromNavigationMode( mNavigationObject->navigationMode() );
87 connect( mNavigationObject, &QgsTemporalNavigationObject::navigationModeChanged, this, &QgsTemporalControllerWidget::setWidgetStateFromNavigationMode );
88 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalExtentsChanged, this, &QgsTemporalControllerWidget::setDates );
89 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalFrameDurationChanged, this, [ = ]( const QgsInterval & timeStep )
90 {
91 if ( mBlockFrameDurationUpdates )
92 return;
93
94 mBlockFrameDurationUpdates++;
95 updateTimeStepInputs( timeStep );
96 mBlockFrameDurationUpdates--;
97 } );
98 connect( mNavigationOff, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationOff_clicked );
99 connect( mNavigationFixedRange, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationFixedRange_clicked );
100 connect( mNavigationAnimated, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationAnimated_clicked );
101 connect( mNavigationMovie, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationMovie_clicked );
102
103 connect( mNavigationObject, &QgsTemporalNavigationObject::stateChanged, this, [ = ]( Qgis::AnimationState state )
104 {
105 mAnimationController->setState( state );
106 mMovieController->setState( state );
107 } );
108
109 connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
110 connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
111 connect( mFixedRangeStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
112 connect( mFixedRangeEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
113 connect( mStepSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
114 connect( mTimeStepsComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
115 connect( mAnimationSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
116 connect( mMovieSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
117
118 connect( mTotalFramesSpinBox, qOverload<int>( &QSpinBox::valueChanged ), this, [ = ]( int frames )
119 {
120 mNavigationObject->setTotalMovieFrames( frames );
121 } );
122
123 mStepSpinBox->setClearValue( 1 );
124
125 connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider );
126 connect( mNavigationObject, &QgsTemporalNavigationObject::totalMovieFramesChanged, this, &QgsTemporalControllerWidget::totalMovieFramesChanged );
127
128 connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked );
129
130 mMapLayerModel = new QgsMapLayerModel( this );
131
132 mRangeMenu.reset( new QMenu( this ) );
133
134 mRangeSetToAllLayersAction = new QAction( tr( "Set to Full Range" ), mRangeMenu.get() );
135 mRangeSetToAllLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRefresh.svg" ) ) );
136 connect( mRangeSetToAllLayersAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered );
137 mRangeMenu->addAction( mRangeSetToAllLayersAction );
138
139 mRangeSetToProjectAction = new QAction( tr( "Set to Preset Project Range" ), mRangeMenu.get() );
140 connect( mRangeSetToProjectAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered );
141 mRangeMenu->addAction( mRangeSetToProjectAction );
142
143 mRangeMenu->addSeparator();
144
145 mRangeLayersSubMenu.reset( new QMenu( tr( "Set to Single Layer's Range" ), mRangeMenu.get() ) );
146 mRangeLayersSubMenu->setEnabled( false );
147 mRangeMenu->addMenu( mRangeLayersSubMenu.get() );
148 connect( mRangeMenu.get(), &QMenu::aboutToShow, this, &QgsTemporalControllerWidget::aboutToShowRangeMenu );
149
150 mSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
151 mSetRangeButton->setMenu( mRangeMenu.get() );
152 mSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
153 mFixedRangeSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
154 mFixedRangeSetRangeButton->setMenu( mRangeMenu.get() );
155 mFixedRangeSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
156
157 connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
158 connect( mExportMovieButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
159
160 QgsDateTimeRange range;
161
162 if ( QgsProject::instance()->timeSettings() )
164
165 if ( range.begin().isValid() && range.end().isValid() )
166 {
167 whileBlocking( mStartDateTime )->setDateTime( range.begin() );
168 whileBlocking( mEndDateTime )->setDateTime( range.end() );
169 whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
170 whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
171 }
172
173 for ( const Qgis::TemporalUnit u :
174 {
186 } )
187 {
188 mTimeStepsComboBox->addItem( u != Qgis::TemporalUnit::IrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), static_cast< int >( u ) );
189 }
190
191 // TODO: might want to choose an appropriate default unit based on the range
192 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Hours ) ) );
193
194 // NOTE 'minimum' and 'decimals' should be in sync with the 'decimals' in qgstemporalcontrollerwidgetbase.ui
195 mStepSpinBox->setDecimals( 3 );
196 // minimum timestep one millisecond
197 mStepSpinBox->setMinimum( 0.001 );
198 mStepSpinBox->setMaximum( std::numeric_limits<int>::max() );
199 mStepSpinBox->setSingleStep( 1 );
200 mStepSpinBox->setValue( 1 );
201
202 updateFrameDuration();
203
204 connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject );
205 connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded );
206 connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared );
207}
208
210{
211 return true;
212}
213
215{
216 if ( ( mAnimationSlider->hasFocus() || mMovieSlider->hasFocus() ) && e->key() == Qt::Key_Space )
217 {
218 mAnimationController->togglePause();
219 // connections will auto-sync mMovieController state!
220 }
222}
223
224void QgsTemporalControllerWidget::aboutToShowRangeMenu()
225{
226 QgsDateTimeRange projectRange;
227 if ( QgsProject::instance()->timeSettings() )
228 projectRange = QgsProject::instance()->timeSettings()->temporalRange();
229 mRangeSetToProjectAction->setEnabled( projectRange.begin().isValid() && projectRange.end().isValid() );
230
231 mRangeLayersSubMenu->clear();
232 for ( int i = 0; i < mMapLayerModel->rowCount(); ++i )
233 {
234 const QModelIndex index = mMapLayerModel->index( i, 0 );
235 QgsMapLayer *currentLayer = mMapLayerModel->data( index, static_cast< int >( QgsMapLayerModel::CustomRole::Layer ) ).value<QgsMapLayer *>();
236 if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
237 continue;
238
239 const QIcon icon = qvariant_cast<QIcon>( mMapLayerModel->data( index, Qt::DecorationRole ) );
240 const QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString();
241 const QgsDateTimeRange range = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
242 if ( range.begin().isValid() && range.end().isValid() )
243 {
244 QAction *action = new QAction( icon, text, mRangeLayersSubMenu.get() );
245 connect( action, &QAction::triggered, this, [ = ]
246 {
247 setDates( range );
248 saveRangeToProject();
249 } );
250 mRangeLayersSubMenu->addAction( action );
251 }
252 }
253 mRangeLayersSubMenu->setEnabled( !mRangeLayersSubMenu->actions().isEmpty() );
254}
255
256void QgsTemporalControllerWidget::updateTemporalExtent()
257{
258 // TODO - consider whether the overall time range set for animations should include the end date time or not.
259 // (currently it DOES include the end date time).
260 const QDateTime start = mStartDateTime->dateTime();
261 const QDateTime end = mEndDateTime->dateTime();
262 const bool isTimeInstant = start == end;
263 const QgsDateTimeRange temporalExtent = QgsDateTimeRange( start, end,
264 true, !isTimeInstant && mNavigationObject->navigationMode() == Qgis::TemporalNavigationMode::FixedRange ? false : true );
265 mNavigationObject->setTemporalExtents( temporalExtent );
266 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
267 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
268 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
269 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
270}
271
272void QgsTemporalControllerWidget::updateFrameDuration()
273{
274 if ( mBlockSettingUpdates )
275 return;
276
277 // save new settings into project
278 const Qgis::TemporalUnit unit = static_cast< Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() );
280 QgsProject::instance()->timeSettings()->setTimeStep( unit == Qgis::TemporalUnit::IrregularStep ? 1 : mStepSpinBox->value() );
281
282 if ( !mBlockFrameDurationUpdates )
283 {
284 mNavigationObject->setFrameDuration(
285 QgsInterval( QgsProject::instance()->timeSettings()->timeStep(),
286 QgsProject::instance()->timeSettings()->timeStepUnit() ) );
287 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
288 }
289 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
290 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
291 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
292 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
293
295 {
296 mStepSpinBox->setEnabled( false );
297 mStepSpinBox->setValue( 1 );
298 mAnimationSlider->setTickInterval( 1 );
299 mAnimationSlider->setTickPosition( QSlider::TicksBothSides );
300 }
301 else
302 {
303 mStepSpinBox->setEnabled( true );
304 mAnimationSlider->setTickInterval( 0 );
305 mAnimationSlider->setTickPosition( QSlider::NoTicks );
306 }
307}
308
309void QgsTemporalControllerWidget::setWidgetStateFromProject()
310{
311 mBlockSettingUpdates++;
312 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( QgsProject::instance()->timeSettings()->timeStepUnit() ) ) );
313 mStepSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() );
314 mBlockSettingUpdates--;
315
316 bool ok = false;
317 const Qgis::TemporalNavigationMode mode = static_cast< Qgis::TemporalNavigationMode>( QgsProject::instance()->readNumEntry( QStringLiteral( "TemporalControllerWidget" ),
318 QStringLiteral( "/NavigationMode" ), 0, &ok ) );
319 if ( ok )
320 {
321 mNavigationObject->setNavigationMode( mode );
322 setWidgetStateFromNavigationMode( mode );
323 }
324 else
325 {
327 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
328 }
329
330 mNavigationObject->setTotalMovieFrames( QgsProject::instance()->timeSettings()->totalMovieFrames() );
331 mTotalFramesSpinBox->setValue( QgsProject::instance()->timeSettings()->totalMovieFrames() );
332
333 const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) );
334 const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) );
335 if ( !startString.isEmpty() && !endString.isEmpty() )
336 {
337 whileBlocking( mStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
338 whileBlocking( mEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
339 whileBlocking( mFixedRangeStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
340 whileBlocking( mFixedRangeEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
341 }
342 else
343 {
344 setDatesToProjectTime( false );
345 }
346 updateTemporalExtent();
347 updateFrameDuration();
348
349 mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() );
350 mNavigationObject->setTemporalRangeCumulative( QgsProject::instance()->timeSettings()->isTemporalRangeCumulative() );
351}
352
353void QgsTemporalControllerWidget::mNavigationOff_clicked()
354{
355 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
356 static_cast<int>( Qgis::TemporalNavigationMode::Disabled ) );
357
359 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
360}
361
362void QgsTemporalControllerWidget::mNavigationFixedRange_clicked()
363{
364 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
365 static_cast<int>( Qgis::TemporalNavigationMode::FixedRange ) );
366
368 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::FixedRange );
369}
370
371void QgsTemporalControllerWidget::mNavigationAnimated_clicked()
372{
373 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
374 static_cast<int>( Qgis::TemporalNavigationMode::Animated ) );
375
377 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Animated );
378}
379
380void QgsTemporalControllerWidget::mNavigationMovie_clicked()
381{
382 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
383 static_cast<int>( Qgis::TemporalNavigationMode::Movie ) );
384
386 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Movie );
387}
388
389void QgsTemporalControllerWidget::setWidgetStateFromNavigationMode( const Qgis::TemporalNavigationMode mode )
390{
391 mNavigationOff->setChecked( mode == Qgis::TemporalNavigationMode::Disabled );
392 mNavigationFixedRange->setChecked( mode == Qgis::TemporalNavigationMode::FixedRange );
393 mNavigationAnimated->setChecked( mode == Qgis::TemporalNavigationMode::Animated );
394 mNavigationMovie->setChecked( mode == Qgis::TemporalNavigationMode::Movie );
395
396 switch ( mode )
397 {
399 mNavigationModeStackedWidget->setCurrentIndex( 0 );
400 break;
402 mNavigationModeStackedWidget->setCurrentIndex( 1 );
403 break;
405 mNavigationModeStackedWidget->setCurrentIndex( 2 );
406 break;
408 mNavigationModeStackedWidget->setCurrentIndex( 3 );
409 break;
410 }
411}
412
413void QgsTemporalControllerWidget::onLayersAdded( const QList<QgsMapLayer *> &layers )
414{
415 if ( !mHasTemporalLayersLoaded )
416 {
417 for ( QgsMapLayer *layer : layers )
418 {
419 if ( layer->temporalProperties() )
420 {
421 mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive();
422
423 if ( !mHasTemporalLayersLoaded )
424 {
425 connect( layer, &QgsMapLayer::dataSourceChanged, this, [ this, layer ]
426 {
427 if ( layer->isValid() && layer->temporalProperties()->isActive() && !mHasTemporalLayersLoaded )
428 {
429 mHasTemporalLayersLoaded = true;
430 firstTemporalLayerLoaded( layer );
431 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
432 }
433 } );
434 }
435
436 firstTemporalLayerLoaded( layer );
437 }
438 }
439 }
440
442}
443
444void QgsTemporalControllerWidget::firstTemporalLayerLoaded( QgsMapLayer *layer )
445{
446 setDatesToProjectTime( true );
447
448 if ( QgsMeshLayer *meshLayer = qobject_cast<QgsMeshLayer *>( layer ) )
449 {
450 mBlockFrameDurationUpdates++;
451 setTimeStep( meshLayer->firstValidTimeStep() );
452 mBlockFrameDurationUpdates--;
453 updateFrameDuration();
454 }
455 else if ( QgsRasterLayer *rasterLayer = qobject_cast<QgsRasterLayer *>( layer ) )
456 {
457 if ( rasterLayer->dataProvider() && rasterLayer->dataProvider()->temporalCapabilities() )
458 {
459 mBlockFrameDurationUpdates++;
460 setTimeStep( rasterLayer->dataProvider()->temporalCapabilities()->defaultInterval() );
461 mBlockFrameDurationUpdates--;
462 updateFrameDuration();
463 }
464 }
465}
466
467void QgsTemporalControllerWidget::onProjectCleared()
468{
469 mHasTemporalLayersLoaded = false;
470
472 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
473
474 // default to showing the last 24 hours, ending at the current date's hour, in one hour blocks...
475 // it's COMPLETELY arbitrary, but better than starting with a "zero length" duration!
476 const QTime startOfCurrentHour = QTime( QTime::currentTime().hour(), 0, 0 );
477 const QDateTime end = QDateTime( QDate::currentDate(), startOfCurrentHour, Qt::UTC );
478 const QDateTime start = end.addSecs( -24 * 60 * 60 );
479
480 whileBlocking( mStartDateTime )->setDateTime( start );
481 whileBlocking( mEndDateTime )->setDateTime( end );
482 whileBlocking( mFixedRangeStartDateTime )->setDateTime( start );
483 whileBlocking( mFixedRangeEndDateTime )->setDateTime( end );
484
485 updateTemporalExtent();
486 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Hours ) ) );
487 mStepSpinBox->setValue( 1 );
488}
489
490void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range )
491{
492 whileBlocking( mAnimationSlider )->setValue( mNavigationObject->currentFrameNumber() );
493 whileBlocking( mMovieSlider )->setValue( mNavigationObject->currentFrameNumber() );
494 updateRangeLabel( range );
495}
496
497void QgsTemporalControllerWidget::totalMovieFramesChanged( long long frames )
498{
500 mTotalFramesSpinBox->setValue( frames );
501 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( frames ) );
502}
503
504void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range )
505{
506 QString timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss" );
507 // but if timesteps are < 1 second (as: in milliseconds), add milliseconds to the format
508 if ( static_cast< Qgis::TemporalUnit >( mTimeStepsComboBox->currentData().toInt() ) == Qgis::TemporalUnit::Milliseconds )
509 timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss.zzz" );
510 switch ( mNavigationObject->navigationMode() )
511 {
513 mCurrentRangeLabel->setText( tr( "Current frame: %1 ≤ <i>t</i> &lt; %2" ).arg(
514 range.begin().toString( timeFrameFormat ),
515 range.end().toString( timeFrameFormat ) ) );
516 break;
518 mCurrentRangeLabel->setText( tr( "Range: %1 ≤ <i>t</i> &lt; %2" ).arg(
519 range.begin().toString( timeFrameFormat ),
520 range.end().toString( timeFrameFormat ) ) );
521 break;
523 mCurrentRangeLabel->setText( tr( "Temporal navigation disabled" ) );
524 break;
526 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( mNavigationObject->totalMovieFrames() ) );
527 break;
528 }
529}
530
535
536void QgsTemporalControllerWidget::settings_clicked()
537{
538 QgsTemporalMapSettingsWidget *settingsWidget = new QgsTemporalMapSettingsWidget( this );
539 settingsWidget->setFrameRateValue( mNavigationObject->framesPerSecond() );
540 settingsWidget->setIsTemporalRangeCumulative( mNavigationObject->temporalRangeCumulative() );
541
542 connect( settingsWidget, &QgsTemporalMapSettingsWidget::frameRateChanged, this, [ = ]( double rate )
543 {
544 // save new settings into project
546 mNavigationObject->setFramesPerSecond( rate );
547 } );
548
549 connect( settingsWidget, &QgsTemporalMapSettingsWidget::temporalRangeCumulativeChanged, this, [ = ]( bool state )
550 {
551 // save new settings into project
553 mNavigationObject->setTemporalRangeCumulative( state );
554 } );
555 openPanel( settingsWidget );
556}
557
558void QgsTemporalControllerWidget::timeSlider_valueChanged( int value )
559{
560 mNavigationObject->setCurrentFrameNumber( value );
561}
562
563void QgsTemporalControllerWidget::startEndDateTime_changed()
564{
565 whileBlocking( mFixedRangeStartDateTime )->setDateTime( mStartDateTime->dateTime() );
566 whileBlocking( mFixedRangeEndDateTime )->setDateTime( mEndDateTime->dateTime() );
567
568 updateTemporalExtent();
569 saveRangeToProject();
570}
571
572void QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed()
573{
574 whileBlocking( mStartDateTime )->setDateTime( mFixedRangeStartDateTime->dateTime() );
575 whileBlocking( mEndDateTime )->setDateTime( mFixedRangeEndDateTime->dateTime() );
576
577 updateTemporalExtent();
578 saveRangeToProject();
579}
580
581void QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered()
582{
583 setDatesToAllLayers();
584 saveRangeToProject();
585}
586
587void QgsTemporalControllerWidget::setTimeStep( const QgsInterval &timeStep )
588{
589 if ( ! timeStep.isValid() || timeStep.seconds() <= 0 )
590 return;
591
592 int selectedUnit = -1;
593 double selectedValue = std::numeric_limits<double>::max();
595 {
596 // Search the time unit the most appropriate :
597 // the one that gives the smallest time step value for double spin box with round value (if possible) and/or the less signifiant digits
598
599 int stringSize = std::numeric_limits<int>::max();
600 const int precision = mStepSpinBox->decimals();
601 for ( int i = 0; i < mTimeStepsComboBox->count(); ++i )
602 {
603 const Qgis::TemporalUnit unit = static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->itemData( i ).toInt() );
604 const double value = timeStep.seconds() * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Seconds, unit );
605 QString string = QString::number( value, 'f', precision );
606
607 const thread_local QRegularExpression trailingZeroRegEx = QRegularExpression( QStringLiteral( "0+$" ) );
608 //remove trailing zero
609 string.remove( trailingZeroRegEx );
610
611 const thread_local QRegularExpression trailingPointRegEx = QRegularExpression( QStringLiteral( "[.]+$" ) );
612 //remove last point if present
613 string.remove( trailingPointRegEx );
614
615 if ( value >= 1
616 && string.size() <= stringSize // less significant digit than currently selected
617 && value < selectedValue ) // less than currently selected
618 {
619 selectedUnit = i;
620 selectedValue = value;
621 stringSize = string.size();
622 }
623 else if ( string != '0'
624 && string.size() < precision + 2 //round value (ex: 0.xx with precision=3)
625 && string.size() < stringSize ) //less significant digit than currently selected
626 {
627 selectedUnit = i ;
628 selectedValue = value ;
629 stringSize = string.size();
630 }
631 }
632 }
633 else
634 {
635 selectedUnit = mTimeStepsComboBox->findData( static_cast< int >( timeStep.originalUnit() ) );
636 selectedValue = 1;
637 }
638
639 if ( selectedUnit >= 0 )
640 {
641 mStepSpinBox->setValue( selectedValue );
642 mTimeStepsComboBox->setCurrentIndex( selectedUnit );
643 }
644
645 updateFrameDuration();
646}
647
648void QgsTemporalControllerWidget::updateTimeStepInputs( const QgsInterval &timeStep )
649{
650 if ( ! timeStep.isValid() || timeStep.seconds() <= 0.0001 )
651 return;
652
653 QString timeDisplayFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss" );
655 {
656 timeDisplayFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss.zzz" );
657 // very big change that you have to update the range too, as defaulting to NOT handling millis
658 updateTemporalExtent();
659 }
660 mStartDateTime->setDisplayFormat( timeDisplayFormat );
661 mEndDateTime->setDisplayFormat( timeDisplayFormat );
662 mFixedRangeStartDateTime->setDisplayFormat( timeDisplayFormat );
663 mFixedRangeEndDateTime->setDisplayFormat( timeDisplayFormat );
664
665 // Only update ui when the intervals are different
666 if ( timeStep == QgsInterval( mStepSpinBox->value(),
667 static_cast< Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) ) )
668 return;
669
670 if ( timeStep.originalUnit() != Qgis::TemporalUnit::Unknown )
671 {
672 mStepSpinBox->setValue( timeStep.originalDuration() );
673 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( timeStep.originalUnit() ) ) );
674 }
675
676 updateFrameDuration();
677}
678
679void QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered()
680{
681 setDatesToProjectTime( false );
682 saveRangeToProject();
683}
684
685void QgsTemporalControllerWidget::setDates( const QgsDateTimeRange &range )
686{
687 if ( range.begin().isValid() && range.end().isValid() )
688 {
689 whileBlocking( mStartDateTime )->setDateTime( range.begin() );
690 whileBlocking( mEndDateTime )->setDateTime( range.end() );
691 whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
692 whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
693 updateTemporalExtent();
694 }
695}
696
697void QgsTemporalControllerWidget::setDatesToAllLayers()
698{
699 QgsDateTimeRange range;
702
703 setDates( range );
704}
705
706void QgsTemporalControllerWidget::setDatesToProjectTime( bool tryLastStoredRange )
707{
708 QgsDateTimeRange range;
709
710 if ( tryLastStoredRange )
711 {
712 const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) );
713 const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) );
714 if ( !startString.isEmpty() && !endString.isEmpty() )
715 {
716 range = QgsDateTimeRange( QDateTime::fromString( startString, Qt::ISODateWithMs ),
717 QDateTime::fromString( endString, Qt::ISODateWithMs ) );
718 }
719 }
720
721 // by default try taking the project's fixed temporal extent
722 if ( ( !range.begin().isValid() || !range.end().isValid() ) && QgsProject::instance()->timeSettings() )
724
725 // if that's not set, calculate the extent from the project's layers
726 if ( !range.begin().isValid() || !range.end().isValid() )
727 {
729 }
730
732
733 setDates( range );
734}
735
736void QgsTemporalControllerWidget::saveRangeToProject()
737{
738 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
739 QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
740 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
741 QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
742}
TemporalNavigationMode
Temporal navigation modes.
Definition qgis.h:2366
@ Animated
Temporal navigation relies on frames within a datetime range.
@ Movie
Movie mode – behaves like a video player, with a fixed frame duration and no temporal range.
@ FixedRange
Temporal navigation relies on a fixed datetime range.
@ Disabled
Temporal navigation is disabled.
PlaybackOperation
Media playback operations.
Definition qgis.h:2395
@ Pause
Pause playback.
@ PlayReverse
Play in reverse.
@ PlayForward
Play forward.
@ SkipToStart
Jump to start of playback.
@ PreviousFrame
Step to previous frame.
@ SkipToEnd
Jump to end of playback.
@ NextFrame
Step to next frame.
TemporalUnit
Temporal units.
Definition qgis.h:4823
@ IrregularStep
Special 'irregular step' time unit, used for temporal data which uses irregular, non-real-world unit ...
@ Milliseconds
Milliseconds.
@ Unknown
Unknown time unit.
@ Centuries
Centuries.
AnimationState
Animation states.
Definition qgis.h:2382
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A representation of the interval between two datetime values.
Definition qgsinterval.h:46
double originalDuration() const
Returns the original interval duration.
bool isValid() const
Returns true if the interval is valid.
double seconds() const
Returns the interval duration in seconds.
Qgis::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
The QgsMapLayerModel class is a model to display layers in widgets.
int rowCount(const QModelIndex &parent=QModelIndex()) const override
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
@ Layer
Stores pointer to the map layer itself.
virtual QgsDateTimeRange calculateTemporalExtent(QgsMapLayer *layer) const
Attempts to calculate the overall temporal extent for the specified layer, using the settings defined...
Base class for all map layer types.
Definition qgsmaplayer.h:76
void dataSourceChanged()
Emitted whenever the layer's data source has been changed.
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
Represents a mesh layer supporting display of data on structured or unstructured meshes.
Base class for any widget that can be shown as a inline panel.
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
void keyPressEvent(QKeyEvent *event) override
Overridden key press event to handle the esc event on the widget.
void operationTriggered(Qgis::PlaybackOperation operation)
Emitted when a playback operation is triggered.
QgsDateTimeRange temporalRange() const
Returns the project's temporal range, which indicates the earliest and latest datetime ranges associa...
void setTotalMovieFrames(long long frames)
Sets the total number of frames for the movie.
void setFramesPerSecond(double rate)
Sets the project's default animation frame rate, in frames per second.
Qgis::TemporalUnit timeStepUnit() const
Returns the project's time step (length of one animation frame) unit, which is used as the default va...
void setTimeStepUnit(Qgis::TemporalUnit unit)
Sets the project's time step (length of one animation frame) unit, which is used as the default value...
void setIsTemporalRangeCumulative(bool state)
Sets the project's temporal range as cumulative in animation settings.
void setTimeStep(double step)
Sets the project's time step (length of one animation frame), which is used as the default value when...
int readNumEntry(const QString &scope, const QString &key, int def=0, bool *ok=nullptr) const
Reads an integer from the specified scope and key.
static QgsProject * instance()
Returns the QgsProject singleton instance.
void cleared()
Emitted when the project is cleared (and additionally when an open project is cleared just before a n...
QString readEntry(const QString &scope, const QString &key, const QString &def=QString(), bool *ok=nullptr) const
Reads a string from the specified scope and key.
bool writeEntry(const QString &scope, const QString &key, bool value)
Write a boolean value to the project file.
void readProject(const QDomDocument &document)
Emitted when a project is being read.
void layersAdded(const QList< QgsMapLayer * > &layers)
Emitted when one or more layers were added to the registry.
const QgsProjectTimeSettings * timeSettings() const
Returns the project's time settings, which contains the project's temporal range and other time based...
Represents a raster layer.
bool applySizeConstraintsToStack() const override
Returns true if the size constraints and hints for the panel widget should be applied to the parent Q...
void exportAnimation()
Triggered when an animation should be exported.
void keyPressEvent(QKeyEvent *e) override
QgsTemporalNavigationObject * temporalController()
Returns the temporal controller object used by this object in navigation.
QgsTemporalControllerWidget(QWidget *parent=nullptr)
Constructor for QgsTemporalControllerWidget, with the specified parent widget.
void updateTemporalRange(const QgsDateTimeRange &range)
Signals that a temporal range has changed and needs to be updated in all connected objects.
Implements a temporal controller based on a frame by frame navigation and animation.
void stateChanged(Qgis::AnimationState state)
Emitted whenever the animation state changes.
long long totalMovieFrames() const
Returns the total number of frames for the movie.
void previous()
Jumps back to the previous frame.
double framesPerSecond() const
Returns the animation frame rate, in frames per second.
void setAvailableTemporalRanges(const QList< QgsDateTimeRange > &ranges)
Sets the list of all available temporal ranges which have data available.
void setFrameDuration(const QgsInterval &duration)
Sets the frame duration, which dictates the temporal length of each frame in the animation.
void navigationModeChanged(Qgis::TemporalNavigationMode mode)
Emitted whenever the navigation mode changes.
void setNavigationMode(const Qgis::TemporalNavigationMode mode)
Sets the temporal navigation mode.
void playForward()
Starts the animation playing in a forward direction up till the end of all frames.
long long currentFrameNumber() const
Returns the current frame number.
void rewindToStart()
Rewinds the temporal navigation to start of the temporal extent.
void pause()
Pauses the temporal navigation.
void setCurrentFrameNumber(long long frame)
Sets the current animation frame number.
long long totalFrameCount() const
Returns the total number of frames for the navigation.
void skipToEnd()
Skips the temporal navigation to end of the temporal extent.
void temporalFrameDurationChanged(const QgsInterval &interval)
Emitted whenever the frameDuration interval of the controller changes.
void setFramesPerSecond(double rate)
Sets the animation frame rate, in frames per second.
bool temporalRangeCumulative() const
Returns the animation temporal range cumulative settings.
void next()
Advances to the next frame.
void totalMovieFramesChanged(long long frames)
Emitted whenever the total number of frames in the movie is changed.
void setTotalMovieFrames(long long frames)
Sets the total number of frames for the movie.
void setTemporalExtents(const QgsDateTimeRange &extents)
Sets the navigation temporal extents, which dictate the earliest and latest date time possible in the...
Qgis::TemporalNavigationMode navigationMode() const
Returns the current temporal navigation mode.
void setTemporalRangeCumulative(bool state)
Sets the animation temporal range as cumulative.
void setLooping(bool loop)
Sets whether the animation should loop after hitting the end or start frame.
void playBackward()
Starts the animation playing in a reverse direction until the beginning of the time range.
void temporalExtentsChanged(const QgsDateTimeRange &extent)
Emitted whenever the temporalExtent extent changes.
bool isActive() const
Returns true if the temporal property is active.
T begin() const
Returns the beginning of the range.
Definition qgsrange.h:444
T end() const
Returns the upper bound of the range.
Definition qgsrange.h:451
static QgsDateTimeRange calculateTemporalRangeForProject(QgsProject *project)
Calculates the temporal range for a project.
static QList< QgsDateTimeRange > usedTemporalRangesForProject(QgsProject *project)
Calculates all temporal ranges which are in use for a project.
static Q_INVOKABLE QString toString(Qgis::DistanceUnit unit)
Returns a translated string representing a distance unit.
static Q_INVOKABLE double fromUnitToUnitFactor(Qgis::DistanceUnit fromUnit, Qgis::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition qgis.h:5862
QgsTemporalRange< QDateTime > QgsDateTimeRange
QgsRange which stores a range of date times.
Definition qgsrange.h:742
int precision