Skip to content
Permalink
Browse files

[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
  • Loading branch information
nyalldawson committed Mar 25, 2021
1 parent 9c1ddfc commit 7434c1b34fc6fba226ea037fc722a13c41c26ecd
@@ -133,7 +133,7 @@ Returns the current frame number.
.. seealso:: :py:func:`setCurrentFrameNumber`
%End

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

@@ -93,8 +93,20 @@ QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long

const long long nextFrame = frame + 1;

const QDateTime begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
const QDateTime end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
QDateTime begin;
QDateTime end;
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
{
if ( mAllRanges.empty() )
return QgsDateTimeRange();

return frame < mAllRanges.size() ? mAllRanges.at( frame ) : mAllRanges.constLast();
}
else
{
begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
}

QDateTime frameStart = begin;

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

void QgsTemporalNavigationObject::setFrameDuration( QgsInterval frameDuration )
void QgsTemporalNavigationObject::setFrameDuration( const QgsInterval &frameDuration )
{
if ( mFrameDuration == frameDuration )
{
return;
}

QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
mFrameDuration = frameDuration;

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

long long QgsTemporalNavigationObject::totalFrameCount() const
{
QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
return std::floor( totalAnimationLength.seconds() / mFrameDuration.seconds() ) + 1;
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
{
return mAllRanges.count();
}
else
{
QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
return std::floor( totalAnimationLength.seconds() / mFrameDuration.seconds() ) + 1;
}
}

void QgsTemporalNavigationObject::setAnimationState( AnimationState mode )
@@ -323,30 +343,46 @@ QgsTemporalNavigationObject::AnimationState QgsTemporalNavigationObject::animati
long long QgsTemporalNavigationObject::findBestFrameNumberForFrameStart( const QDateTime &frameStart ) const
{
long long bestFrame = 0;
QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
// Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
long long roughFrameStart = 0;
long long roughFrameEnd = totalFrameCount();
// For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
// large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
if ( mFrameDuration.originalUnit() != QgsUnitTypes::TemporalMonths && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalYears && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalDecades && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalCenturies )
if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
{
// Only if we receive a valid frameStart, that is within current mTemporalExtents
// We tend to receive a framestart of 'now()' upon startup for example
if ( mTemporalExtents.contains( frameStart ) )
for ( const QgsDateTimeRange &range : mAllRanges )
{
roughFrameStart = std::floor( ( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() );
if ( range.contains( frameStart ) )
return bestFrame;
else if ( range.begin() > frameStart )
// if we've gone past the target date, go back one frame if possible
return std::max( 0LL, bestFrame - 1 );
bestFrame++;
}
roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
return mAllRanges.count() - 1;
}
for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
else
{
QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
if ( range.overlaps( testFrame ) )
QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
// Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
long long roughFrameStart = 0;
long long roughFrameEnd = totalFrameCount();
// For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
// large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
if ( mFrameDuration.originalUnit() != QgsUnitTypes::TemporalMonths && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalYears && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalDecades && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalCenturies )
{
bestFrame = i;
break;
// Only if we receive a valid frameStart, that is within current mTemporalExtents
// We tend to receive a framestart of 'now()' upon startup for example
if ( mTemporalExtents.contains( frameStart ) )
{
roughFrameStart = std::floor( ( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() );
}
roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
}
for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
{
QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
if ( range.overlaps( testFrame ) )
{
bestFrame = i;
break;
}
}
return bestFrame;
}
return bestFrame;
}
@@ -155,7 +155,7 @@ class CORE_EXPORT QgsTemporalNavigationObject : public QgsTemporalController, pu
*
* \see frameDuration()
*/
void setFrameDuration( QgsInterval duration );
void setFrameDuration( const QgsInterval &duration );

/**
* Returns the current set frame duration, which dictates the temporal length of each frame in the animation.
@@ -140,10 +140,11 @@ QgsTemporalControllerWidget::QgsTemporalControllerWidget( QWidget *parent )
QgsUnitTypes::TemporalMonths,
QgsUnitTypes::TemporalYears,
QgsUnitTypes::TemporalDecades,
QgsUnitTypes::TemporalCenturies
QgsUnitTypes::TemporalCenturies,
QgsUnitTypes::TemporalIrregularStep,
} )
{
mTimeStepsComboBox->addItem( QgsUnitTypes::toString( u ), u );
mTimeStepsComboBox->addItem( u != QgsUnitTypes::TemporalIrregularStep ? QgsUnitTypes::toString( u ) : tr( "source timestamps" ), u );
}

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

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

if ( !mBlockFrameDurationUpdates )
{
@@ -302,6 +304,20 @@ void QgsTemporalControllerWidget::updateFrameDuration()
}
mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
mSlider->setValue( mNavigationObject->currentFrameNumber() );

if ( unit == QgsUnitTypes::TemporalIrregularStep )
{
mStepSpinBox->setEnabled( false );
mStepSpinBox->setValue( 1 );
mSlider->setTickInterval( 1 );
mSlider->setTickPosition( QSlider::TicksBothSides );
}
else
{
mStepSpinBox->setEnabled( true );
mSlider->setTickInterval( 0 );
mSlider->setTickPosition( QSlider::NoTicks );
}
}

void QgsTemporalControllerWidget::setWidgetStateFromProject()
@@ -44,6 +44,7 @@ class TestQgsTemporalNavigationObject : public QObject
void frameSettings();
void navigationMode();
void expressionContext();
void testIrregularStep();

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

void TestQgsTemporalNavigationObject::testIrregularStep()
{
// test using the navigation in irregular step mode
QgsTemporalNavigationObject object;
QList< QgsDateTimeRange > ranges{ QgsDateTimeRange(
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ),
QgsDateTimeRange(
QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ),
QgsDateTimeRange(
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) )
};
object.setAvailableTemporalRanges( ranges );

object.setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalIrregularStep ) );

QCOMPARE( object.totalFrameCount(), 3LL );

QCOMPARE( object.dateTimeRangeForFrameNumber( 0 ), QgsDateTimeRange(
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ) );
// negative should return first frame range
QCOMPARE( object.dateTimeRangeForFrameNumber( -1 ), QgsDateTimeRange(
QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ) );
QCOMPARE( object.dateTimeRangeForFrameNumber( 1 ), QgsDateTimeRange(
QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ) );
QCOMPARE( object.dateTimeRangeForFrameNumber( 2 ), QgsDateTimeRange(
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ) );
QCOMPARE( object.dateTimeRangeForFrameNumber( 5 ), QgsDateTimeRange(
QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ),
QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ) );

QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2019, 1, 1 ), QTime() ) ), 0LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 10 ), QTime( 0, 0, 0 ) ) ), 0LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 11 ), QTime( 0, 0, 0 ) ) ), 0LL );
// in between available ranges, go back a frame
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 12 ), QTime( 0, 0, 0 ) ) ), 0LL );

QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 15 ), QTime( 0, 0, 0 ) ) ), 1LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 16 ), QTime( 0, 0, 0 ) ) ), 1LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 1, 20 ), QTime( 0, 0, 0 ) ) ), 1LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 2, 15 ), QTime( 0, 0, 0 ) ) ), 1LL );

QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 3, 1 ), QTime( 0, 0, 0 ) ) ), 2LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 3, 2 ), QTime( 0, 0, 0 ) ) ), 2LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 4, 5 ), QTime( 0, 0, 0 ) ) ), 2LL );
QCOMPARE( object.findBestFrameNumberForFrameStart( QDateTime( QDate( 2020, 5, 6 ), QTime( 0, 0, 0 ) ) ), 2LL );
}

QGSTEST_MAIN( TestQgsTemporalNavigationObject )
#include "testqgstemporalnavigationobject.moc"

0 comments on commit 7434c1b

Please sign in to comment.