QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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"
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
192void 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
224void 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
243void 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
262void 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
286void 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
300void 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
335void 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
376void 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
385void 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
394void 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
403void 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
423void 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
454void 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
477void 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
500void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range )
501{
502 whileBlocking( mSlider )->setValue( mNavigationObject->currentFrameNumber() );
503 updateRangeLabel( range );
504}
505
506void 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
535void 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
557void QgsTemporalControllerWidget::timeSlider_valueChanged( int value )
558{
559 mNavigationObject->setCurrentFrameNumber( value );
560}
561
562void QgsTemporalControllerWidget::startEndDateTime_changed()
563{
564 whileBlocking( mFixedRangeStartDateTime )->setDateTime( mStartDateTime->dateTime() );
565 whileBlocking( mFixedRangeEndDateTime )->setDateTime( mEndDateTime->dateTime() );
566
567 updateTemporalExtent();
568 saveRangeToProject();
569}
570
571void QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed()
572{
573 whileBlocking( mStartDateTime )->setDateTime( mFixedRangeStartDateTime->dateTime() );
574 whileBlocking( mEndDateTime )->setDateTime( mFixedRangeEndDateTime->dateTime() );
575
576 updateTemporalExtent();
577 saveRangeToProject();
578}
579
580void QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered()
581{
582 setDatesToAllLayers();
583 saveRangeToProject();
584}
585
586void 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
647void 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
678void QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered()
679{
680 setDatesToProjectTime();
681 saveRangeToProject();
682}
683
684void 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
696void QgsTemporalControllerWidget::setDatesToAllLayers()
697{
698 QgsDateTimeRange range;
701
702 setDates( range );
703}
704
705void 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
724void 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:1503
Represents a mesh layer supporting display of data on structured or unstructured meshes.
Definition: qgsmeshlayer.h:100
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:477
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 stateChanged(QgsTemporalNavigationObject::AnimationState state)
Emitted whenever the animation state changes.
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 setFramesPerSecond(double rate)
Sets the animation frame rate, in frames per second.
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 navigationModeChanged(QgsTemporalNavigationObject::NavigationMode mode)
Emitted whenever the navigation mode changes.
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:2453
int precision