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