Skip to content

Commit 7434c1b

Browse files
committedMar 25, 2021
[feature] Add temporal navigation step for "source timestamps"
When selected, this causes the temporal navigation to step between all available time ranges from layers in the project. It's useful when a project contains layers with non-contiguous available times, e.g. from a WMS-T which images available at irregular dates, and you want to only step between time ranges where the next available image is shown. Refs Natural resources Canada Contract: 3000720707
1 parent 9c1ddfc commit 7434c1b

File tree

5 files changed

+136
-29
lines changed

5 files changed

+136
-29
lines changed
 

‎python/core/auto_generated/qgstemporalnavigationobject.sip.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Returns the current frame number.
133133
.. seealso:: :py:func:`setCurrentFrameNumber`
134134
%End
135135

136-
void setFrameDuration( QgsInterval duration );
136+
void setFrameDuration( const QgsInterval &duration );
137137
%Docstring
138138
Sets the frame ``duration``, which dictates the temporal length of each frame in the animation.
139139

‎src/core/qgstemporalnavigationobject.cpp

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,20 @@ QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long
9393

9494
const long long nextFrame = frame + 1;
9595

96-
const QDateTime begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
97-
const QDateTime end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
96+
QDateTime begin;
97+
QDateTime end;
98+
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
99+
{
100+
if ( mAllRanges.empty() )
101+
return QgsDateTimeRange();
102+
103+
return frame < mAllRanges.size() ? mAllRanges.at( frame ) : mAllRanges.constLast();
104+
}
105+
else
106+
{
107+
begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
108+
end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
109+
}
98110

99111
QDateTime frameStart = begin;
100112

@@ -196,12 +208,13 @@ long long QgsTemporalNavigationObject::currentFrameNumber() const
196208
return mCurrentFrameNumber;
197209
}
198210

