QGIS API Documentation 3.39.0-Master (bca3cdb6021)
Loading...
Searching...
No Matches
qgstemporalcontrollerwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstemporalcontrollerwidget.cpp
3 ------------------------------
4 begin : February 2020
5 copyright : (C) 2020 by Samweli Mwakisambwe
6 email : samweli at kartoza dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsapplication.h"
20#include "qgsmaplayermodel.h"
21#include "qgsproject.h"
24#include "qgstemporalutils.h"
26#include "qgsmeshlayer.h"
27#include "qgsrasterlayer.h"
28#include "qgsunittypes.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 auto handleOperation = [ = ]( Qgis::PlaybackOperation operation )
47 {
48 switch ( operation )
49 {
51 mNavigationObject->rewindToStart();
52 break;
53
55 mNavigationObject->previous();
56 break;
57
59 mNavigationObject->playBackward();
60 break;
61
63 mNavigationObject->pause();
64 break;
65
67 mNavigationObject->playForward();
68 break;
69
71 mNavigationObject->next();
72 break;
73
75 mNavigationObject->skipToEnd();
76 break;
77 }
78 };
79 connect( mAnimationController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
80 connect( mMovieController, &QgsPlaybackControllerWidget::operationTriggered, this, handleOperation );
81
82 connect( mAnimationLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); mMovieLoopingCheckBox->setChecked( state ); } );
83 connect( mMovieLoopingCheckBox, &QCheckBox::toggled, this, [ = ]( bool state ) { mNavigationObject->setLooping( state ); mAnimationLoopingCheckBox->setChecked( state ); } );
84
85 setWidgetStateFromNavigationMode( mNavigationObject->navigationMode() );
86 connect( mNavigationObject, &QgsTemporalNavigationObject::navigationModeChanged, this, &QgsTemporalControllerWidget::setWidgetStateFromNavigationMode );
87 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalExtentsChanged, this, &QgsTemporalControllerWidget::setDates );
88 connect( mNavigationObject, &QgsTemporalNavigationObject::temporalFrameDurationChanged, this, [ = ]( const QgsInterval & timeStep )
89 {
90 if ( mBlockFrameDurationUpdates )
91 return;
92
93 mBlockFrameDurationUpdates++;
94 updateTimeStepInputs( timeStep );
95 mBlockFrameDurationUpdates--;
96 } );
97 connect( mNavigationOff, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationOff_clicked );
98 connect( mNavigationFixedRange, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationFixedRange_clicked );
99 connect( mNavigationAnimated, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationAnimated_clicked );
100 connect( mNavigationMovie, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mNavigationMovie_clicked );
101
102 connect( mNavigationObject, &QgsTemporalNavigationObject::stateChanged, this, [ = ]( Qgis::AnimationState state )
103 {
104 mAnimationController->setState( state );
105 mMovieController->setState( state );
106 } );
107
108 connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
109 connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::startEndDateTime_changed );
110 connect( mFixedRangeStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
111 connect( mFixedRangeEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::fixedRangeStartEndDateTime_changed );
112 connect( mStepSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
113 connect( mTimeStepsComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration );
114 connect( mAnimationSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
115 connect( mMovieSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged );
116
117 connect( mTotalFramesSpinBox, qOverload<int>( &QSpinBox::valueChanged ), this, [ = ]( int frames )
118 {
119 mNavigationObject->setTotalMovieFrames( frames );
120 } );
121
122 mStepSpinBox->setClearValue( 1 );
123
124 connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider );
125 connect( mNavigationObject, &QgsTemporalNavigationObject::totalMovieFramesChanged, this, &QgsTemporalControllerWidget::totalMovieFramesChanged );
126
127 connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked );
128
129 mMapLayerModel = new QgsMapLayerModel( this );
130
131 mRangeMenu.reset( new QMenu( this ) );
132
133 mRangeSetToAllLayersAction = new QAction( tr( "Set to Full Range" ), mRangeMenu.get() );
134 mRangeSetToAllLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRefresh.svg" ) ) );
135 connect( mRangeSetToAllLayersAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToAllLayersAction_triggered );
136 mRangeMenu->addAction( mRangeSetToAllLayersAction );
137
138 mRangeSetToProjectAction = new QAction( tr( "Set to Preset Project Range" ), mRangeMenu.get() );
139 connect( mRangeSetToProjectAction, &QAction::triggered, this, &QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered );
140 mRangeMenu->addAction( mRangeSetToProjectAction );
141
142 mRangeMenu->addSeparator();
143
144 mRangeLayersSubMenu.reset( new QMenu( tr( "Set to Single Layer's Range" ), mRangeMenu.get() ) );
145 mRangeLayersSubMenu->setEnabled( false );
146 mRangeMenu->addMenu( mRangeLayersSubMenu.get() );
147 connect( mRangeMenu.get(), &QMenu::aboutToShow, this, &QgsTemporalControllerWidget::aboutToShowRangeMenu );
148
149 mSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
150 mSetRangeButton->setMenu( mRangeMenu.get() );
151 mSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
152 mFixedRangeSetRangeButton->setPopupMode( QToolButton::MenuButtonPopup );
153 mFixedRangeSetRangeButton->setMenu( mRangeMenu.get() );
154 mFixedRangeSetRangeButton->setDefaultAction( mRangeSetToAllLayersAction );
155
156 connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
157 connect( mExportMovieButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
158
159 QgsDateTimeRange range;
160
161 if ( QgsProject::instance()->timeSettings() )
163
164 if ( range.begin().isValid() && range.end().isValid() )
165 {
166 whileBlocking( mStartDateTime )->setDateTime( range.begin() );
167 whileBlocking( mEndDateTime )->setDateTime( range.end() );
168 whileBlocking( mFixedRangeStartDateTime )->setDateTime( range.begin() );
169 whileBlocking( mFixedRangeEndDateTime )->setDateTime( range.end() );
170 }
171
172 for ( const Qgis::TemporalUnit u :
173 {
185 } )
186 {
187 mTimeStepsComboBox->addItem( u != Qgis::TemporalUnit::IrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), static_cast< int >( u ) );
188 }
189
190 // TODO: might want to choose an appropriate default unit based on the range
191 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Hours ) ) );
192
193 // NOTE 'minimum' and 'decimals' should be in sync with the 'decimals' in qgstemporalcontrollerwidgetbase.ui
194 mStepSpinBox->setDecimals( 3 );
195 // minimum timestep one millisecond
196 mStepSpinBox->setMinimum( 0.001 );
197 mStepSpinBox->setMaximum( std::numeric_limits<int>::max() );
198 mStepSpinBox->setSingleStep( 1 );
199 mStepSpinBox->setValue( 1 );
200
201 updateFrameDuration();
202
203 connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject );
204 connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded );
205 connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared );
206}
207
209{
210 return true;
211}
212
214{
215 if ( ( mAnimationSlider->hasFocus() || mMovieSlider->hasFocus() ) && e->key() == Qt::Key_Space )
216 {
217 mAnimationController->togglePause();
218 // connections will auto-sync mMovieController state!
219 }
221}
222
223void QgsTemporalControllerWidget::aboutToShowRangeMenu()
224{
225 QgsDateTimeRange projectRange;
226 if ( QgsProject::instance()->timeSettings() )
227 projectRange = QgsProject::instance()->timeSettings()->temporalRange();
228 mRangeSetToProjectAction->setEnabled( projectRange.begin().isValid() && projectRange.end().isValid() );
229
230 mRangeLayersSubMenu->clear();
231 for ( int i = 0; i < mMapLayerModel->rowCount(); ++i )
232 {
233 const QModelIndex index = mMapLayerModel->index( i, 0 );
234 QgsMapLayer *currentLayer = mMapLayerModel->data( index, static_cast< int >( QgsMapLayerModel::CustomRole::Layer ) ).value<QgsMapLayer *>();
235 if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
236 continue;
237
238 const QIcon icon = qvariant_cast<QIcon>( mMapLayerModel->data( index, Qt::DecorationRole ) );
239 const QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString();
240 const QgsDateTimeRange range = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
241 if ( range.begin().isValid() && range.end().isValid() )
242 {
243 QAction *action = new QAction( icon, text, mRangeLayersSubMenu.get() );
244 connect( action, &QAction::triggered, this, [ = ]
245 {
246 setDates( range );
247 saveRangeToProject();
248 } );
249 mRangeLayersSubMenu->addAction( action );
250 }
251 }
252 mRangeLayersSubMenu->setEnabled( !mRangeLayersSubMenu->actions().isEmpty() );
253}
254
255void QgsTemporalControllerWidget::updateTemporalExtent()
256{
257 // TODO - consider whether the overall time range set for animations should include the end date time or not.
258 // (currently it DOES include the end date time).
259 const QDateTime start = mStartDateTime->dateTime();
260 const QDateTime end = mEndDateTime->dateTime();
261 const bool isTimeInstant = start == end;
262 const QgsDateTimeRange temporalExtent = QgsDateTimeRange( start, end,
263 true, !isTimeInstant && mNavigationObject->navigationMode() == Qgis::TemporalNavigationMode::FixedRange ? false : true );
264 mNavigationObject->setTemporalExtents( temporalExtent );
265 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
266 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
267 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
268 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
269}
270
271void QgsTemporalControllerWidget::updateFrameDuration()
272{
273 if ( mBlockSettingUpdates )
274 return;
275
276 // save new settings into project
277 const Qgis::TemporalUnit unit = static_cast< Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() );
279 QgsProject::instance()->timeSettings()->setTimeStep( unit == Qgis::TemporalUnit::IrregularStep ? 1 : mStepSpinBox->value() );
280
281 if ( !mBlockFrameDurationUpdates )
282 {
283 mNavigationObject->setFrameDuration(
284 QgsInterval( QgsProject::instance()->timeSettings()->timeStep(),
285 QgsProject::instance()->timeSettings()->timeStepUnit() ) );
286 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
287 }
288 mAnimationSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
289 mAnimationSlider->setValue( mNavigationObject->currentFrameNumber() );
290 mMovieSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
291 mMovieSlider->setValue( mNavigationObject->currentFrameNumber() );
292
294 {
295 mStepSpinBox->setEnabled( false );
296 mStepSpinBox->setValue( 1 );
297 mAnimationSlider->setTickInterval( 1 );
298 mAnimationSlider->setTickPosition( QSlider::TicksBothSides );
299 }
300 else
301 {
302 mStepSpinBox->setEnabled( true );
303 mAnimationSlider->setTickInterval( 0 );
304 mAnimationSlider->setTickPosition( QSlider::NoTicks );
305 }
306}
307
308void QgsTemporalControllerWidget::setWidgetStateFromProject()
309{
310 mBlockSettingUpdates++;
311 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( QgsProject::instance()->timeSettings()->timeStepUnit() ) ) );
312 mStepSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() );
313 mBlockSettingUpdates--;
314
315 bool ok = false;
316 const Qgis::TemporalNavigationMode mode = static_cast< Qgis::TemporalNavigationMode>( QgsProject::instance()->readNumEntry( QStringLiteral( "TemporalControllerWidget" ),
317 QStringLiteral( "/NavigationMode" ), 0, &ok ) );
318 if ( ok )
319 {
320 mNavigationObject->setNavigationMode( mode );
321 setWidgetStateFromNavigationMode( mode );
322 }
323 else
324 {
326 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
327 }
328
329 mNavigationObject->setTotalMovieFrames( QgsProject::instance()->timeSettings()->totalMovieFrames() );
330 mTotalFramesSpinBox->setValue( QgsProject::instance()->timeSettings()->totalMovieFrames() );
331
332 const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) );
333 const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) );
334 if ( !startString.isEmpty() && !endString.isEmpty() )
335 {
336 whileBlocking( mStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
337 whileBlocking( mEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
338 whileBlocking( mFixedRangeStartDateTime )->setDateTime( QDateTime::fromString( startString, Qt::ISODateWithMs ) );
339 whileBlocking( mFixedRangeEndDateTime )->setDateTime( QDateTime::fromString( endString, Qt::ISODateWithMs ) );
340 }
341 else
342 {
343 setDatesToProjectTime( false );
344 }
345 updateTemporalExtent();
346 updateFrameDuration();
347
348 mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() );
349 mNavigationObject->setTemporalRangeCumulative( QgsProject::instance()->timeSettings()->isTemporalRangeCumulative() );
350}
351
352void QgsTemporalControllerWidget::mNavigationOff_clicked()
353{
354 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
355 static_cast<int>( Qgis::TemporalNavigationMode::Disabled ) );
356
358 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
359}
360
361void QgsTemporalControllerWidget::mNavigationFixedRange_clicked()
362{
363 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
364 static_cast<int>( Qgis::TemporalNavigationMode::FixedRange ) );
365
367 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::FixedRange );
368}
369
370void QgsTemporalControllerWidget::mNavigationAnimated_clicked()
371{
372 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
373 static_cast<int>( Qgis::TemporalNavigationMode::Animated ) );
374
376 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Animated );
377}
378
379void QgsTemporalControllerWidget::mNavigationMovie_clicked()
380{
381 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/NavigationMode" ),
382 static_cast<int>( Qgis::TemporalNavigationMode::Movie ) );
383
385 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Movie );
386}
387
388void QgsTemporalControllerWidget::setWidgetStateFromNavigationMode( const Qgis::TemporalNavigationMode mode )
389{
390 mNavigationOff->setChecked( mode == Qgis::TemporalNavigationMode::Disabled );
391 mNavigationFixedRange->setChecked( mode == Qgis::TemporalNavigationMode::FixedRange );
392 mNavigationAnimated->setChecked( mode == Qgis::TemporalNavigationMode::Animated );
393 mNavigationMovie->setChecked( mode == Qgis::TemporalNavigationMode::Movie );
394
395 switch ( mode )
396 {
398 mNavigationModeStackedWidget->setCurrentIndex( 0 );
399 break;
401 mNavigationModeStackedWidget->setCurrentIndex( 1 );
402 break;
404 mNavigationModeStackedWidget->setCurrentIndex( 2 );
405 break;
407 mNavigationModeStackedWidget->setCurrentIndex( 3 );
408 break;
409 }
410}
411
412void QgsTemporalControllerWidget::onLayersAdded( const QList<QgsMapLayer *> &layers )
413{
414 if ( !mHasTemporalLayersLoaded )
415 {
416 for ( QgsMapLayer *layer : layers )
417 {
418 if ( layer->temporalProperties() )
419 {
420 mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive();
421
422 if ( !mHasTemporalLayersLoaded )
423 {
424 connect( layer, &QgsMapLayer::dataSourceChanged, this, [ this, layer ]
425 {
426 if ( layer->isValid() && layer->temporalProperties()->isActive() && !mHasTemporalLayersLoaded )
427 {
428 mHasTemporalLayersLoaded = true;
429 firstTemporalLayerLoaded( layer );
430 mNavigationObject->setAvailableTemporalRanges( QgsTemporalUtils::usedTemporalRangesForProject( QgsProject::instance() ) );
431 }
432 } );
433 }
434
435 firstTemporalLayerLoaded( layer );
436 }
437 }
438 }
439
441}
442
443void QgsTemporalControllerWidget::firstTemporalLayerLoaded( QgsMapLayer *layer )
444{
445 setDatesToProjectTime( true );
446
447 if ( QgsMeshLayer *meshLayer = qobject_cast<QgsMeshLayer *>( layer ) )
448 {
449 mBlockFrameDurationUpdates++;
450 setTimeStep( meshLayer->firstValidTimeStep() );
451 mBlockFrameDurationUpdates--;
452 updateFrameDuration();
453 }
454 else if ( QgsRasterLayer *rasterLayer = qobject_cast<QgsRasterLayer *>( layer ) )
455 {
456 if ( rasterLayer->dataProvider() && rasterLayer->dataProvider()->temporalCapabilities() )
457 {
458 mBlockFrameDurationUpdates++;
459 setTimeStep( rasterLayer->dataProvider()->temporalCapabilities()->defaultInterval() );
460 mBlockFrameDurationUpdates--;
461 updateFrameDuration();
462 }
463 }
464}
465
466void QgsTemporalControllerWidget::onProjectCleared()
467{
468 mHasTemporalLayersLoaded = false;
469
471 setWidgetStateFromNavigationMode( Qgis::TemporalNavigationMode::Disabled );
472
473 // default to showing the last 24 hours, ending at the current date's hour, in one hour blocks...
474 // it's COMPLETELY arbitrary, but better than starting with a "zero length" duration!
475 const QTime startOfCurrentHour = QTime( QTime::currentTime().hour(), 0, 0 );
476 const QDateTime end = QDateTime( QDate::currentDate(), startOfCurrentHour, Qt::UTC );
477 const QDateTime start = end.addSecs( -24 * 60 * 60 );
478
479 whileBlocking( mStartDateTime )->setDateTime( start );
480 whileBlocking( mEndDateTime )->setDateTime( end );
481 whileBlocking( mFixedRangeStartDateTime )->setDateTime( start );
482 whileBlocking( mFixedRangeEndDateTime )->setDateTime( end );
483
484 updateTemporalExtent();
485 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Hours ) ) );
486 mStepSpinBox->setValue( 1 );
487}
488
489void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range )
490{
491 whileBlocking( mAnimationSlider )->setValue( mNavigationObject->currentFrameNumber() );
492 whileBlocking( mMovieSlider )->setValue( mNavigationObject->currentFrameNumber() );
493 updateRangeLabel( range );
494}
495
496void QgsTemporalControllerWidget::totalMovieFramesChanged( long long frames )
497{
499 mTotalFramesSpinBox->setValue( frames );
500 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( frames ) );
501}
502
503void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range )
504{
505 QString timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss" );
506 // but if timesteps are < 1 second (as: in milliseconds), add milliseconds to the format
507 if ( static_cast< Qgis::TemporalUnit >( mTimeStepsComboBox->currentData().toInt() ) == Qgis::TemporalUnit::Milliseconds )
508 timeFrameFormat = QStringLiteral( "yyyy-MM-dd HH:mm:ss.zzz" );
509 switch ( mNavigationObject->navigationMode() )
510 {
512 mCurrentRangeLabel->setText( tr( "Current frame: %1 ≤ <i>t</i> &lt; %2" ).arg(
513 range.begin().toString( timeFrameFormat ),
514 range.end().toString( timeFrameFormat ) ) );
515 break;
517 mCurrentRangeLabel->setText( tr( "Range: %1 ≤ <i>t</i> &lt; %2" ).arg(
518 range.begin().toString( timeFrameFormat ),
519 range.end().toString( timeFrameFormat ) ) );
520 break;
522 mCurrentRangeLabel->setText( tr( "Temporal navigation disabled" ) );
523 break;
525 mCurrentRangeLabel->setText( tr( "Current frame: %1/%2" ).arg( mNavigationObject->currentFrameNumber() ).arg( mNavigationObject->totalMovieFrames() ) );
526 break;
527 }
528}
529
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 Qgis::TemporalUnit unit = static_cast<Qgis::TemporalUnit>( mTimeStepsComboBox->itemData( i ).toInt() );
603 const double value = timeStep.seconds() * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Seconds, 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< Qgis::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) ) )
667 return;
668
669 if ( timeStep.originalUnit() != Qgis::TemporalUnit::Unknown )
670 {
671 mStepSpinBox->setValue( timeStep.originalDuration() );
672 mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( static_cast< int >( timeStep.originalUnit() ) ) );
673 }
674
675 updateFrameDuration();
676}
677
678void QgsTemporalControllerWidget::mRangeSetToProjectAction_triggered()
679{
680 setDatesToProjectTime( false );
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( bool tryLastStoredRange )
706{
707 QgsDateTimeRange range;
708
709 if ( tryLastStoredRange )
710 {
711 const QString startString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/StartDateTime" ) );
712 const QString endString = QgsProject::instance()->readEntry( QStringLiteral( "TemporalControllerWidget" ), QStringLiteral( "/EndDateTime" ) );
713 if ( !startString.isEmpty() && !endString.isEmpty() )
714 {
715 range = QgsDateTimeRange( QDateTime::fromString( startString, Qt::ISODateWithMs ),
716 QDateTime::fromString( endString, Qt::ISODateWithMs ) );
717 }
718 }
719
720 // by default try taking the project's fixed temporal extent
721 if ( ( !range.begin().isValid() || !range.end().isValid() ) && QgsProject::instance()->timeSettings() )
723
724 // if that's not set, calculate the extent from the project's layers
725 if ( !range.begin().isValid() || !range.end().isValid() )
726 {
728 }
729
731
732 setDates( range );
733}
734
735void QgsTemporalControllerWidget::saveRangeToProject()
736{
737 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
738 QStringLiteral( "/StartDateTime" ), mStartDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
739 QgsProject::instance()->writeEntry( QStringLiteral( "TemporalControllerWidget" ),
740 QStringLiteral( "/EndDateTime" ), mEndDateTime->dateTime().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODateWithMs ) );
741}
TemporalNavigationMode
Temporal navigation modes.
Definition qgis.h:2366
@ Animated
Temporal navigation relies on frames within a datetime range.
@ Movie
Movie mode – behaves like a video player, with a fixed frame duration and no temporal range.
@ FixedRange
Temporal navigation relies on a fixed datetime range.
@ Disabled
Temporal navigation is disabled.
PlaybackOperation
Media playback operations.
Definition qgis.h:2395
@ Pause
Pause playback.
@ PlayReverse
Play in reverse.
@ PlayForward
Play forward.
@ SkipToStart
Jump to start of playback.
@ PreviousFrame
Step to previous frame.
@ SkipToEnd
Jump to end of playback.
@ NextFrame
Step to next frame.
TemporalUnit
Temporal units.
Definition qgis.h:4795
@ IrregularStep
Special 'irregular step' time unit, used for temporal data which uses irregular, non-real-world unit ...
@ Milliseconds
Milliseconds.
@ Unknown
Unknown time unit.
@ Centuries
Centuries.
AnimationState
Animation states.
Definition qgis.h:2382
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A representation of the interval between two datetime values.
Definition qgsinterval.h:46
double originalDuration() const
Returns the original interval duration.
bool isValid() const
Returns true if the interval is valid.
double seconds() const
Returns the interval duration in seconds.
Qgis::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
The QgsMapLayerModel class is a model to display layers in widgets.
int rowCount(const QModelIndex &parent=QModelIndex()) const override
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
@ Layer
Stores pointer to the map layer itself.
virtual QgsDateTimeRange calculateTemporalExtent(QgsMapLayer *layer) const
Attempts to calculate the overall temporal extent for the specified layer, using the settings defined...
Base class for all map layer types.
Definition qgsmaplayer.h:76
void dataSourceChanged()
Emitted whenever the layer's data source has been changed.
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
Represents a mesh layer supporting display of data on structured or unstructured meshes.
Base class for any widget that can be shown as a inline panel.
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
void keyPressEvent(QKeyEvent *event) override
Overridden key press event to handle the esc event on the widget.
void operationTriggered(Qgis::PlaybackOperation operation)
Emitted when a playback operation is triggered.
QgsDateTimeRange temporalRange() const
Returns the project's temporal range, which indicates the earliest and latest datetime ranges associa...
void setTotalMovieFrames(long long frames)
Sets the total number of frames for the movie.
void setFramesPerSecond(double rate)
Sets the project's default animation frame rate, in frames per second.
Qgis::TemporalUnit timeStepUnit() const
Returns the project's time step (length of one animation frame) unit, which is used as the default va...
void setTimeStepUnit(Qgis::TemporalUnit unit)
Sets the project's time step (length of one animation frame) unit, which is used as the default value...
void setIsTemporalRangeCumulative(bool state)
Sets the project's temporal range as cumulative in animation settings.
void setTimeStep(double step)
Sets the project's time step (length of one animation frame), which is used as the default value when...
int readNumEntry(const QString &scope, const QString &key, int def=0, bool *ok=nullptr) const
Reads an integer from the specified scope and key.
static QgsProject * instance()
Returns the QgsProject singleton instance.
void cleared()
Emitted when the project is cleared (and additionally when an open project is cleared just before a n...
QString readEntry(const QString &scope, const QString &key, const QString &def=QString(), bool *ok=nullptr) const
Reads a string from the specified scope and key.
bool writeEntry(const QString &scope, const QString &key, bool value)
Write a boolean value to the project file.
void readProject(const QDomDocument &document)
Emitted when a project is being read.
void layersAdded(const QList< QgsMapLayer * > &layers)
Emitted when one or more layers were added to the registry.
const QgsProjectTimeSettings * timeSettings() const
Returns the project's time settings, which contains the project's temporal range and other time based...
Represents a raster layer.
bool applySizeConstraintsToStack() const override
Returns true if the size constraints and hints for the panel widget should be applied to the parent Q...
void exportAnimation()
Triggered when an animation should be exported.
void keyPressEvent(QKeyEvent *e) override
QgsTemporalNavigationObject * temporalController()
Returns the temporal controller object used by this object in navigation.
QgsTemporalControllerWidget(QWidget *parent=nullptr)
Constructor for QgsTemporalControllerWidget, with the specified parent widget.
void updateTemporalRange(const QgsDateTimeRange &range)
Signals that a temporal range has changed and needs to be updated in all connected objects.
Implements a temporal controller based on a frame by frame navigation and animation.
void stateChanged(Qgis::AnimationState state)
Emitted whenever the animation state changes.
long long totalMovieFrames() const
Returns the total number of frames for the movie.
void previous()
Jumps back to the previous frame.
double framesPerSecond() const
Returns the animation frame rate, in frames per second.
void setAvailableTemporalRanges(const QList< QgsDateTimeRange > &ranges)
Sets the list of all available temporal ranges which have data available.
void setFrameDuration(const QgsInterval &duration)
Sets the frame duration, which dictates the temporal length of each frame in the animation.
void navigationModeChanged(Qgis::TemporalNavigationMode mode)
Emitted whenever the navigation mode changes.
void setNavigationMode(const Qgis::TemporalNavigationMode mode)
Sets the temporal navigation mode.
void playForward()
Starts the animation playing in a forward direction up till the end of all frames.
long long currentFrameNumber() const
Returns the current frame number.
void rewindToStart()
Rewinds the temporal navigation to start of the temporal extent.
void pause()
Pauses the temporal navigation.
void setCurrentFrameNumber(long long frame)
Sets the current animation frame number.
long long totalFrameCount() const
Returns the total number of frames for the navigation.
void skipToEnd()
Skips the temporal navigation to end of the temporal extent.
void temporalFrameDurationChanged(const QgsInterval &interval)
Emitted whenever the frameDuration interval of the controller changes.
void setFramesPerSecond(double rate)
Sets the animation frame rate, in frames per second.
bool temporalRangeCumulative() const
Returns the animation temporal range cumulative settings.
void next()
Advances to the next frame.
void totalMovieFramesChanged(long long frames)
Emitted whenever the total number of frames in the movie is changed.
void setTotalMovieFrames(long long frames)
Sets the total number of frames for the movie.
void setTemporalExtents(const QgsDateTimeRange &extents)
Sets the navigation temporal extents, which dictate the earliest and latest date time possible in the...
Qgis::TemporalNavigationMode navigationMode() const
Returns the current temporal navigation mode.
void setTemporalRangeCumulative(bool state)
Sets the animation temporal range as cumulative.
void setLooping(bool loop)
Sets whether the animation should loop after hitting the end or start frame.
void playBackward()
Starts the animation playing in a reverse direction until the beginning of the time range.
void temporalExtentsChanged(const QgsDateTimeRange &extent)
Emitted whenever the temporalExtent extent changes.
bool isActive() const
Returns true if the temporal property is active.
T begin() const
Returns the beginning of the range.
Definition qgsrange.h:444
T end() const
Returns the upper bound of the range.
Definition qgsrange.h:451
static QgsDateTimeRange calculateTemporalRangeForProject(QgsProject *project)
Calculates the temporal range for a project.
static QList< QgsDateTimeRange > usedTemporalRangesForProject(QgsProject *project)
Calculates all temporal ranges which are in use for a project.
static Q_INVOKABLE QString toString(Qgis::DistanceUnit unit)
Returns a translated string representing a distance unit.
static Q_INVOKABLE double fromUnitToUnitFactor(Qgis::DistanceUnit fromUnit, Qgis::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
QgsSignalBlocker< Object > whileBlocking(Object *object)
Temporarily blocks signals from a QObject while calling a single method from the object.
Definition qgis.h:5787
QgsTemporalRange< QDateTime > QgsDateTimeRange
QgsRange which stores a range of date times.
Definition qgsrange.h:742
int precision