QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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 
23  : QgsTemporalController( parent )
24 {
25  mNewFrameTimer = new QTimer( this );
26 
27  connect( mNewFrameTimer, &QTimer::timeout,
28  this, &QgsTemporalNavigationObject::timerTimeout );
29 }
30 
31 void QgsTemporalNavigationObject::timerTimeout()
32 {
33  switch ( mPlayBackMode )
34  {
35  case AnimationState::Forward:
36  next();
37  if ( mCurrentFrameNumber >= totalFrameCount() - 1 )
38  {
39  if ( mLoopAnimation )
40  mCurrentFrameNumber = -1; // we don't jump immediately to frame 0, instead we delay that till the next timeout
41  else
42  pause();
43  }
44  break;
45 
46  case AnimationState::Reverse:
47  previous();
48  if ( mCurrentFrameNumber <= 0 )
49  {
50  if ( mLoopAnimation )
51  mCurrentFrameNumber = totalFrameCount(); // we don't jump immediately to real last frame..., instead we delay that till the next timeout
52  else
53  pause();
54  }
55  break;
56 
57  case AnimationState::Idle:
58  // should not happen - in an idle state the timeout won't occur
59  break;
60  }
61 }
62 
64 {
65  return mLoopAnimation;
66 }
67 
68 void QgsTemporalNavigationObject::setLooping( bool loopAnimation )
69 {
70  mLoopAnimation = loopAnimation;
71 }
72 
74 {
75  std::unique_ptr< QgsExpressionContextScope > scope = std::make_unique< QgsExpressionContextScope >( QStringLiteral( "temporal" ) );
76  scope->setVariable( QStringLiteral( "frame_rate" ), mFramesPerSecond, true );
77  scope->setVariable( QStringLiteral( "frame_number" ), mCurrentFrameNumber, true );
78  scope->setVariable( QStringLiteral( "frame_duration" ), mFrameDuration, true );
79  scope->setVariable( QStringLiteral( "frame_timestep" ), mFrameDuration.originalDuration(), true );
80  scope->setVariable( QStringLiteral( "frame_timestep_unit" ), mFrameDuration.originalUnit(), true );
81  scope->setVariable( QStringLiteral( "animation_start_time" ), mTemporalExtents.begin(), true );
82  scope->setVariable( QStringLiteral( "animation_end_time" ), mTemporalExtents.end(), true );
83  scope->setVariable( QStringLiteral( "animation_interval" ), mTemporalExtents.end() - mTemporalExtents.begin(), true );
84  return scope.release();
85 }
86 
87 QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long long frame ) const
88 {
89  const QDateTime start = mTemporalExtents.begin();
90 
91  if ( frame < 0 )
92  frame = 0;
93 
94  const long long nextFrame = frame + 1;
95 
96  QDateTime begin;
97  QDateTime end;
98  if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
99  {
100  if ( mAllRanges.empty() )
101  return QgsDateTimeRange();
102 
103  return frame < mAllRanges.size() ? mAllRanges.at( frame ) : mAllRanges.constLast();
104  }
105  else
106  {
107  begin = QgsTemporalUtils::calculateFrameTime( start, frame, mFrameDuration );
108  end = QgsTemporalUtils::calculateFrameTime( start, nextFrame, mFrameDuration );
109  }
110 
111  QDateTime frameStart = begin;
112 
113  if ( mCumulativeTemporalRange )
114  frameStart = start;
115 
116  return QgsDateTimeRange( frameStart, end, true, false );
117 }
118 
120 {
121  if ( mNavigationMode == mode )
122  return;
123 
124  mNavigationMode = mode;
125  emit navigationModeChanged( mode );
126 
127  if ( !mBlockUpdateTemporalRangeSignal )
128  {
129  switch ( mNavigationMode )
130  {
131  case Animated:
132  emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) );
133  break;
134  case FixedRange:
135  emit updateTemporalRange( mTemporalExtents );
136  break;
137  case NavigationOff:
138  emit updateTemporalRange( QgsDateTimeRange() );
139  break;
140  }
141  }
142 }
143 
144 void QgsTemporalNavigationObject::setTemporalExtents( const QgsDateTimeRange &temporalExtents )
145 {
146  if ( mTemporalExtents == temporalExtents )
147  {
148  return;
149  }
150  const QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
151  mTemporalExtents = temporalExtents;
152  mCurrentFrameNumber = findBestFrameNumberForFrameStart( oldFrame.begin() );
153  emit temporalExtentsChanged( mTemporalExtents );
154 
155  switch ( mNavigationMode )
156  {
157  case Animated:
158  {
159  const long long currentFrameNumber = mCurrentFrameNumber;
160 
161  // Force to emit signal if the current frame number doesn't change
162  if ( currentFrameNumber == mCurrentFrameNumber && !mBlockUpdateTemporalRangeSignal )
163  emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) );
164  break;
165  }
166  case FixedRange:
167  if ( !mBlockUpdateTemporalRangeSignal )
168  emit updateTemporalRange( mTemporalExtents );
169  break;
170  case NavigationOff:
171  break;
172  }
173 
174 }
175 
177 {
178  return mTemporalExtents;
179 }
180 
181 void QgsTemporalNavigationObject::setAvailableTemporalRanges( const QList<QgsDateTimeRange> &ranges )
182 {
183  mAllRanges = ranges;
184 }
185 
187 {
188  return mAllRanges;
189 }
190 
192 {
193  if ( mCurrentFrameNumber != frameNumber )
194  {
195  mCurrentFrameNumber = std::max( 0LL, std::min( frameNumber, totalFrameCount() - 1 ) );
196  const QgsDateTimeRange range = dateTimeRangeForFrameNumber( mCurrentFrameNumber );
197 
198  if ( !mBlockUpdateTemporalRangeSignal )
199  emit updateTemporalRange( range );
200  }
201 }
202 
204 {
205  return mCurrentFrameNumber;
206 }
207 
209 {
210  if ( mFrameDuration == frameDuration )
211  {
212  return;
213  }
214 
215  const QgsDateTimeRange oldFrame = dateTimeRangeForFrameNumber( currentFrameNumber() );
216  mFrameDuration = frameDuration;
217 
218  mCurrentFrameNumber = findBestFrameNumberForFrameStart( oldFrame.begin() );
219  emit temporalFrameDurationChanged( mFrameDuration );
220 
221  // forcing an update of our views
222  const QgsDateTimeRange range = dateTimeRangeForFrameNumber( mCurrentFrameNumber );
223 
224  if ( !mBlockUpdateTemporalRangeSignal && mNavigationMode == Animated )
225  emit updateTemporalRange( range );
226 }
227 
229 {
230  return mFrameDuration;
231 }
232 
233 void QgsTemporalNavigationObject::setFramesPerSecond( double framesPerSeconds )
234 {
235  if ( framesPerSeconds > 0 )
236  {
237  mFramesPerSecond = framesPerSeconds;
238  mNewFrameTimer->setInterval( static_cast< int >( ( 1.0 / mFramesPerSecond ) * 1000 ) );
239  }
240 }
241 
243 {
244  return mFramesPerSecond;
245 }
246 
248 {
249  mCumulativeTemporalRange = state;
250 }
251 
253 {
254  return mCumulativeTemporalRange;
255 }
256 
258 {
259  mNewFrameTimer->start( static_cast< int >( ( 1.0 / mFramesPerSecond ) * 1000 ) );
260 }
261 
263 {
264  mNewFrameTimer->stop();
265  setAnimationState( AnimationState::Idle );
266 }
267 
269 {
270  if ( mPlayBackMode == Idle && mCurrentFrameNumber >= totalFrameCount() - 1 )
271  {
272  // if we are paused at the end of the video, and the user hits play, we automatically rewind and play again
273  rewindToStart();
274  }
275 
276  setAnimationState( AnimationState::Forward );
277  play();
278 }
279 
281 {
282  if ( mPlayBackMode == Idle && mCurrentFrameNumber <= 0 )
283  {
284  // 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
285  skipToEnd();
286  }
287 
288  setAnimationState( AnimationState::Reverse );
289  play();
290 }
291 
293 {
294  setCurrentFrameNumber( mCurrentFrameNumber + 1 );
295 }
296 
298 {
299  setCurrentFrameNumber( mCurrentFrameNumber - 1 );
300 }
301 
303 {
305 }
306 
308 {
309  const long long frame = totalFrameCount() - 1;
310  setCurrentFrameNumber( frame );
311 }
312 
314 {
315  if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
316  {
317  return mAllRanges.count();
318  }
319  else
320  {
321  const QgsInterval totalAnimationLength = mTemporalExtents.end() - mTemporalExtents.begin();
322  return static_cast< long long >( std::ceil( totalAnimationLength.seconds() / mFrameDuration.seconds() ) );
323  }
324 }
325 
327 {
328  if ( mode != mPlayBackMode )
329  {
330  mPlayBackMode = mode;
331  emit stateChanged( mPlayBackMode );
332  }
333 }
334 
336 {
337  return mPlayBackMode;
338 }
339 
340 long long QgsTemporalNavigationObject::findBestFrameNumberForFrameStart( const QDateTime &frameStart ) const
341 {
342  long long bestFrame = 0;
343  if ( mFrameDuration.originalUnit() == QgsUnitTypes::TemporalIrregularStep )
344  {
345  for ( const QgsDateTimeRange &range : mAllRanges )
346  {
347  if ( range.contains( frameStart ) )
348  return bestFrame;
349  else if ( range.begin() > frameStart )
350  // if we've gone past the target date, go back one frame if possible
351  return std::max( 0LL, bestFrame - 1 );
352  bestFrame++;
353  }
354  return mAllRanges.count() - 1;
355  }
356  else
357  {
358  const QgsDateTimeRange testFrame = QgsDateTimeRange( frameStart, frameStart ); // creating an 'instant' Range
359  // Earlier we looped from frame 0 till totalFrameCount() here, but this loop grew potentially gigantic
360  long long roughFrameStart = 0;
361  long long roughFrameEnd = totalFrameCount();
362  // For the smaller step frames we calculate an educated guess, to prevent the loop becoming too
363  // large, freezing the ui (eg having a mTemporalExtents of several months and the user selects milliseconds)
364  if ( mFrameDuration.originalUnit() != QgsUnitTypes::TemporalMonths && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalYears && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalDecades && mFrameDuration.originalUnit() != QgsUnitTypes::TemporalCenturies )
365  {
366  // Only if we receive a valid frameStart, that is within current mTemporalExtents
367  // We tend to receive a framestart of 'now()' upon startup for example
368  if ( mTemporalExtents.contains( frameStart ) )
369  {
370  roughFrameStart = static_cast< long long >( std::floor( ( frameStart - mTemporalExtents.begin() ).seconds() / mFrameDuration.seconds() ) );
371  }
372  roughFrameEnd = roughFrameStart + 100; // just in case we miss the guess
373  }
374  for ( long long i = roughFrameStart; i < roughFrameEnd; ++i )
375  {
376  const QgsDateTimeRange range = dateTimeRangeForFrameNumber( i );
377  if ( range.overlaps( testFrame ) )
378  {
379  bestFrame = i;
380  break;
381  }
382  }
383  return bestFrame;
384  }
385 }
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:280
double seconds() const
Returns the interval duration in seconds.
Definition: qgsinterval.h:236
QgsUnitTypes::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
Definition: qgsinterval.h:295
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...
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 navigationModeChanged(NavigationMode mode)
Emitted whenever the navigation mode 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 ...
void stateChanged(AnimationState state)
Emitted whenever the animation state changes.
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 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.
@ TemporalMonths
Months.
Definition: qgsunittypes.h:157
@ TemporalIrregularStep
Special "irregular step" time unit, used for temporal data which uses irregular, non-real-world unit ...
Definition: qgsunittypes.h:161
@ TemporalDecades
Decades.
Definition: qgsunittypes.h:159
@ TemporalCenturies
Centuries.
Definition: qgsunittypes.h:160
@ TemporalYears
Years.
Definition: qgsunittypes.h:158