Skip to content
Permalink
Browse files

Pass time step and time step unit into QgsTemporalNavigationObject

Currently, we pass the frame duration as a QgsInterval and use the average
duration of a month or year during the animation, for instance, 30 days
rather than a month. This makes it impossible to have an animation that
displays on a particular day each month, as the day in the next month will
change depending on the number of days in the previous month.

This changes QgsTemporalNavigationObject to take the time step and time
step unit as separate arguments. The settings in
QgsTemporalUtils::exportAnimation are left unchanged, because in this case
the user interface is already set up to use an interval.

If the time step has a fractional value, the frame duration is calculated
using a QgsInterval as before. If it has an integer value, the calculation
uses QDateTime to advance by the specified time step instead. So a value of
1.5 months results in a frame duration of 45 days, but a value of 1 month
will result in a duration that depends on the length of the current month.

Fixes #37829.
  • Loading branch information
dminor authored and nyalldawson committed Jan 6, 2021
1 parent 3ca2137 commit 9da580cb3d4e58f530486ffbf6ec77ec4d3601d9
@@ -109,22 +109,44 @@ Returns the current frame number.
.. seealso:: :py:func:`setCurrentFrameNumber`
%End

void setFrameDuration( QgsInterval duration );
void setFrameTimeStep( double timeStep );
%Docstring
Sets the frame ``duration``, which dictates the temporal length of each frame in the animation.
Sets the frame ``timestep``, which together with the frame timestep unit dictates
the temporal length of each frame in the animation.

.. note::

Calling this will reset the :py:func:`~QgsTemporalNavigationObject.currentFrameNumber` to the closest temporal match for the previous temporal range.

.. seealso:: :py:func:`frameDuration`
.. seealso:: :py:func:`frameFrameTimeStep`
%End

QgsInterval frameDuration() const;
void setFrameTimeStepUnit( QgsUnitTypes::TemporalUnit timeStepUnit );
%Docstring
Returns the current set frame duration, which dictates the temporal length of each frame in the animation.
Sets the frame ``timestep`` unit, which together with the frame time step dictates
the temporal length of each frame in the animation.

.. seealso:: :py:func:`setFrameDuration`
.. note::

Calling this will reset the :py:func:`~QgsTemporalNavigationObject.currentFrameNumber` to the first frame.

.. seealso:: :py:func:`frameTimeStepUnit`
%End

double frameTimeStep() const;
%Docstring
Returns the current set frame timestep, which together with frame timestep unit
dictates the temporal length of each frame in the animation.

.. seealso:: :py:func:`setFrameTimeStep`
%End

QgsUnitTypes::TemporalUnit frameTimeStepUnit() const;
%Docstring
Returns the current set frame timestep unit, which together with frame timestep
dictates the temporal length of each frame in the animation.

.. seealso:: :py:func:`setFrameTimeStepUnit`
%End

QgsDateTimeRange dateTimeRangeForFrameNumber( long long frame ) const;
@@ -74,7 +74,8 @@ QgsExpressionContextScope *QgsTemporalNavigationObject::createExpressionContextS
std::unique_ptr< QgsExpressionContextScope > scope = qgis::make_unique< QgsExpressionContextScope >( QStringLiteral( "temporal" ) );
scope->setVariable( QStringLiteral( "frame_rate" ), mFramesPerSecond, true );
scope->setVariable( QStringLiteral( "frame_number" ), mCurrentFrameNumber, true );
scope->setVariable( QStringLiteral( "frame_duration" ), mFrameDuration, true );
scope->setVariable( QStringLiteral( "frame_timestep" ), mFrameTimeStep, true );
scope->setVariable( QStringLiteral( "frame_timestepunit" ), mFrameTimeStepUnit, true );
scope->setVariable( QStringLiteral( "animation_start_time" ), mTemporalExtents.begin(), true );
scope->setVariable( QStringLiteral( "animation_end_time" ), mTemporalExtents.end(), true );
scope->setVariable( QStringLiteral( "animation_interval" ), mTemporalExtents.end() - mTemporalExtents.begin(), true );
@@ -90,8 +91,70 @@ QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long

const long long nextFrame = frame + 1;

const QDateTime begin = start.addSecs( frame * mFrameDuration.seconds() );
const QDateTime end = start.addSecs( nextFrame * mFrameDuration.seconds() );
QDateTime begin;
QDateTime end;

