QGIS API Documentation  3.25.0-Master (10b47c2603)
qgstemporalutils.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgstemporalutils.cpp
3  -----------------------
4  Date : March 2020
5  Copyright : (C) 2020 by Nyall Dawson
6  Email : nyall dot dawson at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgstemporalutils.h"
17 #include "qgsproject.h"
19 #include "qgsrasterlayer.h"
20 #include "qgsmeshlayer.h"
21 #include "qgsvectorlayer.h"
26 #include "qgsmapdecoration.h"
27 #include "qgsmapsettings.h"
30 
31 #include <QRegularExpression>
32 
34 {
35  const QMap<QString, QgsMapLayer *> mapLayers = project->mapLayers();
36  QDateTime minDate;
37  QDateTime maxDate;
38 
39  for ( auto it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it )
40  {
41  QgsMapLayer *currentLayer = it.value();
42 
43  if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
44  continue;
45  const QgsDateTimeRange layerRange = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
46 
47  if ( layerRange.begin().isValid() && ( !minDate.isValid() || layerRange.begin() < minDate ) )
48  minDate = layerRange.begin();
49  if ( layerRange.end().isValid() && ( !maxDate.isValid() || layerRange.end() > maxDate ) )
50  maxDate = layerRange.end();
51  }
52 
53  return QgsDateTimeRange( minDate, maxDate );
54 }
55 
56 QList< QgsDateTimeRange > QgsTemporalUtils::usedTemporalRangesForProject( QgsProject *project )
57 {
58  const QMap<QString, QgsMapLayer *> mapLayers = project->mapLayers();
59 
60  QList< QgsDateTimeRange > ranges;
61  for ( auto it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it )
62  {
63  QgsMapLayer *currentLayer = it.value();
64 
65  if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
66  continue;
67 
68  ranges.append( currentLayer->temporalProperties()->allTemporalRanges( currentLayer ) );
69  }
70 
71  return QgsDateTimeRange::mergeRanges( ranges );
72 }
73 
74 bool QgsTemporalUtils::exportAnimation( const QgsMapSettings &mapSettings, const QgsTemporalUtils::AnimationExportSettings &settings, QString &error, QgsFeedback *feedback )
75 {
76  if ( settings.fileNameTemplate.isEmpty() )
77  {
78  error = QObject::tr( "Filename template is empty" );
79  return false;
80  }
81  const int numberOfDigits = settings.fileNameTemplate.count( QLatin1Char( '#' ) );
82  if ( numberOfDigits < 0 )
83  {
84  error = QObject::tr( "Wrong filename template format (must contain #)" );
85  return false;
86  }
87  const QString token( numberOfDigits, QLatin1Char( '#' ) );
88  if ( !settings.fileNameTemplate.contains( token ) )
89  {
90  error = QObject::tr( "Filename template must contain all # placeholders in one continuous group." );
91  return false;
92  }
93  if ( !QDir().mkpath( settings.outputDirectory ) )
94  {
95  error = QObject::tr( "Output directory creation failure." );
96  return false;
97  }
98 
100  navigator.setTemporalExtents( settings.animationRange );
101  navigator.setFrameDuration( settings.frameDuration );
102  QgsMapSettings ms = mapSettings;
103  const QgsExpressionContext context = ms.expressionContext();
104  ms.setFrameRate( settings.frameRate );
105 
106  const long long totalFrames = navigator.totalFrameCount();
107  long long currentFrame = 0;
108 
109  while ( currentFrame < totalFrames )
110  {
111  if ( feedback )
112  {
113  if ( feedback->isCanceled() )
114  {
115  error = QObject::tr( "Export canceled" );
116  return false;
117  }
118  feedback->setProgress( currentFrame / static_cast<double>( totalFrames ) * 100 );
119  }
120 
121  navigator.setCurrentFrameNumber( currentFrame );
122 
123  ms.setIsTemporal( true );
124  ms.setTemporalRange( navigator.dateTimeRangeForFrameNumber( currentFrame ) );
125  ms.setCurrentFrame( currentFrame );
126 
127  QgsExpressionContext frameContext = context;
128  frameContext.appendScope( navigator.createExpressionContextScope() );
130  ms.setExpressionContext( frameContext );
131 
132  QString fileName( settings.fileNameTemplate );
133  const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( currentFrame, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
134  fileName.replace( token, frameNoPaddedLeft );
135  const QString path = QDir( settings.outputDirectory ).filePath( fileName );
136 
137  QImage img = QImage( ms.outputSize(), ms.outputImageFormat() );
138  img.setDotsPerMeterX( 1000 * ms.outputDpi() / 25.4 );
139  img.setDotsPerMeterY( 1000 * ms.outputDpi() / 25.4 );
140  img.fill( ms.backgroundColor().rgb() );
141 
142  QPainter p( &img );
143  QgsMapRendererCustomPainterJob job( ms, &p );
144  job.start();
145  job.waitForFinished();
146 
148  context.setPainter( &p );
149 
150  const auto constMDecorations = settings.decorations;
151  for ( QgsMapDecoration *decoration : constMDecorations )
152  {
153  decoration->render( ms, context );
154  }
155 
156  p.end();
157 
158  img.save( path );
159 
160  ++currentFrame;
161  }
162 
163  return true;
164 }
165 
166 
167 QDateTime QgsTemporalUtils::calculateFrameTime( const QDateTime &start, const long long frame, const QgsInterval &interval )
168 {
169 
170  double unused;
171  const bool isFractional = !qgsDoubleNear( fabs( modf( interval.originalDuration(), &unused ) ), 0.0 );
172 
173  if ( isFractional || interval.originalUnit() == QgsUnitTypes::TemporalUnit::TemporalUnknownUnit )
174  {
175  const double duration = interval.seconds();
176  return start.addMSecs( frame * duration * 1000 );
177  }
178  else
179  {
180  switch ( interval.originalUnit() )
181  {
182  case QgsUnitTypes::TemporalUnit::TemporalMilliseconds:
183  return start.addMSecs( frame * interval.originalDuration() );
184  case QgsUnitTypes::TemporalUnit::TemporalSeconds:
185  return start.addSecs( frame * interval.originalDuration() );
186  case QgsUnitTypes::TemporalUnit::TemporalMinutes:
187  return start.addSecs( 60 * frame * interval.originalDuration() );
188  case QgsUnitTypes::TemporalUnit::TemporalHours:
189  return start.addSecs( 3600 * frame * interval.originalDuration() );
190  case QgsUnitTypes::TemporalUnit::TemporalDays:
191  return start.addDays( frame * interval.originalDuration() );
192  case QgsUnitTypes::TemporalUnit::TemporalWeeks:
193  return start.addDays( 7 * frame * interval.originalDuration() );
194  case QgsUnitTypes::TemporalUnit::TemporalMonths:
195  return start.addMonths( frame * interval.originalDuration() );
196  case QgsUnitTypes::TemporalUnit::TemporalYears:
197  return start.addYears( frame * interval.originalDuration() );
198  case QgsUnitTypes::TemporalUnit::TemporalDecades:
199  return start.addYears( 10 * frame * interval.originalDuration() );
200  case QgsUnitTypes::TemporalUnit::TemporalCenturies:
201  return start.addYears( 100 * frame * interval.originalDuration() );
202  case QgsUnitTypes::TemporalUnit::TemporalUnknownUnit:
203  // handled above
204  return QDateTime();
205  case QgsUnitTypes::TemporalUnit::TemporalIrregularStep:
206  // not supported by this method
207  return QDateTime();
208  }
209  }
210  return QDateTime();
211 }
212 
213 QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok, bool &maxValuesExceeded, int maxValues )
214 {
215  ok = false;
216  const QgsTimeDuration timeDuration( QgsTimeDuration::fromString( duration, ok ) );
217  if ( !ok )
218  return {};
219 
220  if ( timeDuration.years == 0 && timeDuration.months == 0 && timeDuration.weeks == 0 && timeDuration.days == 0
221  && timeDuration.hours == 0 && timeDuration.minutes == 0 && timeDuration.seconds == 0 )
222  {
223  ok = false;
224  return {};
225  }
226  return calculateDateTimesUsingDuration( start, end, timeDuration, maxValuesExceeded, maxValues );
227 }
228 
229 QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QgsTimeDuration &timeDuration, bool &maxValuesExceeded, int maxValues )
230 {
231  QList<QDateTime> res;
232  QDateTime current = start;
233  maxValuesExceeded = false;
234  while ( current <= end )
235  {
236  res << current;
237 
238  if ( maxValues >= 0 && res.size() > maxValues )
239  {
240  maxValuesExceeded = true;
241  break;
242  }
243 
244  if ( timeDuration.years )
245  current = current.addYears( timeDuration.years );
246  if ( timeDuration.months )
247  current = current.addMonths( timeDuration.months );
248  if ( timeDuration.weeks || timeDuration.days )
249  current = current.addDays( timeDuration.weeks * 7 + timeDuration.days );
250  if ( timeDuration.hours || timeDuration.minutes || timeDuration.seconds )
251  current = current.addSecs( timeDuration.hours * 60LL * 60 + timeDuration.minutes * 60 + timeDuration.seconds );
252  }
253  return res;
254 }
255 
256 QList<QDateTime> QgsTemporalUtils::calculateDateTimesFromISO8601( const QString &string, bool &ok, bool &maxValuesExceeded, int maxValues )
257 {
258  ok = false;
259  maxValuesExceeded = false;
260  const QStringList parts = string.split( '/' );
261  if ( parts.length() != 3 )
262  {
263  return {};
264  }
265 
266  const QDateTime start = QDateTime::fromString( parts.at( 0 ), Qt::ISODate );
267  if ( !start.isValid() )
268  return {};
269  const QDateTime end = QDateTime::fromString( parts.at( 1 ), Qt::ISODate );
270  if ( !end.isValid() )
271  return {};
272 
273  return calculateDateTimesUsingDuration( start, end, parts.at( 2 ), ok, maxValuesExceeded, maxValues );
274 }
275 
276 //
277 // QgsTimeDuration
278 //
279 
281 {
283 }
284 
286 {
287  QString text( "P" );
288 
289  if ( years )
290  {
291  text.append( QString::number( years ) );
292  text.append( 'Y' );
293  }
294  if ( months )
295  {
296  text.append( QString::number( months ) );
297  text.append( 'M' );
298  }
299  if ( days )
300  {
301  text.append( QString::number( days ) );
302  text.append( 'D' );
303  }
304 
305  if ( hours )
306  {
307  if ( !text.contains( 'T' ) )
308  text.append( 'T' );
309  text.append( QString::number( hours ) );
310  text.append( 'H' );
311  }
312  if ( minutes )
313  {
314  if ( !text.contains( 'T' ) )
315  text.append( 'T' );
316  text.append( QString::number( minutes ) );
317  text.append( 'M' );
318  }
319  if ( seconds )
320  {
321  if ( !text.contains( 'T' ) )
322  text.append( 'T' );
323  text.append( QString::number( seconds ) );
324  text.append( 'S' );
325  }
326  return text;
327 }
328 
329 long long QgsTimeDuration::toSeconds() const
330 {
331  long long secs = 0.0;
332 
333  if ( years )
334  secs += years * QgsInterval::YEARS;
335  if ( months )
336  secs += months * QgsInterval::MONTHS;
337  if ( days )
338  secs += days * QgsInterval::DAY;
339  if ( hours )
340  secs += hours * QgsInterval::HOUR;
341  if ( minutes )
342  secs += minutes * QgsInterval::MINUTE;
343  if ( seconds )
344  secs += seconds;
345 
346  return secs;
347 }
348 
349 QDateTime QgsTimeDuration::addToDateTime( const QDateTime &dateTime )
350 {
351  QDateTime resultDateTime = dateTime;
352 
353  if ( years )
354  resultDateTime = resultDateTime.addYears( years );
355  if ( months )
356  resultDateTime = resultDateTime.addMonths( months );
357  if ( weeks || days )
358  resultDateTime = resultDateTime.addDays( weeks * 7 + days );
359  if ( hours || minutes || seconds )
360  resultDateTime = resultDateTime.addSecs( hours * 60LL * 60 + minutes * 60 + seconds );
361 
362  return resultDateTime;
363 }
364 
365 QgsTimeDuration QgsTimeDuration::fromString( const QString &string, bool &ok )
366 {
367  ok = false;
368  thread_local const QRegularExpression sRx( QStringLiteral( R"(P(?:([\d]+)Y)?(?:([\d]+)M)?(?:([\d]+)W)?(?:([\d]+)D)?(?:T(?:([\d]+)H)?(?:([\d]+)M)?(?:([\d\.]+)S)?)?$)" ) );
369 
370  const QRegularExpressionMatch match = sRx.match( string );
371  QgsTimeDuration duration;
372  if ( match.hasMatch() )
373  {
374  ok = true;
375  duration.years = match.captured( 1 ).toInt();
376  duration.months = match.captured( 2 ).toInt();
377  duration.weeks = match.captured( 3 ).toInt();
378  duration.days = match.captured( 4 ).toInt();
379  duration.hours = match.captured( 5 ).toInt();
380  duration.minutes = match.captured( 6 ).toInt();
381  duration.seconds = match.captured( 7 ).toDouble();
382  }
383  return duration;
384 }
static QgsExpressionContextScope * mapSettingsScope(const QgsMapSettings &mapSettings)
Creates a new scope which contains variables and functions relating to a QgsMapSettings object.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:45
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:63
A representation of the interval between two datetime values.
Definition: qgsinterval.h:42
static const int MINUTE
Seconds per minute.
Definition: qgsinterval.h:58
double originalDuration() const
Returns the original interval duration.
Definition: qgsinterval.h:280
static const int MONTHS
Seconds per month, based on 30 day month.
Definition: qgsinterval.h:50
double seconds() const
Returns the interval duration in seconds.
Definition: qgsinterval.h:236
static const int HOUR
Seconds per hour.
Definition: qgsinterval.h:56
static const int DAY
Seconds per day.
Definition: qgsinterval.h:54
static const int YEARS
Seconds per year (average)
Definition: qgsinterval.h:48
QgsUnitTypes::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
Definition: qgsinterval.h:295
Interface for map decorations.
virtual QgsDateTimeRange calculateTemporalExtent(QgsMapLayer *layer) const
Attempts to calculate the overall temporal extent for the specified layer, using the settings defined...
virtual QList< QgsDateTimeRange > allTemporalRanges(QgsMapLayer *layer) const
Attempts to calculate the overall list of all temporal extents which are contained in the specified l...
Base class for all map layer types.
Definition: qgsmaplayer.h:73
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
Definition: qgsmaplayer.h:1489
Job implementation that renders everything sequentially using a custom painter.
void waitForFinished() override
Block until the job has finished.
void start()
Start the rendering job and immediately return.
The QgsMapSettings class contains configuration for rendering of the map.
void setFrameRate(double rate)
Sets the frame rate of the map (in frames per second), for maps which are part of an animation.
QColor backgroundColor() const
Returns the background color of the map.
const QgsExpressionContext & expressionContext() const
Gets the expression context.
QSize outputSize() const
Returns the size of the resulting map image, in pixels.
QImage::Format outputImageFormat() const
format of internal QImage, default QImage::Format_ARGB32_Premultiplied
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context.
void setCurrentFrame(long long frame)
Sets the current frame of the map, for maps which are part of an animation.
double outputDpi() const
Returns the DPI (dots per inch) used for conversion between real world units (e.g.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition: qgsproject.h:104
QMap< QString, QgsMapLayer * > mapLayers(const bool validOnly=false) const
Returns a map of all registered layers by layer ID.
Contains information about the context of a rendering operation.
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
Implements a temporal controller based on a frame by frame navigation and animation.
void setFrameDuration(const QgsInterval &duration)
Sets the frame duration, which dictates the temporal length of each frame in the animation.
QgsExpressionContextScope * createExpressionContextScope() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
void setCurrentFrameNumber(long long frame)
Sets the current animation frame number.
long long totalFrameCount() const
Returns the total number of frames for the navigation.
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...
bool isActive() const
Returns true if the temporal property is active.
void setIsTemporal(bool enabled)
Sets whether the temporal range is enabled (i.e.
void setTemporalRange(const QgsDateTimeRange &range)
Sets the temporal range for the object.
static QList< QDateTime > calculateDateTimesUsingDuration(const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok, bool &maxValuesExceeded, int maxValues=-1)
Calculates a complete list of datetimes between start and end, using the specified ISO8601 duration s...
static QgsDateTimeRange calculateTemporalRangeForProject(QgsProject *project)
Calculates the temporal range for a project.
static bool exportAnimation(const QgsMapSettings &mapSettings, const AnimationExportSettings &settings, QString &error, QgsFeedback *feedback=nullptr)
Exports animation frames by rendering the map to multiple destination images.
static QList< QgsDateTimeRange > usedTemporalRangesForProject(QgsProject *project)
Calculates all temporal ranges which are in use for a project.
static QDateTime calculateFrameTime(const QDateTime &start, const long long frame, const QgsInterval &interval)
Calculates the frame time for an animation.
static QList< QDateTime > calculateDateTimesFromISO8601(const QString &string, bool &ok, bool &maxValuesExceeded, int maxValues=-1)
Calculates a complete list of datetimes from a ISO8601 string containing a duration (eg "2021-03-23T0...
Contains utility methods for working with temporal layers and projects.
long long toSeconds() const
Returns the total duration in seconds.
QString toString() const
Converts the duration to an ISO8601 duration string.
static QgsTimeDuration fromString(const QString &string, bool &ok)
Creates a QgsTimeDuration from a string value.
int minutes
Minutes.
QDateTime addToDateTime(const QDateTime &dateTime)
Adds this duration to a starting dateTime value.
double seconds
Seconds.
QgsInterval toInterval() const
Converts the duration to an interval value.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2062
Contains settings relating to exporting animations.
double frameRate
Target animation frame rate in frames per second.
QgsDateTimeRange animationRange
Dictates the overall temporal range of the animation.
QgsInterval frameDuration
Duration of individual export frames.
QString fileNameTemplate
The filename template for exporting the frames.
QString outputDirectory
Destination directory for created image files.
QList< QgsMapDecoration * > decorations
List of decorations to draw onto exported frames.