QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgstemporalnavigationobject.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstemporalnavigationobject.cpp
3 ---------------
4 begin : March 2020
5 copyright : (C) 2020 by Samweli Mwakisambwe
6 email : samweli at kartoza dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgis.h"
20#include "qgstemporalutils.h"
21#include "qgsunittypes.h"
22
24 : QgsTemporalController( parent )
25{
26 mNewFrameTimer = new QTimer( this );
27
28 connect( mNewFrameTimer, &QTimer::timeout,
29 this, &QgsTemporalNavigationObject::timerTimeout );
30}
31
32void QgsTemporalNavigationObject::timerTimeout()
33{
34 switch ( mPlayBackMode )
35 {
36 case AnimationState::Forward:
37 next();
38 if ( mCurrentFrameNumber >= totalFrameCount() - 1 )
39 {
40 if ( mLoopAnimation )
41 mCurrentFrameNumber = -1; // we don't jump immediately to frame 0, instead we delay that till the next timeout
42 else
43 pause();
44 }
45 break;
46
47 case AnimationState::Reverse:
48 previous();
49 if ( mCurrentFrameNumber <= 0 )
50 {
51 if ( mLoopAnimation )
52 mCurrentFrameNumber = totalFrameCount(); // we don't jump immediately to real last frame..., instead we delay that till the next timeout
53 else
54 pause();
55 }
56 break;
57
58 case AnimationState::Idle:
59 // should not happen - in an idle state the timeout won't occur
60 break;
61 }
62}
63
65{
66 return mLoopAnimation;
67}
68
70{
71 mLoopAnimation = loopAnimation;
72}
73
75{
76 std::unique_ptr< QgsExpressionContextScope > scope = std::make_unique< QgsExpressionContextScope >( QStringLiteral( "temporal" ) );
77 scope->setVariable( QStringLiteral( "frame_rate" ), mFramesPerSecond, true );
78 scope->setVariable( QStringLiteral( "frame_number" ), mCurrentFrameNumber, true );
79 scope->setVariable( QStringLiteral( "frame_duration" ), mFrameDuration, true );
80 scope->setVariable( QStringLiteral( "frame_timestep" ), mFrameDuration.originalDuration(), true );
81 scope->setVariable( QStringLiteral( "frame_timestep_unit" ), static_cast< int >( mFrameDuration.originalUnit() ), true );
82 scope->setVariable( QStringLiteral( "frame_timestep_units" ), QgsUnitTypes::toString( mFrameDuration.originalUnit() ), true );
83 scope->setVariable( QStringLiteral( "animation_start_time" ), mTemporalExtents.begin(), true );
84 scope->setVariable( QStringLiteral( "animation_end_time" ), mTemporalExtents.end(), true );
85 scope->setVariable( QStringLiteral( "animation_interval" ), QgsInterval( mTemporalExtents.end() - mTemporalExtents.begin() ), true );
86
87 scope->addHiddenVariable( QStringLiteral( "frame_timestep_unit" ) );
88
89 return scope.release();
90}
91
92QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long long frame ) const
93{
94 const QDateTime start = mTemporalExtents.begin();
95
96 if ( frame < 0 )
97 frame = 0;
98
99 const long long nextFrame = frame + 1;
100
101 QDateTime begin;
102 QDateTime end;
103 if ( mFrameDuration.originalUnit() == Qgis::TemporalUnit::IrregularStep )
104 {
105 if ( mAllRanges.empty() )
106 return QgsDateTimeRange();
107
108 return frame < mAllRanges.size() ? mAllRanges.at( frame ) : mAllRanges.constLast();
109 }
110 else
111 {
112 begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
113 end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
114 }
115
116 QDateTime frameStart = begin;
117
118 if ( mCumulativeTemporalRange )
119 frameStart = start;
120
121 return QgsDateTimeRange( frameStart, end, true, false );
122}
123
125{
126 if ( mNavigationMode == mode )
127 return;
128
129 mNavigationMode = mode;
130 emit navigationModeChanged( mode );
131
132 if ( !mBlockUpdateTemporalRangeSignal )
133 {
134 switch ( mNavigationMode )
135 {
136 case Animated:
137 emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) );
138 break;
139 case FixedRange:
140 emit updateTemporalRange( mTemporalExtents );
141 break;
142 case NavigationOff:
143 emit updateTemporalRange( QgsDateTimeRange() );
144 break;
145 }
146 }
147}
148
149void QgsTemporalNavigationObject::setTemporalExtents( const QgsDateTimeRange &temporalExtents )
150{
151 if ( mTemporalExtents == temporalExtents )
152 {
153 return;
154 }
155 const QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
156 mTemporalExtents = temporalExtents;
157 mCurrentFrameNumber = findBestFrameNumberForFrameStart( oldFrame.begin() );
158 emit temporalExtentsChanged( mTemporalExtents );
159
160 switch ( mNavigationMode )
161 {
162 case Animated:
163 {
164 const long long currentFrameNumber = mCurrentFrameNumber;
165
166 // Force to emit signal if the current frame number doesn't change
167 if ( currentFrameNumber == mCurrentFrameNumber && !mBlockUpdateTemporalRangeSignal )
168 emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) );
169 break;
170 }
171 case FixedRange:
172 if ( !mBlockUpdateTemporalRangeSignal )
173 emit updateTemporalRange( mTemporalExtents );
174 break;
175 case NavigationOff:
176 break;
177 }
178
179}
180
182{
183 return mTemporalExtents;
184}
185
186void QgsTemporalNavigationObject::setAvailableTemporalRanges( const QList<QgsDateTimeRange> &ranges )
187{
188 mAllRanges = ranges;
189}
190
192{
193 return mAllRanges;
194}
195
197{
198 if ( mCurrentFrameNumber != frameNumber )
199 {
200 mCurrentFrameNumber = std::max( 0LL, std::min( frameNumber, totalFrameCount() - 1 ) );
201 const QgsDateTimeRange range = dateTimeRangeForFrameNumber( mCurrentFrameNumber );
202
203 if ( !mBlockUpdateTemporalRangeSignal )
204 emit updateTemporalRange( range );
205 }
206}
207
209{
210 return mCurrentFrameNumber;
211}
212
214{
215 if ( mFrameDuration == frameDuration )
216 {
217 return;
218 }
219
220 const QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
221 mFrameDuration = frameDuration;
222
223 mCurrentFrameNumber = findBestFrameNumberForFrameStart( oldFrame.begin() );
224 emit temporalFrameDurationChanged( mFrameDuration );
225
226 // forcing an update of our views
227 const QgsDateTimeRange range = dateTimeRangeForFrameNumber( mCurrentFrameNumber );
228
229 if ( !mBlockUpdateTemporalRangeSignal && mNavigationMode == Animated )
230 emit updateTemporalRange( range );
231}
232
234{
235 return mFrameDuration;
236}
237
239{
240 if ( framesPerSeconds > 0 )
241 {
242 mFramesPerSecond = framesPerSeconds;
243 mNewFrameTimer->setInterval( static_cast< int >( ( 1.0 / mFramesPerSecond ) * 1000 ) );
244 }
245}
246
248{
249 return mFramesPerSecond;
250}
251
253{
254 mCumulativeTemporalRange = state;
255}
256
258{
259 return mCumulativeTemporalRange;
260}
261
263{
264 mNewFrameTimer->start( static_cast< int >( ( 1.0 / mFramesPerSecond ) * 1000 ) );
265}
266
268{
269 mNewFrameTimer->stop();
270 setAnimationState( AnimationState::Idle );
271}
272
274{
275 if ( mPlayBackMode == Idle && mCurrentFrameNumber >= totalFrameCount() - 1 )
276 {
277 // if we are paused at the end of the video, and the user hits play, we automatically rewind and play again
279 }
280
281 setAnimationState( AnimationState::Forward );
282 play();
283}
284
286{
287 if ( mPlayBackMode == Idle && mCurrentFrameNumber <= 0 )
288 {
289 // if we are paused at the start of the video, and the user hits play, we automatically skip to end and play in reverse again
290 skipToEnd();
291 }
292
293 setAnimationState( AnimationState::Reverse );
294 play();
295}
296
298{
299 setCurrentFrameNumber( mCurrentFrameNumber + 1 );
300}
301
303{
304 setCurrentFrameNumber( mCurrentFrameNumber - 1 );
305}
306
308{
310}
311
313{
314 const long long frame = totalFrameCount() - 1;
315 setCurrentFrameNumber( frame );
316}
317
319{
320 if ( mFrameDuration.originalUnit() == Qgis::TemporalUnit::IrregularStep )
321 {
322 return mAllRanges.count();
323 }
324 else
325 {
326 const QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
327 return static_cast< long long >( std::ceil( totalAnimationLength.seconds() / mFrameDuration.seconds() ) );
328 }
329}
330
332{
333 if ( mode != mPlayBackMode )
334 {
335 mPlayBackMode = mode;
336 emit stateChanged( mPlayBackMode );
337 }
338}
339
341{
342 return mPlayBackMode;
343}
344
345long long QgsTemporalNavigationObject::findBestFrameNumberForFrameStart( const QDateTime &frameStart ) const
346{
347 long long bestFrame = 0;
348 if ( mFrameDuration.originalUnit() == Qgis::TemporalUnit::IrregularStep )
349 {
350 for ( const QgsDateTimeRange &range : mAllRanges )
351 {
352 if ( range.contains( frameStart ) )
353 return bestFrame;
354 else if ( range.begin() > frameStart )
355 // if we've gone past the target date, go back one frame if possible
356 return std::max( 0LL, bestFrame - 1 );
357 bestFrame++;
358 }
359 return mAllRanges.count() - 1;
360 }
361 else
362 {
363 const QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
364 // Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
365 long long roughFrameStart = 0;
366 long long roughFrameEnd = totalFrameCount();
367 // For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
368 // large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
369 if ( mFrameDuration.originalUnit() != Qgis::TemporalUnit::Months && mFrameDuration.originalUnit() != Qgis::TemporalUnit::Years && mFrameDuration.originalUnit() != Qgis::TemporalUnit::Decades && mFrameDuration.originalUnit() != Qgis::TemporalUnit::Centuries )
370 {
371 // Only if we receive a valid frameStart, that is within current mTemporalExtents
372 // We tend to receive a framestart of 'now()' upon startup for example
373 if ( mTemporalExtents.contains( frameStart ) )
374 {
375 roughFrameStart = static_cast< long long >( std::floor( QgsInterval( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() ) );
376 }
377 roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
378 }
379 for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
380 {
381 const QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
382 if ( range.overlaps( testFrame ) )
383 {
384 bestFrame = i;
385 break;
386 }
387 }
388 return bestFrame;
389 }
390}
Single scope for storing variables and functions for use within a QgsExpressionContext.
A representation of the interval between two datetime values.
Definition: qgsinterval.h:42
double originalDuration() const
Returns the original interval duration.
Definition: qgsinterval.h:286
double seconds() const
Returns the interval duration in seconds.
Definition: qgsinterval.h:242
Qgis::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
Definition: qgsinterval.h:301
A controller base class for temporal objects, contains a signal for notifying updates of the objects ...
void updateTemporalRange(const QgsDateTimeRange &range)
Signals that a temporal range has changed and needs to be updated in all connected objects.
bool isLooping() const
Returns true if the animation should loop after hitting the end or start frame.
void previous()
Jumps back to the previous frame.
double framesPerSecond() const
Returns the animation frame rate, in frames per second.
void setAvailableTemporalRanges(const QList< QgsDateTimeRange > &ranges)
Sets the list of all available temporal ranges which have data available.
void setFrameDuration(const QgsInterval &duration)
Sets the frame duration, which dictates the temporal length of each frame in the animation.
long long findBestFrameNumberForFrameStart(const QDateTime &frameStart) const
Returns the best suited frame number for the specified datetime, based on the start of the correspond...
void stateChanged(QgsTemporalNavigationObject::AnimationState state)
Emitted whenever the animation state changes.
QgsExpressionContextScope * createExpressionContextScope() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
void playForward()
Starts the animation playing in a forward direction up till the end of all frames.
NavigationMode
Represents the current temporal navigation mode.
@ NavigationOff
Temporal navigation is disabled.
@ FixedRange
Temporal navigation relies on a fixed datetime range.
@ Animated
Temporal navigation relies on frames within a datetime range.
long long currentFrameNumber() const
Returns the current frame number.
void rewindToStart()
Rewinds the temporal navigation to start of the temporal extent.
void pause()
Pauses the temporal navigation.
void setCurrentFrameNumber(long long frame)
Sets the current animation frame number.
long long totalFrameCount() const
Returns the total number of frames for the navigation.
void skipToEnd()
Skips the temporal navigation to end of the temporal extent.
void temporalFrameDurationChanged(const QgsInterval &interval)
Emitted whenever the frameDuration interval of the controller changes.
void setFramesPerSecond(double rate)
Sets the animation frame rate, in frames per second.
QgsDateTimeRange temporalExtents() const
Returns the navigation temporal extents, which dictate the earliest and latest date time possible in ...
AnimationState
Represents the current animation state.
bool temporalRangeCumulative() const
Returns the animation temporal range cumulative settings.
void next()
Advances to the next frame.
QgsDateTimeRange dateTimeRangeForFrameNumber(long long frame) const
Calculates the temporal range associated with a particular animation frame.
void setTemporalExtents(const QgsDateTimeRange &extents)
Sets the navigation temporal extents, which dictate the earliest and latest date time possible in the...
void setTemporalRangeCumulative(bool state)
Sets the animation temporal range as cumulative.
QList< QgsDateTimeRange > availableTemporalRanges() const
Returns the list of all available temporal ranges which have data available.
void navigationModeChanged(QgsTemporalNavigationObject::NavigationMode mode)
Emitted whenever the navigation mode changes.
void play()
Starts playing the temporal navigation from its current frame, using the direction specified by anima...
void setLooping(bool loop)
Sets whether the animation should loop after hitting the end or start frame.
void playBackward()
Starts the animation playing in a reverse direction until the beginning of the time range.
void temporalExtentsChanged(const QgsDateTimeRange &extent)
Emitted whenever the temporalExtent extent changes.
void setAnimationState(AnimationState state)
Sets the current animation state.
void setNavigationMode(const NavigationMode mode)
Sets the temporal navigation mode.
QgsTemporalNavigationObject(QObject *parent=nullptr)
Constructor for QgsTemporalNavigationObject, with the specified parent object.
AnimationState animationState() const
Returns the current animation state.
QgsInterval frameDuration() const
Returns the current set frame duration, which dictates the temporal length of each frame in the anima...
static QDateTime calculateFrameTime(const QDateTime &start, const long long frame, const QgsInterval &interval)
Calculates the frame time for an animation.
static Q_INVOKABLE QString toString(Qgis::DistanceUnit unit)
Returns a translated string representing a distance unit.