199-
void QgsTemporalNavigationObject::setFrameDuration( QgsInterval frameDuration )
211+
void QgsTemporalNavigationObject::setFrameDuration( const QgsInterval &frameDuration )
200212
{
201213
if ( mFrameDuration == frameDuration )
202214
{
203215
return;
204216
}
217+
205218
QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
206219
mFrameDuration = frameDuration;
207220

@@ -302,8 +315,15 @@ void QgsTemporalNavigationObject::skipToEnd()
302315

303316
long long QgsTemporalNavigationObject::totalFrameCount() const
304317
{
305-
QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
306-
return std::floor( totalAnimationLength.seconds() / mFrameDuration.seconds() ) + 1;
318+
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
319+
{
320+
return mAllRanges.count();
321+
}
322+
else
323+
{
324+
QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
325+
return std::floor( totalAnimationLength.seconds() / mFrameDuration.seconds() ) + 1;
326+
}
307327
}
308328

309329
void QgsTemporalNavigationObject::setAnimationState( AnimationState mode )
@@ -323,30 +343,46 @@ QgsTemporalNavigationObject::AnimationState QgsTemporalNavigationObject::animati
323343
long long QgsTemporalNavigationObject::findBestFrameNumberForFrameStart( const QDateTime &frameStart ) const
324344
{
325345
long long bestFrame = 0;
326-
QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
327-
// Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
328-
long long roughFrameStart = 0;
329-
long long roughFrameEnd = totalFrameCount();
330-
// For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
331-
// large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
332-
if ( mFrameDuration.originalUnit() != QgsUnitTypes::TemporalMonths && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalYears && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalDecades && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalCenturies )
346+
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
333347
{
334-
// Only if we receive a valid frameStart, that is within current mTemporalExtents
335-
// We tend to receive a framestart of 'now()' upon startup for example
336-
if ( mTemporalExtents.contains( frameStart ) )
348+
for ( const QgsDateTimeRange &range : mAllRanges )
337349
{
338-
roughFrameStart = std::floor( ( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() );
350+
if ( range.contains( frameStart ) )
351+
return bestFrame;
352+
else if ( range.begin() > frameStart )
353+
// if we've gone past the target date, go back one frame if possible
354+
return std::max( 0LL, bestFrame - 1 );
355+
bestFrame++;
339356
}
340-
roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
357+
return mAllRanges.count() - 1;
341358
}
342-
for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
359+
else
343360
{
344-
QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
345-
if ( range.overlaps( testFrame ) )
361+
QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
362+
// Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
363+
long long roughFrameStart = 0;
364+
long long roughFrameEnd = totalFrameCount();
365+
// For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
366+
// large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
367+
if ( mFrameDuration.originalUnit() != QgsUnitTypes::TemporalMonths && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalYears && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalDecades && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalCenturies )
346368
{
347-
bestFrame = i;
348-
break;
369+
// Only if we receive a valid frameStart, that is within current mTemporalExtents
370+
// We tend to receive a framestart of 'now()' upon startup for example
371+
if ( mTemporalExtents.contains( frameStart ) )
372+
{
373+
roughFrameStart = std::floor( ( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() );
374+
}
375+
roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
376+
}
377+
for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
378+
{
379+
QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
380+
if ( range.overlaps( testFrame ) )
381+
{
382+
bestFrame = i;
383+
break;
384+
}
349385
}
386+
return bestFrame;
350387
}
351-
return bestFrame;
352388
}

‎src/core/qgstemporalnavigationobject.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class CORE_EXPORT QgsTemporalNavigationObject : public QgsTemporalController, pu
155155
*
156156
* \see frameDuration()
157157
*/
158-
void setFrameDuration( QgsInterval duration );
158+
void setFrameDuration( const QgsInterval &duration );
159159

160160
/**
161161
* Returns the current set frame duration, which dictates the temporal length of each frame in the animation.

‎src/gui/qgstemporalcontrollerwidget.cpp

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ QgsTemporalControllerWidget::QgsTemporalControllerWidget( QWidget *parent )
140140
QgsUnitTypes::TemporalMonths,
141141
QgsUnitTypes::TemporalYears,
142142
QgsUnitTypes::TemporalDecades,
143-
QgsUnitTypes::TemporalCenturies
143+
QgsUnitTypes::TemporalCenturies,
144+
QgsUnitTypes::TemporalIrregularStep,
144145
} )
145146
{
146-
mTimeStepsComboBox->addItem( QgsUnitTypes::toString( u ), u );
147+
mTimeStepsComboBox->addItem( u != QgsUnitTypes::TemporalIrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), u );
147148
}
148149

149150
// TODO: might want to choose an appropriate default unit based on the range
@@ -290,8 +291,9 @@ void QgsTemporalControllerWidget::updateFrameDuration()
290291
return;
291292

292293
// save new settings into project
293-
QgsProject::instance()->timeSettings()->setTimeStepUnit( static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) );
294-
QgsProject::instance()->timeSettings()->setTimeStep( mStepSpinBox->value() );
294+
QgsUnitTypes::TemporalUnit unit = static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() );
295+
QgsProject::instance()->timeSettings()->setTimeStepUnit( unit );
296+
QgsProject::instance()->timeSettings()->setTimeStep( unit == QgsUnitTypes::TemporalIrregularStep ? 1 : mStepSpinBox->value() );
295297

296298
if ( !mBlockFrameDurationUpdates )
297299
{
@@ -302,6 +304,20 @@ void QgsTemporalControllerWidget::updateFrameDuration()
302304
}
303305
mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
304306
mSlider->setValue( mNavigationObject->currentFrameNumber() );
307+
308+
if ( unit == QgsUnitTypes::TemporalIrregularStep )
309+
{
310+
mStepSpinBox->setEnabled( false );
311+
mStepSpinBox->setValue( 1 );
312+
mSlider->setTickInterval( 1 );
313+
mSlider->setTickPosition( QSlider::TicksBothSides );
314+
}
315+
else
316+
{
317+
mStepSpinBox->setEnabled( true );
318+
mSlider->setTickInterval( 0 );
319+
mSlider->setTickPosition( QSlider::NoTicks );
320+
}
305321
}
306322

307323
void QgsTemporalControllerWidget::setWidgetStateFromProject()

‎tests/src/core/testqgstemporalnavigationobject.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class TestQgsTemporalNavigationObject : public QObject
4444
void frameSettings();
4545
void navigationMode();
4646
void expressionContext();
47+
void testIrregularStep();
4748

4849
private:
4950
QgsTemporalNavigationObject *navigationObject = nullptr;
@@ -262,5 +263,59 @@ void TestQgsTemporalNavigationObject::expressionContext()
262263
QCOMPARE( scope->variable( QStringLiteral( "animation_interval" ) ).value< QgsInterval >(), range.end() - range.begin() );
263264
}
264265

266+
void TestQgsTemporalNavigationObject::testIrregularStep()
267+
{
268+
// test using the navigation in irregular step mode
269+
QgsTemporalNavigationObject object;
270+
QList< QgsDateTimeRange > ranges{ QgsDateTimeRange(
271+
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
272+
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ),
273+
QgsDateTimeRange(
274+
QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ),
275+
QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ),
276+
QgsDateTimeRange(
277+
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
278+
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) )
279+
};
280+
object.setAvailableTemporalRanges( ranges );
281+
282+
object.setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalIrregularStep ) );
283+
284+
QCOMPARE( object.totalFrameCount(), 3LL );
285+
286+
QCOMPARE( object.dateTimeRangeForFrameNumber( 0 ), QgsDateTimeRange(
287+
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
288+
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ) );
289+
// negative should return first frame range
290+
QCOMPARE( object.dateTimeRangeForFrameNumber( -1 ), QgsDateTimeRange(
291+
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
292+
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ) );
293+
QCOMPARE( object.dateTimeRangeForFrameNumber( 1 ), QgsDateTimeRange(
294+
QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ),
295+
QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ) );
296+
QCOMPARE( object.dateTimeRangeForFrameNumber( 2 ), QgsDateTimeRange(
297+
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
298+
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ) );
299+
QCOMPARE( object.dateTimeRangeForFrameNumber( 5 ), QgsDateTimeRange(
300+
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
301+
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ) );
302+
303+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2019, 1, 1 ), QTime() ) ), 0LL );
304+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ) ), 0LL );
305+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ), 0LL );
306+
// in between available ranges, go back a frame
307+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 12 ), QTime( 0, 0, 0 ) ) ), 0LL );
308+
309+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ) ), 1LL );
310+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 16 ), QTime( 0, 0, 0 ) ) ), 1LL );
311+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ), 1LL );
312+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 2, 15 ), QTime( 0, 0, 0 ) ) ), 1LL );
313+
314+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ) ), 2LL );
315+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 3, 2 ), QTime( 0, 0, 0 ) ) ), 2LL );
316+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ), 2LL );
317+
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 5, 6 ), QTime( 0, 0, 0 ) ) ), 2LL );
318+
}
319+
265320
QGSTEST_MAIN( TestQgsTemporalNavigationObject )
266321
#include "testqgstemporalnavigationobject.moc"

0 commit comments

Comments
 (0)
Please sign in to comment.