Skip to content

Commit

Permalink
[feature] Add temporal navigation step for "source timestamps"
Browse files Browse the repository at this point in the history
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 7434c1b
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 29 deletions.
Expand Up @@ -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.

Expand Down
82 changes: 59 additions & 23 deletions src/core/qgstemporalnavigationobject.cpp
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 )
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion src/core/qgstemporalnavigationobject.h
Expand Up @@ -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.
Expand Down
24 changes: 20 additions & 4 deletions src/gui/qgstemporalcontrollerwidget.cpp
Expand Up @@ -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
Expand Down Expand Up @@ -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 )
{
Expand All @@ -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()
Expand Down
55 changes: 55 additions & 0 deletions tests/src/core/testqgstemporalnavigationobject.cpp
Expand Up @@ -44,6 +44,7 @@ class TestQgsTemporalNavigationObject : public QObject
void frameSettings();
void navigationMode();
void expressionContext();
void testIrregularStep();

private:
QgsTemporalNavigationObject *navigationObject = nullptr;
Expand Down Expand Up @@ -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.