// If mFrameTimeStep is fractional, we use QgsInterval to determine the
// duration of the frame, which uses average durations for months and years.
// Otherwise, we use QDateTime to advance by the exact duration of the current
// month or year. So a time step of 1.5 months will result in a duration of 45
// days, but a time step of 1 month will result in a duration that depends upon
// the number of days in the current month.
if ( mFrameTimeStepIsFractional )
{
double duration = QgsInterval( mFrameTimeStep, mFrameTimeStepUnit ).seconds();
begin = start.addSecs( frame * duration );
end = start.addSecs( nextFrame * duration );
}
else
{
switch ( mFrameTimeStepUnit )
{
case QgsUnitTypes::TemporalUnit::TemporalMilliseconds:
begin = start.addMSecs( frame * mFrameTimeStep );
end = start.addMSecs( nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalSeconds:
begin = start.addSecs( frame * mFrameTimeStep );
end = start.addSecs( nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalMinutes:
begin = start.addSecs( 60 * frame * mFrameTimeStep );
end = start.addSecs( 60 * nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalHours:
begin = start.addSecs( 3600 * frame * mFrameTimeStep );
end = start.addSecs( 3600 * nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalDays:
begin = start.addDays( frame * mFrameTimeStep );
end = start.addDays( nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalWeeks:
begin = start.addDays( 7 * frame * mFrameTimeStep );
end = start.addDays( 7 * nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalMonths:
begin = start.addMonths( frame * mFrameTimeStep );
end = start.addMonths( nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalYears:
begin = start.addYears( frame * mFrameTimeStep );
end = start.addYears( nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalDecades:
begin = start.addYears( 10 * frame * mFrameTimeStep );
end = start.addYears( 10 * nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalCenturies:
begin = start.addYears( 100 * frame * mFrameTimeStep );
end = start.addYears( 100 * nextFrame * mFrameTimeStep );
break;
case QgsUnitTypes::TemporalUnit::TemporalUnknownUnit:
Q_ASSERT( false );
break;
}
}

QDateTime frameStart = begin;

@@ -183,29 +246,28 @@ long long QgsTemporalNavigationObject::currentFrameNumber() const
return mCurrentFrameNumber;
}

void QgsTemporalNavigationObject::setFrameDuration( QgsInterval frameDuration )
void QgsTemporalNavigationObject::setFrameTimeStep( double timeStep )
{
if ( mFrameDuration == frameDuration )
{
return;
}
QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
mFrameDuration = frameDuration;
mCurrentFrameNumber = findBestFrameNumberForFrameStart( oldFrame.begin() );
emit temporalFrameDurationChanged( mFrameDuration );

// temporarily disable the updateTemporalRange signal, as we'll emit it ourselves at the end of this function...
mFrameTimeStep = timeStep;
double unused;
mFrameTimeStepIsFractional = fabs( modf( mFrameTimeStep, &unused ) ) > 0.00001;
setCurrentFrameNumber( 0 );
}

// forcing an update of our views
QgsDateTimeRange range = dateTimeRangeForFrameNumber( mCurrentFrameNumber );
void QgsTemporalNavigationObject::setFrameTimeStepUnit( QgsUnitTypes::TemporalUnit timeStepUnit )
{
mFrameTimeStepUnit = timeStepUnit;
setCurrentFrameNumber( 0 );
}

if ( !mBlockUpdateTemporalRangeSignal && mNavigationMode != NavigationOff )
emit updateTemporalRange( range );
double QgsTemporalNavigationObject::frameTimeStep() const
{
return mFrameTimeStep;
}

QgsInterval QgsTemporalNavigationObject::frameDuration() const
QgsUnitTypes::TemporalUnit QgsTemporalNavigationObject::frameTimeStepUnit() const
{
return mFrameDuration;
return mFrameTimeStepUnit;
}

void QgsTemporalNavigationObject::setFramesPerSecond( double framesPerSeconds )
@@ -291,7 +353,7 @@ void QgsTemporalNavigationObject::skipToEnd()
long long QgsTemporalNavigationObject::totalFrameCount() const
{
QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
return std::floor( totalAnimationLength.seconds() / mFrameDuration.seconds() ) + 1;
return std::floor( totalAnimationLength.seconds() / QgsInterval( mFrameTimeStep, mFrameTimeStepUnit ).seconds() ) + 1;
}

void QgsTemporalNavigationObject::setAnimationState( AnimationState mode )
@@ -127,20 +127,40 @@ class CORE_EXPORT QgsTemporalNavigationObject : public QgsTemporalController, pu
long long currentFrameNumber() const;

/**
* Sets the frame \a duration, which dictates the temporal length of each frame in the animation.
* Sets the frame \a timestep, which together with the frame timestep unit dictates
* the temporal length of each frame in the animation.
*
* \note Calling this will reset the currentFrameNumber() to the closest temporal match for the previous temporal range.
*
* \see frameDuration()
* \see frameFrameTimeStep()
*/
void setFrameDuration( QgsInterval duration );
void setFrameTimeStep( double timeStep );

/**
* Returns the current set frame duration, which dictates the temporal length of each frame in the animation.
* Sets the frame \a timestep unit, which together with the frame time step dictates
* the temporal length of each frame in the animation.
*
* \see setFrameDuration()
* \note Calling this will reset the currentFrameNumber() to the first frame.
*
* \see frameTimeStepUnit()
*/
void setFrameTimeStepUnit( QgsUnitTypes::TemporalUnit timeStepUnit );

/**
* Returns the current set frame timestep, which together with frame timestep unit
* dictates the temporal length of each frame in the animation.
*
* \see setFrameTimeStep()
*/
double frameTimeStep() const;

/**
* Returns the current set frame timestep unit, which together with frame timestep
* dictates the temporal length of each frame in the animation.
*
* \see setFrameTimeStepUnit()
*/
QgsInterval frameDuration() const;
QgsUnitTypes::TemporalUnit frameTimeStepUnit() const;

/**
* Calculates the temporal range associated with a particular animation \a frame.
@@ -304,7 +324,9 @@ class CORE_EXPORT QgsTemporalNavigationObject : public QgsTemporalController, pu
long long mCurrentFrameNumber = 0;

//! Frame duration
QgsInterval mFrameDuration;
double mFrameTimeStep;
bool mFrameTimeStepIsFractional;
QgsUnitTypes::TemporalUnit mFrameTimeStepUnit;

//! Member for frame rate
double mFramesPerSecond = 1;
@@ -80,7 +80,8 @@ bool QgsTemporalUtils::exportAnimation( const QgsMapSettings &mapSettings, const

QgsTemporalNavigationObject navigator;
navigator.setTemporalExtents( settings.animationRange );
navigator.setFrameDuration( settings.frameDuration );
navigator.setFrameTimeStep( settings.frameDuration.seconds() );
navigator.setFrameTimeStepUnit( QgsUnitTypes::TemporalUnit::TemporalSeconds );
QgsMapSettings ms = mapSettings;
const QgsExpressionContext context = ms.expressionContext();

@@ -297,8 +297,8 @@ void QgsTemporalControllerWidget::updateFrameDuration()

if ( !mBlockFrameDurationUpdates )
{
mNavigationObject->setFrameDuration( QgsInterval( QgsProject::instance()->timeSettings()->timeStep(),
QgsProject::instance()->timeSettings()->timeStepUnit() ) );
mNavigationObject->setFrameTimeStep( QgsProject::instance()->timeSettings()->timeStep() );
mNavigationObject->setFrameTimeStepUnit( QgsProject::instance()->timeSettings()->timeStepUnit() );
mSlider->setValue( mNavigationObject->currentFrameNumber() );
}
mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 );
@@ -86,7 +86,8 @@ void TestQgsTemporalNavigationObject::animationState()
);
navigationObject->setTemporalExtents( range );

navigationObject->setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalMonths ) );
navigationObject->setFrameTimeStep( 1 );
navigationObject->setFrameTimeStepUnit( QgsUnitTypes::TemporalMonths );

qRegisterMetaType<QgsTemporalNavigationObject::AnimationState>( "AnimationState" );
QSignalSpy stateSignal( navigationObject, &QgsTemporalNavigationObject::stateChanged );
@@ -195,9 +196,10 @@ void TestQgsTemporalNavigationObject::frameSettings()
navigationObject->setTemporalExtents( range );
QCOMPARE( temporalRangeSignal.count(), 1 );

navigationObject->setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalHours ) );
QCOMPARE( navigationObject->frameDuration(), QgsInterval( 1, QgsUnitTypes::TemporalHours ) );
QCOMPARE( temporalRangeSignal.count(), 2 );
navigationObject->setFrameTimeStep( 1 );
navigationObject->setFrameTimeStepUnit( QgsUnitTypes::TemporalHours );
QCOMPARE( navigationObject->frameTimeStep(), 1 );
QCOMPARE( navigationObject->frameTimeStepUnit(), QgsUnitTypes::TemporalHours );

QCOMPARE( navigationObject->currentFrameNumber(), 0 );
QCOMPARE( navigationObject->totalFrameCount(), 5 );
@@ -242,13 +244,15 @@ void TestQgsTemporalNavigationObject::expressionContext()
QDateTime( QDate( 2020, 1, 1 ), QTime( 12, 0, 0 ) )
);
object.setTemporalExtents( range );
object.setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalHours ) );
object.setFrameTimeStep( 1 );
object.setFrameTimeStepUnit( QgsUnitTypes::TemporalHours );
object.setCurrentFrameNumber( 1 );
object.setFramesPerSecond( 30 );

std::unique_ptr< QgsExpressionContextScope > scope( object.createExpressionContextScope() );
QCOMPARE( scope->variable( QStringLiteral( "frame_rate" ) ).toDouble(), 30.0 );
QCOMPARE( scope->variable( QStringLiteral( "frame_duration" ) ).value< QgsInterval >().seconds(), 3600.0 );
QCOMPARE( scope->variable( QStringLiteral( "frame_timestep" ) ).value< double >(), 1.0 );
QCOMPARE( scope->variable( QStringLiteral( "frame_timestepunit" ) ).value< QgsUnitTypes::TemporalUnit >(), QgsUnitTypes::TemporalUnit::TemporalHours );
QCOMPARE( scope->variable( QStringLiteral( "frame_number" ) ).toInt(), 1 );
QCOMPARE( scope->variable( QStringLiteral( "animation_start_time" ) ).toDateTime(), range.begin() );
QCOMPARE( scope->variable( QStringLiteral( "animation_end_time" ) ).toDateTime(), range.end() );

0 comments on commit 9da580c

Please sign in to comment.
You can’t perform that action at this time.