QGIS API Documentation  3.25.0-Master (6b426f5f8a)
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 "qgsgui.h"
21 #include "qgsmaplayermodel.h"
22 #include "qgsproject.h"
23 #include "qgsprojecttimesettings.h"
25 #include "qgstemporalutils.h"
27 #include "qgsmeshlayer.h"
28 #include "qgsrasterlayer.h"
29 
30 #include <QAction>
31 #include <QMenu>
32 #include <QRegularExpression>
33 
35  : QgsPanelWidget( parent )
36 {
37  setupUi( this );
38 
39  mNavigationObject = new QgsTemporalNavigationObject( this );
40 
41  mStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
42  mEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
43  mFixedRangeStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
44  mFixedRangeEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
45 
46  connect( mForwardButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePlayForward );
47  connect( mBackButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePlayBackward );
48  connect( mStopButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::togglePause );
49  connect( mNextButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::next );
50  connect( mPreviousButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::previous );
51  connect( mFastForwardButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::skipToEnd );
52  connect( mRewindButton, &QPushButton::clicked, mNavigationObject, &QgsTemporalNavigationObject::rewindToStart );
53  connect( mLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); } );
54 
55  setWidgetStateFromNavigationMode( mNavigationObject->navigationMode() );
56  connect( mNavigationObject, &QgsTemporalNavigationObject::navigationModeChanged, this, &QgsTemporalControllerWidget::setWidgetStateFromNavigationMode );
57  connect( mNavigationObject, &QgsTemporalNavigationObject::temporalExtentsChanged, this, &QgsTemporalControllerWidget::setDates );
58  connect( mNavigationObject, &QgsTemporalNavigationObject::temporalFrameDurationChanged, this, [ = ]( const QgsInterval & timeStep )
59  {
60  if ( mBlockFrameDurationUpdates )
61  return;
62 
63  mBlockFrameDurationUpdates++;
64  updateTimeStepInputs( timeStep );
65  mBlockFrameDurationUpdates--;
66  } );
67  connect( mNavigationOff, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationOff_clicked );
68  connect( mNavigationFixedRange, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationFixedRange_clicked );
69  connect( mNavigationAnimated, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationAnimated_clicked );
70 
71  connect( mNavigationObject, &QgsTemporalNavigationObject::stateChanged, this, [ = ]( QgsTemporalNavigationObject::AnimationState state )
72  {
73  mForwardButton->setChecked( state == QgsTemporalNavigationObject::Forward );
74  mBackButton->setChecked( state == QgsTemporalNavigationObject::Reverse );
75  mStopButton->setChecked( state == QgsTemporalNavigationObject::Idle );
76  } );
77 
78  connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
79  connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
80  connect( mFixedRangeStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
81  connect( mFixedRangeEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
82  connect( mStepSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
83  connect( mTimeStepsComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
84  connect( mSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
85 
86  mStepSpinBox->setClearValue( 1 );
87 
88  connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider );
89 
90  connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked );
91 
92  mMapLayerModel = new QgsMapLayerModel( this );
93 
94  mRangeMenu.reset( new QMenu( this ) );
95 
96  mRangeSetToAllLayersAction = new QAction( tr( "Set to Full Range" ), mRangeMenu.get() );
97  mRangeSetToAllLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRefresh.svg" ) ) );
98  connect( mRangeSetToAllLayersAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered );
99  mRangeMenu->addAction( mRangeSetToAllLayersAction );
100 
101  mRangeSetToProjectAction = new QAction( tr( "Set to Preset Project Range" ), mRangeMenu.get() );
102  connect( mRangeSetToProjectAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered );
103  mRangeMenu->addAction( mRangeSetToProjectAction );
104 
105  mRangeMenu->addSeparator();
106 
107  mRangeLayersSubMenu.reset( new QMenu( tr( "Set to Single Layer's Range" ), mRangeMenu.get() ) );
108  mRangeLayersSubMenu->setEnabled( false );
109  mRangeMenu->addMenu( mRangeLayersSubMenu.get() );
110  connect( mRangeMenu.get(), &QMenu::aboutToShow, this, &QgsTemporalControllerWidget::aboutToShowRangeMenu );
111 
112  mSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
113  mSetRangeButton->setMenu( mRangeMenu.get() );
114  mSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
115  mFixedRangeSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
116  mFixedRangeSetRangeButton->setMenu( mRangeMenu.get() );
117  mFixedRangeSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
118 
119  connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
120 
121  QgsDateTimeRange range;
122 
123  if ( QgsProject::instance()->timeSettings() )
125 
126  if ( range.begin().isValid() && range.end().isValid() )
127  {
128  whileBlocking( mStartDateTime )->setDateTime( range.begin() );
129  whileBlocking( mEndDateTime )->setDateTime( range.end() );
130  whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
131  whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
132  }
133 
134  for ( const QgsUnitTypes::TemporalUnit u :
135  {
147  } )
148  {
149  mTimeStepsComboBox->addItem( u != QgsUnitTypes::TemporalIrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), u );
150  }
151 
152  // TODO: might want to choose an appropriate default unit based on the range
153  mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsUnitTypes::TemporalHours ) );
154 
155  // NOTE 'minimum' and 'decimals' should be in sync with the 'decimals' in qgstemporalcontrollerwidgetbase.ui
156  mStepSpinBox->setDecimals( 3 );
157  // minimum timestep one millisecond
158  mStepSpinBox->setMinimum( 0.001 );
159  mStepSpinBox->setMaximum( std::numeric_limits<int>::max() );
160  mStepSpinBox->setSingleStep( 1 );
161  mStepSpinBox->setValue( 1 );
162 
163  mForwardButton->setToolTip( tr( "Play" ) );
164  mBackButton->setToolTip( tr( "Reverse" ) );
165  mNextButton->setToolTip( tr( "Go to next frame" ) );
166  mPreviousButton->setToolTip( tr( "Go to previous frame" ) );
167  mStopButton->setToolTip( tr( "Pause" ) );
168  mRewindButton->setToolTip( tr( "Rewind to start" ) );
169  mFastForwardButton->setToolTip( tr( "Fast forward to end" ) );
170 
171  updateFrameDuration();
172 
173  connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject );
174  connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded );
175  connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared );
176 }
177 
179 {
180  return true;
181 }
182 
184 {
185  if ( mSlider->hasFocus() && e->key() == Qt::Key_Space )
186  {
187  togglePause();
188  }
189  QWidget::keyPressEvent( e );
190 }
191 
192 void QgsTemporalControllerWidget::aboutToShowRangeMenu()
193 {
194  QgsDateTimeRange projectRange;
195  if ( QgsProject::instance()->timeSettings() )
196  projectRange = QgsProject::instance()->timeSettings()->temporalRange();
197  mRangeSetToProjectAction->setEnabled( projectRange.begin().isValid() && projectRange.end().isValid() );
198 
199  mRangeLayersSubMenu->clear();
200  for ( int i = 0; i < mMapLayerModel->rowCount(); ++i )
201  {
202  const QModelIndex index = mMapLayerModel->index( i, 0 );
203  QgsMapLayer *currentLayer = mMapLayerModel->data( index, QgsMapLayerModel::LayerRole ).value<QgsMapLayer *>();
204  if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
205  continue;
206 
207  const QIcon icon = qvariant_cast<QIcon>( mMapLayerModel->data( index, Qt::DecorationRole ) );
208  const QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString();
209  const QgsDateTimeRange range = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
210  if ( range.begin().isValid() && range.end().isValid() )
211  {
212  QAction *action = new QAction( icon, text, mRangeLayersSubMenu.get() );
213  connect( action, &QAction::triggered, this, [ = ]
214  {
215  setDates( range );
216  saveRangeToProject();
217  } );
218  mRangeLayersSubMenu->addAction( action );
219  }
220  }
221  mRangeLayersSubMenu->setEnabled( !mRangeLayersSubMenu->actions().isEmpty() );
222 }
223 
224 void QgsTemporalControllerWidget::togglePlayForward()
225 {
226  mPlayingForward = true;
227 
228  if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Forward )
229  {
230  mStopButton->setChecked( false );
231  mBackButton->setChecked( false );
232  mForwardButton->setChecked( true );
233  mNavigationObject->playForward();
234  }
235  else
236  {
237  mBackButton->setChecked( true );
238  mForwardButton->setChecked( false );
239  mNavigationObject->pause();
240  }
241 }
242 
243 void QgsTemporalControllerWidget::togglePlayBackward()
244 {
245  mPlayingForward = false;
246 
247  if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Reverse )
248  {
249  mStopButton->setChecked( false );
250  mBackButton->setChecked( true );
251  mForwardButton->setChecked( false );
252  mNavigationObject->playBackward();
253  }
254  else
255  {
256  mBackButton->setChecked( true );
257  mBackButton->setChecked( false );
258  mNavigationObject->pause();
259  }
260 }
261 
262 void QgsTemporalControllerWidget::togglePause()
263 {
264  if ( mNavigationObject->animationState() != QgsTemporalNavigationObject::Idle )
265  {
266  mStopButton->setChecked( true );
267  mBackButton->setChecked( false );
268  mForwardButton->setChecked( false );
269  mNavigationObject->pause();
270  }
271  else
272  {
273  mBackButton->setChecked( mPlayingForward ? false : true );
274  mForwardButton->setChecked( mPlayingForward ? false : true );
275  if ( mPlayingForward )
276  {
277  mNavigationObject->playForward();
278  }
279  else
280  {
281  mNavigationObject->playBackward();
282  }
283  }
284 }
285 
286 void QgsTemporalControllerWidget::updateTemporalExtent()
287 {
288  // TODO - consider whether the overall time range set for animations should include the end date time or not.
289  // (currently it DOES include the end date time).
290  const QDateTime start = mStartDateTime->dateTime();
291  const QDateTime end = mEndDateTime->dateTime();
292  const bool isTimeInstant = start == end;
293  const QgsDateTimeRange temporalExtent = QgsDateTimeRange( start, end,
294  true, !isTimeInstant && mNavigationObject->navigationMode() == QgsTemporalNavigationObject::FixedRange ? false : true );
295  mNavigationObject->setTemporalExtents( temporalExtent );
296  mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
297  mSlider->setValue( mNavigationObject->currentFrameNumber() );
298 }
299 
300 void QgsTemporalControllerWidget::updateFrameDuration()
301 {
302  if ( mBlockSettingUpdates )
303  return;
304 
305  // save new settings into project
306  const QgsUnitTypes::TemporalUnit unit = static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() );
308  QgsProject::instance()->timeSettings()->setTimeStep( unit == QgsUnitTypes::TemporalIrregularStep ? 1 : mStepSpinBox->value() );
309 
310  if ( !mBlockFrameDurationUpdates )
311  {
312  mNavigationObject->setFrameDuration(
313  QgsInterval( QgsProject::instance()->timeSettings()->timeStep(),
314  QgsProject::instance()->timeSettings()->timeStepUnit() ) );
315  mSlider->setValue( mNavigationObject->currentFrameNumber() );
316  }
317  mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
318  mSlider->setValue( mNavigationObject->currentFrameNumber() );
319 
321  {
322  mStepSpinBox->setEnabled( false );
323  mStepSpinBox->setValue( 1 );
324  mSlider->setTickInterval( 1 );
325  mSlider->setTickPosition( QSlider::TicksBothSides );
326  }
327  else
328  {
329  mStepSpinBox->setEnabled( true );
330  mSlider->setTickInterval( 0 );
331  mSlider->setTickPosition( QSlider::NoTicks );
332  }
333 }
334 
335 void QgsTemporalControllerWidget::setWidgetStateFromProject()
336 {
337  mBlockSettingUpdates++;
338  mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsProject::instance()->timeSettings()->timeStepUnit() ) );
339  mStepSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() );
340  mBlockSettingUpdates--;
341 
342  bool ok = false;
343  const QgsTemporalNavigationObject::NavigationMode mode = static_cast< QgsTemporalNavigationObject::NavigationMode>( QgsProject::instance()->readNumEntry( QStringLiteral( "TemporalControllerWidget" ),
344  QStringLiteral( "/NavigationMode" ), 0, &ok ) );
345  if ( ok )
346  {
347  mNavigationObject->setNavigationMode( mode );
348  setWidgetStateFromNavigationMode( mode );
349  }
350  else
351  {
353  setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff );
354  }
355 
356  const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) );
357  const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) );
358  if ( !startString.isEmpty() && !endString.isEmpty() )
359  {
360  whileBlocking( mStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
361  whileBlocking( mEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
362  whileBlocking( mFixedRangeStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
363  whileBlocking( mFixedRangeEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
364  }
365  else
366  {
367  setDatesToProjectTime();
368  }
369  updateTemporalExtent();
370  updateFrameDuration();
371 
372  mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() );
373  mNavigationObject->setTemporalRangeCumulative( QgsProject::instance()->timeSettings()->isTemporalRangeCumulative() );
374 }
375 
376 void QgsTemporalControllerWidget::mNavigationOff_clicked()
377 {
378  QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
379  static_cast<int>( QgsTemporalNavigationObject::NavigationOff ) );
380 
382  setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff );
383 }
384 
385 void QgsTemporalControllerWidget::mNavigationFixedRange_clicked()
386 {
387  QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
388  static_cast<int>( QgsTemporalNavigationObject::FixedRange ) );
389 
391  setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::FixedRange );
392 }
393 
394 void QgsTemporalControllerWidget::mNavigationAnimated_clicked()
395 {
396  QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
397  static_cast<int>( QgsTemporalNavigationObject::Animated ) );
398 
400  setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::Animated );
401 }
402 
403 void QgsTemporalControllerWidget::setWidgetStateFromNavigationMode( const QgsTemporalNavigationObject::NavigationMode mode )
404 {
405  mNavigationOff->setChecked( mode == QgsTemporalNavigationObject::NavigationOff );
406  mNavigationFixedRange->setChecked( mode == QgsTemporalNavigationObject::FixedRange );
407  mNavigationAnimated->setChecked( mode == QgsTemporalNavigationObject::Animated );
408 
409  switch ( mode )
410  {
412  mNavigationModeStackedWidget->setCurrentIndex( 0 );
413  break;
415  mNavigationModeStackedWidget->setCurrentIndex( 1 );
416  break;
418  mNavigationModeStackedWidget->setCurrentIndex( 2 );
419  break;
420  }
421 }
422 
423 void QgsTemporalControllerWidget::onLayersAdded( const QList<QgsMapLayer *> &layers )
424 {
425  if ( !mHasTemporalLayersLoaded )
426  {
427  for ( QgsMapLayer *layer : layers )
428  {
429  if ( layer->temporalProperties() )
430  {
431  mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive();
432 
433  if ( !mHasTemporalLayersLoaded )
434  {
435  connect( layer, &QgsMapLayer::dataSourceChanged, this, [ this, layer ]
436  {
437  if ( layer->isValid() && layer->temporalProperties()->isActive() && !mHasTemporalLayersLoaded )
438  {
439  mHasTemporalLayersLoaded = true;
440  firstTemporalLayerLoaded( layer );
441  mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
442  }
443  } );
444  }
445 
446  firstTemporalLayerLoaded( layer );
447  }
448  }
449  }
450 
452 }
453 
454 void QgsTemporalControllerWidget::firstTemporalLayerLoaded( QgsMapLayer *layer )
455 {
456  setDatesToProjectTime();
457 
458  if ( QgsMeshLayer *meshLayer = qobject_cast<QgsMeshLayer *>( layer ) )
459  {
460  mBlockFrameDurationUpdates++;
461  setTimeStep( meshLayer->firstValidTimeStep() );
462  mBlockFrameDurationUpdates--;
463  updateFrameDuration();
464  }
465  else if ( QgsRasterLayer *rasterLayer = qobject_cast<QgsRasterLayer *>( layer ) )
466  {
467  if ( rasterLayer->dataProvider() && rasterLayer->dataProvider()->temporalCapabilities() )
468  {
469  mBlockFrameDurationUpdates++;
470  setTimeStep( rasterLayer->dataProvider()->temporalCapabilities()->defaultInterval() );
471  mBlockFrameDurationUpdates--;
472  updateFrameDuration();
473  }
474  }
475 }
476 
477 void QgsTemporalControllerWidget::onProjectCleared()
478 {
479  mHasTemporalLayersLoaded = false;
480 
482  setWidgetStateFromNavigationMode( QgsTemporalNavigationObject::NavigationOff );
483 
484  // default to showing the last 24 hours, ending at the current date's hour, in one hour blocks...
485  // it's COMPLETELY arbitrary, but better than starting with a "zero length" duration!
486  const QTime startOfCurrentHour = QTime( QTime::currentTime().hour(), 0, 0 );
487  const QDateTime end = QDateTime( QDate::currentDate(), startOfCurrentHour, Qt::UTC );
488  const QDateTime start = end.addSecs( -24 * 60 * 60 );
489 
490  whileBlocking( mStartDateTime )->setDateTime( start );
491  whileBlocking( mEndDateTime )->setDateTime( end );
492  whileBlocking( mFixedRangeStartDateTime )->setDateTime( start );
493  whileBlocking( mFixedRangeEndDateTime )->setDateTime( end );
494 
495  updateTemporalExtent();
496  mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsUnitTypes::TemporalHours ) );
497  mStepSpinBox->setValue( 1 );
498 }
499 
500 void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range )
501 {
502  whileBlocking( mSlider )->setValue( mNavigationObject->currentFrameNumber() );
503  updateRangeLabel( range );
504 }
505 
506 void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range )
507 {
508  QString timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss" );
509  // but if timesteps are < 1 second (as: in milliseconds), add milliseconds to the format
510  if ( mTimeStepsComboBox->currentIndex() == mTimeStepsComboBox->findData( QgsUnitTypes::TemporalMilliseconds ) )
511  timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss.zzz" );
512  switch ( mNavigationObject->navigationMode() )
513  {
515  mCurrentRangeLabel->setText( tr( "Current frame: %1 ≤ <i>t</i> &lt; %2" ).arg(
516  range.begin().toString( timeFrameFormat ),
517  range.end().toString( timeFrameFormat ) ) );
518  break;
520  mCurrentRangeLabel->setText( tr( "Range: %1 ≤ <i>t</i> &lt; %2" ).arg(
521  range.begin().toString( timeFrameFormat ),
522  range.end().toString( timeFrameFormat ) ) );
523  break;
525  mCurrentRangeLabel->setText( tr( "Temporal navigation disabled" ) );
526  break;
527  }
528 }
529 
531 {
532  return mNavigationObject;
533 }
534 
535 void QgsTemporalControllerWidget::settings_clicked()
536 {
537  QgsTemporalMapSettingsWidget *settingsWidget = new QgsTemporalMapSettingsWidget( this );
538  settingsWidget->setFrameRateValue( mNavigationObject->framesPerSecond() );
539  settingsWidget->setIsTemporalRangeCumulative( mNavigationObject->temporalRangeCumulative() );
540 
541  connect( settingsWidget, &QgsTemporalMapSettingsWidget::frameRateChanged, this, [ = ]( double rate )
542  {
543  // save new settings into project
545  mNavigationObject->setFramesPerSecond( rate );
546  } );
547 
548  connect( settingsWidget, &QgsTemporalMapSettingsWidget::temporalRangeCumulativeChanged, this, [ = ]( bool state )
549  {
550  // save new settings into project
552  mNavigationObject->setTemporalRangeCumulative( state );
553  } );
554  openPanel( settingsWidget );
555 }
556 
557 void QgsTemporalControllerWidget::timeSlider_valueChanged( int value )
558 {
559  mNavigationObject->setCurrentFrameNumber( value );
560 }
561 
562 void QgsTemporalControllerWidget::startEndDateTime_changed()
563 {
564  whileBlocking( mFixedRangeStartDateTime )->setDateTime( mStartDateTime->dateTime() );
565  whileBlocking( mFixedRangeEndDateTime )->setDateTime( mEndDateTime->dateTime() );
566 
567  updateTemporalExtent();
568  saveRangeToProject();
569 }
570 
571 void QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed()
572 {
573  whileBlocking( mStartDateTime )->setDateTime( mFixedRangeStartDateTime->dateTime() );
574  whileBlocking( mEndDateTime )->setDateTime( mFixedRangeEndDateTime->dateTime() );
575 
576  updateTemporalExtent();
577  saveRangeToProject();
578 }
579 
580 void QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered()
581 {
582  setDatesToAllLayers();
583  saveRangeToProject();
584 }
585 
586 void QgsTemporalControllerWidget::setTimeStep( const QgsInterval &timeStep )
587 {
588  if ( ! timeStep.isValid() || timeStep.seconds() <= 0 )
589  return;
590 
591  int selectedUnit = -1;
592  double selectedValue = std::numeric_limits<double>::max();
594  {
595  // Search the time unit the most appropriate :
596  // the one that gives the smallest time step value for double spin box with round value (if possible) and/or the less signifiant digits
597 
598  int stringSize = std::numeric_limits<int>::max();
599  const int precision = mStepSpinBox->decimals();
600  for ( int i = 0; i < mTimeStepsComboBox->count(); ++i )
601  {
602  const QgsUnitTypes::TemporalUnit unit = static_cast<QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->itemData( i ).toInt() );
603  const double value = timeStep.seconds() * QgsUnitTypes::fromUnitToUnitFactor( QgsUnitTypes::TemporalSeconds, unit );
604  QString string = QString::number( value, 'f', precision );
605 
606  const thread_local QRegularExpression trailingZeroRegEx = QRegularExpression( QStringLiteral( "0+$" ) );
607  //remove trailing zero
608  string.remove( trailingZeroRegEx );
609 
610  const thread_local QRegularExpression trailingPointRegEx = QRegularExpression( QStringLiteral( "[.]+$" ) );
611  //remove last point if present
612  string.remove( trailingPointRegEx );
613 
614  if ( value >= 1
615  && string.size() <= stringSize // less significant digit than currently selected
616  && value < selectedValue ) // less than currently selected
617  {
618  selectedUnit = i;
619  selectedValue = value;
620  stringSize = string.size();
621  }
622  else if ( string != '0'
623  && string.size() < precision + 2 //round value (ex: 0.xx with precision=3)
624  && string.size() < stringSize ) //less significant digit than currently selected
625  {
626  selectedUnit = i ;
627  selectedValue = value ;
628  stringSize = string.size();
629  }
630  }
631  }
632  else
633  {
634  selectedUnit = mTimeStepsComboBox->findData( static_cast< int >( timeStep.originalUnit() ) );
635  selectedValue = 1;
636  }
637 
638  if ( selectedUnit >= 0 )
639  {
640  mStepSpinBox->setValue( selectedValue );
641  mTimeStepsComboBox->setCurrentIndex( selectedUnit );
642  }
643 
644  updateFrameDuration();
645 }
646 
647 void QgsTemporalControllerWidget::updateTimeStepInputs( const QgsInterval &timeStep )
648 {
649  if ( ! timeStep.isValid() || timeStep.seconds() <= 0.0001 )
650  return;
651 
652  QString timeDisplayFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss" );
654  {
655  timeDisplayFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss.zzz" );
656  // very big change that you have to update the range too, as defaulting to NOT handling millis
657  updateTemporalExtent();
658  }
659  mStartDateTime->setDisplayFormat( timeDisplayFormat );
660  mEndDateTime->setDisplayFormat( timeDisplayFormat );
661  mFixedRangeStartDateTime->setDisplayFormat( timeDisplayFormat );
662  mFixedRangeEndDateTime->setDisplayFormat( timeDisplayFormat );
663 
664  // Only update ui when the intervals are different
665  if ( timeStep == QgsInterval( mStepSpinBox->value(),
666  static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) ) )
667  return;
668 
670  {
671  mStepSpinBox->setValue( timeStep.originalDuration() );
672  mTimeStepsComboBox->setCurrentIndex( timeStep.originalUnit() );
673  }
674 
675  updateFrameDuration();
676 }
677 
678 void QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered()
679 {
680  setDatesToProjectTime();
681  saveRangeToProject();
682 }
683 
684 void QgsTemporalControllerWidget::setDates( const QgsDateTimeRange &range )
685 {
686  if ( range.begin().isValid() && range.end().isValid() )
687  {
688  whileBlocking( mStartDateTime )->setDateTime( range.begin() );
689  whileBlocking( mEndDateTime )->setDateTime( range.end() );
690  whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
691  whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
692  updateTemporalExtent();
693  }
694 }
695 
696 void QgsTemporalControllerWidget::setDatesToAllLayers()
697 {
698  QgsDateTimeRange range;
701 
702  setDates( range );
703 }
704 
705 void QgsTemporalControllerWidget::setDatesToProjectTime()
706 {
707  QgsDateTimeRange range;
708 
709  // by default try taking the project's fixed temporal extent
710  if ( QgsProject::instance()->timeSettings() )
712 
713  // if that's not set, calculate the extent from the project's layers
714  if ( !range.begin().isValid() || !range.end().isValid() )
715  {
717  }
718 
720 
721  setDates( range );
722 }
723 
724 void QgsTemporalControllerWidget::saveRangeToProject()
725 {
726  QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
727  QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
728  QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
729  QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
730 }
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:42
double originalDuration() const
Returns the original interval duration.
Definition: qgsinterval.h:280
bool isValid() const
Returns true if the interval is valid.
Definition: qgsinterval.h:255
double seconds() const
Returns the interval duration in seconds.
Definition: qgsinterval.h:236
QgsUnitTypes::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
Definition: qgsinterval.h:295
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
@ LayerRole
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:73
void dataSourceChanged()
Emitted whenever the layer's data source has been changed.
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
Definition: qgsmaplayer.h:1489
Represents a mesh layer supporting display of data on structured or unstructured meshes.
Definition: qgsmeshlayer.h:99
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 ...
QgsDateTimeRange temporalRange() const
Returns the project's temporal range, which indicates the earliest and latest datetime ranges associa...
void setTimeStepUnit(QgsUnitTypes::TemporalUnit unit)
Sets the project's time step (length of one animation frame) unit, which is used as the default value...
void setFramesPerSecond(double rate)
Sets the project's default animation frame rate, in frames per second.
QgsUnitTypes::TemporalUnit timeStepUnit() const
Returns the project's time step (length of one animation frame) unit, which is used as the default va...
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.
Definition: qgsproject.cpp:472
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.
void readProject(const QDomDocument &)
Emitted when a project is being read.
bool writeEntry(const QString &scope, const QString &key, bool value)
Write a boolean value to the project file.
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 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 playForward()
Starts the animation playing in a forward direction up till the end of all frames.
NavigationMode
Represents the current temporal navigation mode.
@ NavigationOff
Temporal navigation is disabled.
@ FixedRange
Temporal navigation relies on a fixed datetime range.
@ Animated
Temporal navigation relies on frames within a datetime range.
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 navigationModeChanged(NavigationMode mode)
Emitted whenever the navigation mode changes.
void setFramesPerSecond(double rate)
Sets the animation frame rate, in frames per second.
void stateChanged(AnimationState state)
Emitted whenever the animation state changes.
AnimationState
Represents the current animation state.
@ Forward
Animation is playing forward.
@ Reverse
Animation is playing in reverse.
bool temporalRangeCumulative() const
Returns the animation temporal range cumulative settings.
void next()
Advances to the next frame.
void setTemporalExtents(const QgsDateTimeRange &extents)
Sets the navigation temporal extents, which dictate the earliest and latest date time possible in the...
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.
void setNavigationMode(const NavigationMode mode)
Sets the temporal navigation mode.
AnimationState animationState() const
Returns the current animation state.
NavigationMode navigationMode() const
Returns the current temporal navigation mode.
bool isActive() const
Returns true if the temporal property is active.
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(QgsUnitTypes::DistanceUnit unit)
Returns a translated string representing a distance unit.
static Q_INVOKABLE double fromUnitToUnitFactor(QgsUnitTypes::DistanceUnit fromUnit, QgsUnitTypes::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
TemporalUnit
Temporal units.
Definition: qgsunittypes.h:150
@ TemporalMonths
Months.
Definition: qgsunittypes.h:157
@ TemporalUnknownUnit
Unknown time unit.
Definition: qgsunittypes.h:162
@ TemporalWeeks
Weeks.
Definition: qgsunittypes.h:156
@ TemporalMilliseconds
Milliseconds.
Definition: qgsunittypes.h:151
@ TemporalIrregularStep
Special "irregular step" time unit, used for temporal data which uses irregular, non-real-world unit ...
Definition: qgsunittypes.h:161
@ TemporalDays
Days.
Definition: qgsunittypes.h:155
@ TemporalDecades
Decades.
Definition: qgsunittypes.h:159
@ TemporalCenturies
Centuries.
Definition: qgsunittypes.h:160
@ TemporalSeconds
Seconds.
Definition: qgsunittypes.h:152
@ TemporalMinutes
Minutes.
Definition: qgsunittypes.h:153
@ TemporalYears
Years.
Definition: qgsunittypes.h:158
@ TemporalHours
Hours.
Definition: qgsunittypes.h:154
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition: qgis.h:1917
int precision