QGIS API Documentation 3.99.0-Master (d270888f95f)
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
19
20#include <memory>
21
22#include "qgsapplication.h"
23#include "qgsmaplayermodel.h"
25#include "qgsmeshlayer.h"
26#include "qgsproject.h"
28#include "qgsrasterlayer.h"
30#include "qgstemporalutils.h"
31#include "qgsunittypes.h"
32
33#include <QAction>
34#include <QMenu>
35#include <QRegularExpression>
36#include <QString>
37
38#include "moc_qgstemporalcontrollerwidget.cpp"
39
40using namespace Qt::StringLiterals;
41
43 : QgsPanelWidget( parent )
44{
45 setupUi( this );
46
47 mNavigationObject = new QgsTemporalNavigationObject( this );
48
49 mStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
50 mEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
51 mFixedRangeStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
52 mFixedRangeEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() );
53
54 auto handleOperation = [this]( Qgis::PlaybackOperation operation ) {
55 switch ( operation )
56 {
58 mNavigationObject->rewindToStart();
59 break;
60
62 mNavigationObject->previous();
63 break;
64
66 mNavigationObject->playBackward();
67 break;
68
70 mNavigationObject->pause();
71 break;
72
74 mNavigationObject->playForward();
75 break;
76
78 mNavigationObject->next();
79 break;
80
82 mNavigationObject->skipToEnd();
83 break;
84 }
85 };
86 connect( mAnimationController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
87 connect( mMovieController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
88
89 connect( mAnimationLoopingCheckBox, &QCheckBox::toggled, this, [this]( bool state ) { mNavigationObject->setLooping( state ); mMovieLoopingCheckBox->setChecked( state ); } );
90 connect( mMovieLoopingCheckBox, &QCheckBox::toggled, this, [this]( bool state ) { mNavigationObject->setLooping( state ); mAnimationLoopingCheckBox->setChecked( state ); } );
91
92 setWidgetStateFromNavigationMode( mNavigationObject->navigationMode() );
93 connect( mNavigationObject, &QgsTemporalNavigationObject::navigationModeChanged, this, &QgsTemporalControllerWidget::setWidgetStateFromNavigationMode );
94 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalExtentsChanged, this, &QgsTemporalControllerWidget::setDates );
95 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalFrameDurationChanged, this, [this]( const QgsInterval &timeStep ) {
96 if ( mBlockFrameDurationUpdates )
97 return;
98
99 mBlockFrameDurationUpdates++;
100 updateTimeStepInputs( timeStep );
101 mBlockFrameDurationUpdates--;
102 } );
103 connect( mNavigationOff, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationOff_clicked );
104 connect( mNavigationFixedRange, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationFixedRange_clicked );
105 connect( mNavigationAnimated, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationAnimated_clicked );
106 connect( mNavigationMovie, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationMovie_clicked );
107
108 connect( mNavigationObject, &QgsTemporalNavigationObject::stateChanged, this, [this]( Qgis::AnimationState state ) {
109 mAnimationController->setState( state );
110 mMovieController->setState( state );
111 } );
112
113 connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
114 connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
115 connect( mFixedRangeStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
116 connect( mFixedRangeEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
117 connect( mStepSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
118 connect( mTimeStepsComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
119 connect( mAnimationSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
120 connect( mMovieSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
121
122 connect( mTotalFramesSpinBox, qOverload<int>( &QSpinBox::valueChanged ), this, [this]( int frames ) {
123 mNavigationObject->setTotalMovieFrames( frames );
124 } );
125
126 mStepSpinBox->setClearValue( 1 );
127
128 connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider );
129 connect( mNavigationObject, &QgsTemporalNavigationObject::totalMovieFramesChanged, this, &QgsTemporalControllerWidget::totalMovieFramesChanged );
130
131 connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked );
132
133 mMapLayerModel = new QgsMapLayerModel( this );
134
135 mRangeMenu = std::make_unique<QMenu>( this );
136
137 mRangeSetToAllLayersAction = new QAction( tr( "Set to Full Range" ), mRangeMenu.get() );
138 mRangeSetToAllLayersAction->setIcon( QgsApplication::getThemeIcon( u"/mActionRefresh.svg"_s ) );
139 connect( mRangeSetToAllLayersAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered );
140 mRangeMenu->addAction( mRangeSetToAllLayersAction );
141
142 mRangeSetToProjectAction = new QAction( tr( "Set to Preset Project Range" ), mRangeMenu.get() );
143 connect( mRangeSetToProjectAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered );
144 mRangeMenu->addAction( mRangeSetToProjectAction );
145
146 mRangeMenu->addSeparator();
147
148 mRangeLayersSubMenu = std::make_unique<QMenu>( tr( "Set to Single Layer's Range" ), mRangeMenu.get() );
149 mRangeLayersSubMenu->setEnabled( false );
150 mRangeMenu->addMenu( mRangeLayersSubMenu.get() );
151 connect( mRangeMenu.get(), &QMenu::aboutToShow, this, &QgsTemporalControllerWidget::aboutToShowRangeMenu );
152
153 mSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
154 mSetRangeButton->setMenu( mRangeMenu.get() );
155 mSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
156 mFixedRangeSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
157 mFixedRangeSetRangeButton->setMenu( mRangeMenu.get() );
158 mFixedRangeSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
159
160 connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
161 connect( mExportMovieButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
162
163 QgsDateTimeRange range;
164
165 if ( QgsProject::instance()->timeSettings() )
167
168 if ( range.begin().isValid() && range.end().isValid() )
169 {
170 whileBlocking( mStartDateTime )->setDateTime( range.begin() );
171 whileBlocking( mEndDateTime )->setDateTime( range.end() );
172 whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
173 whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
174 }
175
176 for ( const Qgis::TemporalUnit u :
177 {
189 } )
190 {
191 mTimeStepsComboBox->addItem( u != Qgis::TemporalUnit::IrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), static_cast<int>( u ) );
192 }
193
194 // TODO: might want to choose an appropriate default unit based on the range
195 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast<int>( Qgis::TemporalUnit::Hours ) ) );
196
197 // NOTE 'minimum' and 'decimals' should be in sync with the 'decimals' in qgstemporalcontrollerwidgetbase.ui
198 mStepSpinBox->setDecimals( 3 );
199 // minimum timestep one millisecond
200 mStepSpinBox->setMinimum( 0.001 );
201 mStepSpinBox->setMaximum( std::numeric_limits<int>::max() );
202 mStepSpinBox->setSingleStep( 1 );
203 mStepSpinBox->setValue( 1 );
204
205 updateFrameDuration();
206
207 connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject );
208 connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded );
209 connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared );
210}
211
213{
214 return true;
215}
216
218{
219 if ( ( mAnimationSlider->hasFocus() || mMovieSlider->hasFocus() ) && e->key() == Qt::Key_Space )
220 {
221 mAnimationController->togglePause();
222 // connections will auto-sync mMovieController state!
223 }
225}
226
227void QgsTemporalControllerWidget::aboutToShowRangeMenu()
228{
229 QgsDateTimeRange projectRange;
230 if ( QgsProject::instance()->timeSettings() )
231 projectRange = QgsProject::instance()->timeSettings()->temporalRange();
232 mRangeSetToProjectAction->setEnabled( projectRange.begin().isValid() && projectRange.end().isValid() );
233
234 mRangeLayersSubMenu->clear();
235 for ( int i = 0; i < mMapLayerModel->rowCount(); ++i )
236 {
237 const QModelIndex index = mMapLayerModel->index( i, 0 );
238 QgsMapLayer *currentLayer = mMapLayerModel->data( index, static_cast<int>( QgsMapLayerModel::CustomRole::Layer ) ).value<QgsMapLayer *>();
239 if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
240 continue;
241
242 const QIcon icon = qvariant_cast<QIcon>( mMapLayerModel->data( index, Qt::DecorationRole ) );
243 const QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString();
244 const QgsDateTimeRange range = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
245 if ( range.begin().isValid() && range.end().isValid() )
246 {
247 QAction *action = new QAction( icon, text, mRangeLayersSubMenu.get() );
248 connect( action, &QAction::triggered, this, [this, range] {
249 setDates( range );
250 saveRangeToProject();
251 } );
252 mRangeLayersSubMenu->addAction( action );
253 }
254 }
255 mRangeLayersSubMenu->setEnabled( !mRangeLayersSubMenu->actions().isEmpty() );
256}
257
258void QgsTemporalControllerWidget::updateTemporalExtent()
259{
260 // TODO - consider whether the overall time range set for animations should include the end date time or not.
261 // (currently it DOES include the end date time).
262 const QDateTime start = mStartDateTime->dateTime();
263 const QDateTime end = mEndDateTime->dateTime();
264 const bool isTimeInstant = start == end;
265 const QgsDateTimeRange temporalExtent = QgsDateTimeRange( start, end, true, !isTimeInstant && mNavigationObject->navigationMode() == Qgis::TemporalNavigationMode::FixedRange ? false : true );
266 mNavigationObject->setTemporalExtents( temporalExtent );
267 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
268 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
269 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
270 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
271}
272
273void QgsTemporalControllerWidget::updateFrameDuration()
274{
275 if ( mBlockSettingUpdates )
276 return;
277
278 // save new settings into project
279 const Qgis::TemporalUnit unit = static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() );
281 QgsProject::instance()->timeSettings()->setTimeStep( unit == Qgis::TemporalUnit::IrregularStep ? 1 : mStepSpinBox->value() );
282
283 if ( !mBlockFrameDurationUpdates )
284 {
285 mNavigationObject->setFrameDuration(
286 QgsInterval( QgsProject::instance()->timeSettings()->timeStep(), QgsProject::instance()->timeSettings()->timeStepUnit() )
287 );
288 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
289 }
290 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
291 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
292 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
293 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
294
296 {
297 mStepSpinBox->setEnabled( false );
298 mStepSpinBox->setValue( 1 );
299 mAnimationSlider->setTickInterval( 1 );
300 mAnimationSlider->setTickPosition( QSlider::TicksBothSides );
301 }
302 else
303 {
304 mStepSpinBox->setEnabled( true );
305 mAnimationSlider->setTickInterval( 0 );
306 mAnimationSlider->setTickPosition( QSlider::NoTicks );
307 }
308}
309
310void QgsTemporalControllerWidget::setWidgetStateFromProject()
311{
312 mBlockSettingUpdates++;
313 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast<int>( QgsProject::instance()->timeSettings()->timeStepUnit() ) ) );
314 mStepSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() );
315 mBlockSettingUpdates--;
316
317 bool ok = false;
318 const Qgis::TemporalNavigationMode mode = static_cast<Qgis::TemporalNavigationMode>( QgsProject::instance()->readNumEntry( u"TemporalControllerWidget"_s, u"/NavigationMode"_s, 0, &ok ) );
319 if ( ok )
320 {
321 mNavigationObject->setNavigationMode( mode );
322 setWidgetStateFromNavigationMode( mode );
323 }
324 else
325 {
326 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::Disabled );
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( u"TemporalControllerWidget"_s, u"/StartDateTime"_s );
334 const QString endString = QgsProject::instance()->readEntry( u"TemporalControllerWidget"_s, u"/EndDateTime"_s );
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( u"TemporalControllerWidget"_s, u"/NavigationMode"_s, static_cast<int>( Qgis::TemporalNavigationMode::Disabled ) );
356
357 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::Disabled );
358 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
359}
360
361void QgsTemporalControllerWidget::mNavigationFixedRange_clicked()
362{
363 QgsProject::instance()->writeEntry( u"TemporalControllerWidget"_s, u"/NavigationMode"_s, static_cast<int>( Qgis::TemporalNavigationMode::FixedRange ) );
364
365 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::FixedRange );
366 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::FixedRange );
367}
368
369void QgsTemporalControllerWidget::mNavigationAnimated_clicked()
370{
371 QgsProject::instance()->writeEntry( u"TemporalControllerWidget"_s, u"/NavigationMode"_s, static_cast<int>( Qgis::TemporalNavigationMode::Animated ) );
372
373 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::Animated );
374 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Animated );
375}
376
377void QgsTemporalControllerWidget::mNavigationMovie_clicked()
378{
379 QgsProject::instance()->writeEntry( u"TemporalControllerWidget"_s, u"/NavigationMode"_s, static_cast<int>( Qgis::TemporalNavigationMode::Movie ) );
380
381 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::Movie );
382 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Movie );
383}
384
385void QgsTemporalControllerWidget::setWidgetStateFromNavigationMode( const Qgis::TemporalNavigationMode mode )
386{
387 mNavigationOff->setChecked( mode == Qgis::TemporalNavigationMode::Disabled );
388 mNavigationFixedRange->setChecked( mode == Qgis::TemporalNavigationMode::FixedRange );
389 mNavigationAnimated->setChecked( mode == Qgis::TemporalNavigationMode::Animated );
390 mNavigationMovie->setChecked( mode == Qgis::TemporalNavigationMode::Movie );
391
392 switch ( mode )
393 {
395 mNavigationModeStackedWidget->setCurrentIndex( 0 );
396 break;
398 mNavigationModeStackedWidget->setCurrentIndex( 1 );
399 break;
401 mNavigationModeStackedWidget->setCurrentIndex( 2 );
402 break;
404 mNavigationModeStackedWidget->setCurrentIndex( 3 );
405 break;
406 }
407}
408
409void QgsTemporalControllerWidget::onLayersAdded( const QList<QgsMapLayer *> &layers )
410{
411 if ( !mHasTemporalLayersLoaded )
412 {
413 for ( QgsMapLayer *layer : layers )
414 {
415 if ( layer->temporalProperties() )
416 {
417 mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive();
418
419 if ( !mHasTemporalLayersLoaded )
420 {
421 connect( layer, &QgsMapLayer::dataSourceChanged, this, [this, layer] {
422 if ( layer->isValid() && layer->temporalProperties()->isActive() && !mHasTemporalLayersLoaded )
423 {
424 mHasTemporalLayersLoaded = true;
425 firstTemporalLayerLoaded( layer );
426 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
427 }
428 } );
429 }
430
431 firstTemporalLayerLoaded( layer );
432 }
433 }
434 }
435
436 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
437}
438
439void QgsTemporalControllerWidget::firstTemporalLayerLoaded( QgsMapLayer *layer )
440{
441 setDatesToProjectTime( true );
442
443 if ( QgsMeshLayer *meshLayer = qobject_cast<QgsMeshLayer *>( layer ) )
444 {
445 mBlockFrameDurationUpdates++;
446 setTimeStep( meshLayer->firstValidTimeStep() );
447 mBlockFrameDurationUpdates--;
448 updateFrameDuration();
449 }
450 else if ( QgsRasterLayer *rasterLayer = qobject_cast<QgsRasterLayer *>( layer ) )
451 {
452 if ( rasterLayer->dataProvider() && rasterLayer->dataProvider()->temporalCapabilities() )
453 {
454 mBlockFrameDurationUpdates++;
455 setTimeStep( rasterLayer->dataProvider()->temporalCapabilities()->defaultInterval() );
456 mBlockFrameDurationUpdates--;
457 updateFrameDuration();
458 }
459 }
460}
461
462void QgsTemporalControllerWidget::onProjectCleared()
463{
464 mHasTemporalLayersLoaded = false;
465
466 mNavigationObject->setNavigationMode( Qgis::TemporalNavigationMode::Disabled );
467 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
468
469 // default to showing the last 24 hours, ending at the current date's hour, in one hour blocks...
470 // it's COMPLETELY arbitrary, but better than starting with a "zero length" duration!
471 const QTime startOfCurrentHour = QTime( QTime::currentTime().hour(), 0, 0 );
472 const QDateTime end = QDateTime( QDate::currentDate(), startOfCurrentHour, Qt::UTC );
473 const QDateTime start = end.addSecs( -24 * 60 * 60 );
474
475 whileBlocking( mStartDateTime )->setDateTime( start );
476 whileBlocking( mEndDateTime )->setDateTime( end );
477 whileBlocking( mFixedRangeStartDateTime )->setDateTime( start );
478 whileBlocking( mFixedRangeEndDateTime )->setDateTime( end );
479
480 updateTemporalExtent();
481 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast<int>( Qgis::TemporalUnit::Hours ) ) );
482 mStepSpinBox->setValue( 1 );
483}
484
485void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range )
486{
487 whileBlocking( mAnimationSlider )->setValue( mNavigationObject->currentFrameNumber() );
488 whileBlocking( mMovieSlider )->setValue( mNavigationObject->currentFrameNumber() );
489 updateRangeLabel( range );
490}
491
492void QgsTemporalControllerWidget::totalMovieFramesChanged( long long frames )
493{
495 mTotalFramesSpinBox->setValue( frames );
496 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( frames ) );
497}
498
499void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range )
500{
501 QString timeFrameFormat = u"yyyy-MM-dd HH:mm:ss"_s;
502 // but if timesteps are < 1 second (as: in milliseconds), add milliseconds to the format
503 if ( static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) == Qgis::TemporalUnit::Milliseconds )
504 timeFrameFormat = u"yyyy-MM-dd HH:mm:ss.zzz"_s;
505 switch ( mNavigationObject->navigationMode() )
506 {
508 mCurrentRangeLabel->setText( tr( "Current frame: %1 ≤ <i>t</i> &lt; %2" ).arg( range.begin().toString( timeFrameFormat ), range.end().toString( timeFrameFormat ) ) );
509 break;
511 mCurrentRangeLabel->setText( tr( "Range: %1 ≤ <i>t</i> &lt; %2" ).arg( range.begin().toString( timeFrameFormat ), range.end().toString( timeFrameFormat ) ) );
512 break;
514 mCurrentRangeLabel->setText( tr( "Temporal navigation disabled" ) );
515 break;
517 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( mNavigationObject->totalMovieFrames() ) );
518 break;
519 }
520}
521
526
527void QgsTemporalControllerWidget::settings_clicked()
528{
529 QgsTemporalMapSettingsWidget *settingsWidget = new QgsTemporalMapSettingsWidget( this );
530 settingsWidget->setFrameRateValue( mNavigationObject->framesPerSecond() );
531 settingsWidget->setIsTemporalRangeCumulative( mNavigationObject->temporalRangeCumulative() );
532
533 connect( settingsWidget, &QgsTemporalMapSettingsWidget::frameRateChanged, this, [this]( double rate ) {
534 // save new settings into project
536 mNavigationObject->setFramesPerSecond( rate );
537 } );
538
539 connect( settingsWidget, &QgsTemporalMapSettingsWidget::temporalRangeCumulativeChanged, this, [this]( bool state ) {
540 // save new settings into project
542 mNavigationObject->setTemporalRangeCumulative( state );
543 } );
544 openPanel( settingsWidget );
545}
546
547void QgsTemporalControllerWidget::timeSlider_valueChanged( int value )
548{
549 mNavigationObject->setCurrentFrameNumber( value );
550}
551
552void QgsTemporalControllerWidget::startEndDateTime_changed()
553{
554 whileBlocking( mFixedRangeStartDateTime )->setDateTime( mStartDateTime->dateTime() );
555 whileBlocking( mFixedRangeEndDateTime )->setDateTime( mEndDateTime->dateTime() );
556
557 updateTemporalExtent();
558 saveRangeToProject();
559}
560
561void QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed()
562{
563 whileBlocking( mStartDateTime )->setDateTime( mFixedRangeStartDateTime->dateTime() );
564 whileBlocking( mEndDateTime )->setDateTime( mFixedRangeEndDateTime->dateTime() );
565
566 updateTemporalExtent();
567 saveRangeToProject();
568}
569
570void QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered()
571{
572 setDatesToAllLayers();
573 saveRangeToProject();
574}
575
576void QgsTemporalControllerWidget::setTimeStep( const QgsInterval &timeStep )
577{
578 if ( !timeStep.isValid() || timeStep.seconds() <= 0 )
579 return;
580
581 int selectedUnit = -1;
582 double selectedValue = std::numeric_limits<double>::max();
584 {
585 // Search the time unit the most appropriate :
586 // the one that gives the smallest time step value for double spin box with round value (if possible) and/or the less signifiant digits
587
588 int stringSize = std::numeric_limits<int>::max();
589 const int precision = mStepSpinBox->decimals();
590 for ( int i = 0; i < mTimeStepsComboBox->count(); ++i )
591 {
592 const Qgis::TemporalUnit unit = static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->itemData( i ).toInt() );
593 const double value = timeStep.seconds() * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Seconds, unit );
594 QString string = QString::number( value, 'f', precision );
595
596 const thread_local QRegularExpression trailingZeroRegEx = QRegularExpression( u"0+$"_s );
597 //remove trailing zero
598 string.remove( trailingZeroRegEx );
599
600 const thread_local QRegularExpression trailingPointRegEx = QRegularExpression( u"[.]+$"_s );
601 //remove last point if present
602 string.remove( trailingPointRegEx );
603
604 if ( value >= 1
605 && string.size() <= stringSize // less significant digit than currently selected
606 && value < selectedValue ) // less than currently selected
607 {
608 selectedUnit = i;
609 selectedValue = value;
610 stringSize = string.size();
611 }
612 else if ( string != '0'
613 && string.size() < precision + 2 //round value (ex: 0.xx with precision=3)
614 && string.size() < stringSize ) //less significant digit than currently selected
615 {
616 selectedUnit = i;
617 selectedValue = value;
618 stringSize = string.size();
619 }
620 }
621 }
622 else
623 {
624 selectedUnit = mTimeStepsComboBox->findData( static_cast<int>( timeStep.originalUnit() ) );
625 selectedValue = 1;
626 }
627
628 if ( selectedUnit >= 0 )
629 {
630 mStepSpinBox->setValue( selectedValue );
631 mTimeStepsComboBox->setCurrentIndex( selectedUnit );
632 }
633
634 updateFrameDuration();
635}
636
637void QgsTemporalControllerWidget::updateTimeStepInputs( const QgsInterval &timeStep )
638{
639 if ( !timeStep.isValid() || timeStep.seconds() <= 0.0001 )
640 return;
641
642 QString timeDisplayFormat = u"yyyy-MM-dd HH:mm:ss"_s;
644 {
645 timeDisplayFormat = u"yyyy-MM-dd HH:mm:ss.zzz"_s;
646 // very big change that you have to update the range too, as defaulting to NOT handling millis
647 updateTemporalExtent();
648 }
649 mStartDateTime->setDisplayFormat( timeDisplayFormat );
650 mEndDateTime->setDisplayFormat( timeDisplayFormat );
651 mFixedRangeStartDateTime->setDisplayFormat( timeDisplayFormat );
652 mFixedRangeEndDateTime->setDisplayFormat( timeDisplayFormat );
653
654 // Only update ui when the intervals are different
655 if ( timeStep == QgsInterval( mStepSpinBox->value(), static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) ) )
656 return;
657
658 if ( timeStep.originalUnit() != Qgis::TemporalUnit::Unknown )
659 {
660 mStepSpinBox->setValue( timeStep.originalDuration() );
661 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast<int>( timeStep.originalUnit() ) ) );
662 }
663
664 updateFrameDuration();
665}
666
667void QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered()
668{
669 setDatesToProjectTime( false );
670 saveRangeToProject();
671}
672
673void QgsTemporalControllerWidget::setDates( const QgsDateTimeRange &range )
674{
675 if ( range.begin().isValid() && range.end().isValid() )
676 {
677 whileBlocking( mStartDateTime )->setDateTime( range.begin() );
678 whileBlocking( mEndDateTime )->setDateTime( range.end() );
679 whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
680 whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
681 updateTemporalExtent();
682 }
683}
684
685void QgsTemporalControllerWidget::setDatesToAllLayers()
686{
687 QgsDateTimeRange range;
689 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
690
691 setDates( range );
692}
693
694void QgsTemporalControllerWidget::setDatesToProjectTime( bool tryLastStoredRange )
695{
696 QgsDateTimeRange range;
697
698 if ( tryLastStoredRange )
699 {
700 const QString startString = QgsProject::instance()->readEntry( u"TemporalControllerWidget"_s, u"/StartDateTime"_s );
701 const QString endString = QgsProject::instance()->readEntry( u"TemporalControllerWidget"_s, u"/EndDateTime"_s );
702 if ( !startString.isEmpty() && !endString.isEmpty() )
703 {
704 range = QgsDateTimeRange( QDateTime::fromString( startString, Qt::ISODateWithMs ), QDateTime::fromString( endString, Qt::ISODateWithMs ) );
705 }
706 }
707
708 // by default try taking the project's fixed temporal extent
709 if ( ( !range.begin().isValid() || !range.end().isValid() ) && QgsProject::instance()->timeSettings() )
711
712 // if that's not set, calculate the extent from the project's layers
713 if ( !range.begin().isValid() || !range.end().isValid() )
714 {
716 }
717
718 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
719
720 setDates( range );
721}
722
723void QgsTemporalControllerWidget::saveRangeToProject()
724{
725 QgsProject::instance()->writeEntry( u"TemporalControllerWidget"_s, u"/StartDateTime"_s, mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
726 QgsProject::instance()->writeEntry( u"TemporalControllerWidget"_s, u"/EndDateTime"_s, mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
727}
TemporalNavigationMode
Temporal navigation modes.
Definition qgis.h:2592
@ Animated
Temporal navigation relies on frames within a datetime range.
Definition qgis.h:2594
@ Movie
Movie mode – behaves like a video player, with a fixed frame duration and no temporal range.
Definition qgis.h:2596
@ FixedRange
Temporal navigation relies on a fixed datetime range.
Definition qgis.h:2595
@ Disabled
Temporal navigation is disabled.
Definition qgis.h:2593
PlaybackOperation
Media playback operations.
Definition qgis.h:2621
@ Pause
Pause playback.
Definition qgis.h:2625
@ PlayReverse
Play in reverse.
Definition qgis.h:2624
@ PlayForward
Play forward.
Definition qgis.h:2626
@ SkipToStart
Jump to start of playback.
Definition qgis.h:2622
@ PreviousFrame
Step to previous frame.
Definition qgis.h:2623
@ SkipToEnd
Jump to end of playback.
Definition qgis.h:2628
@ NextFrame
Step to next frame.
Definition qgis.h:2627
TemporalUnit
Temporal units.
Definition qgis.h:5231
@ IrregularStep
Special 'irregular step' time unit, used for temporal data which uses irregular, non-real-world unit ...
Definition qgis.h:5242
@ Milliseconds
Milliseconds.
Definition qgis.h:5232
@ Hours
Hours.
Definition qgis.h:5235
@ Unknown
Unknown time unit.
Definition qgis.h:5243
@ Centuries
Centuries.
Definition qgis.h:5241
@ Seconds
Seconds.
Definition qgis.h:5233
@ Weeks
Weeks.
Definition qgis.h:5237
@ Years
Years.
Definition qgis.h:5239
@ Decades
Decades.
Definition qgis.h:5240
@ Months
Months.
Definition qgis.h:5238
@ Minutes
Minutes.
Definition qgis.h:5234
AnimationState
Animation states.
Definition qgis.h:2608
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:50
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.
A model for display of map 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:83
void dataSourceChanged()
Emitted whenever the layer's data source has been changed.
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
QgsPanelWidget(QWidget *parent=nullptr)
Base class for any widget that can be shown as an inline panel.
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...
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.
double framesPerSecond() const
Returns the animation frame rate, in frames per second.
void navigationModeChanged(Qgis::TemporalNavigationMode mode)
Emitted whenever the navigation mode changes.
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 totalMovieFramesChanged(long long frames)
Emitted whenever the total number of frames in the movie is changed.
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:449
T end() const
Returns the upper bound of the range.
Definition qgsrange.h:456
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:6804
QgsTemporalRange< QDateTime > QgsDateTimeRange
QgsRange which stores a range of date times.
Definition qgsrange.h:764