mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-07 00:15:48 -04:00
[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
This commit is contained in:
parent
9c1ddfcf78
commit
7434c1b34f
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user