QGIS API Documentation  3.23.0-Master (eb871beae0)
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 
105  const long long totalFrames = navigator.totalFrameCount();
106  long long currentFrame = 0;
107 
108  while ( currentFrame < totalFrames )
109  {
110  if ( feedback )
111  {
112  if ( feedback->isCanceled() )
113  {
114  error = QObject::tr( "Export canceled" );
115  return false;
116  }
117  feedback->setProgress( currentFrame / static_cast<double>( totalFrames ) * 100 );
118  }
119 
120  navigator.setCurrentFrameNumber( currentFrame );
121 
122  ms.setIsTemporal( true );
123  ms.setTemporalRange( navigator.dateTimeRangeForFrameNumber( currentFrame ) );
124 
125  QgsExpressionContext frameContext = context;
126  frameContext.appendScope( navigator.createExpressionContextScope() );
128  ms.setExpressionContext( frameContext );
129 
130  QString fileName( settings.fileNameTemplate );
131  const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( currentFrame, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
132  fileName.replace( token, frameNoPaddedLeft );
133  const QString path = QDir( settings.outputDirectory ).filePath( fileName );
134 
135  QImage img = QImage( ms.outputSize(), ms.outputImageFormat() );
136  img.setDotsPerMeterX( 1000 * ms.outputDpi() / 25.4 );
137  img.setDotsPerMeterY( 1000 * ms.outputDpi() / 25.4 );
138  img.fill( ms.backgroundColor().rgb() );
139 
140  QPainter p( &img );
141  QgsMapRendererCustomPainterJob job( ms, &p );
142  job.start();
143  job.waitForFinished();
144 
146  context.setPainter( &p );
147 
148  const auto constMDecorations = settings.decorations;
149  for ( QgsMapDecoration *decoration : constMDecorations )
150  {
151  decoration->render( ms, context );
152  }
153 
154  p.end();
155 
156  img.save( path );
157 
158  ++currentFrame;
159  }
160 
161  return true;
162 }
163 
164 
165 QDateTime QgsTemporalUtils::calculateFrameTime( const QDateTime &start, const long long frame, const QgsInterval &interval )
166 {
167 
168  double unused;
169  const bool isFractional = !qgsDoubleNear( fabs( modf( interval.originalDuration(), &unused ) ), 0.0 );
170 
171  if ( isFractional || interval.originalUnit() == QgsUnitTypes::TemporalUnit::TemporalUnknownUnit )
172  {
173  const double duration = interval.seconds();
174  return start.addMSecs( frame * duration * 1000 );
175  }
176  else
177  {
178  switch ( interval.originalUnit() )
179  {
180  case QgsUnitTypes::TemporalUnit::TemporalMilliseconds:
181  return start.addMSecs( frame * interval.originalDuration() );
182  case QgsUnitTypes::TemporalUnit::TemporalSeconds:
183  return start.addSecs( frame * interval.originalDuration() );
184  case QgsUnitTypes::TemporalUnit::TemporalMinutes:
185  return start.addSecs( 60 * frame * interval.originalDuration() );
186  case QgsUnitTypes::TemporalUnit::TemporalHours:
187  return start.addSecs( 3600 * frame * interval.originalDuration() );
188  case QgsUnitTypes::TemporalUnit::TemporalDays:
189  return start.addDays( frame * interval.originalDuration() );
190  case QgsUnitTypes::TemporalUnit::TemporalWeeks:
191  return start.addDays( 7 * frame * interval.originalDuration() );
192  case QgsUnitTypes::TemporalUnit::TemporalMonths:
193  return start.addMonths( frame * interval.originalDuration() );
194  case QgsUnitTypes::TemporalUnit::TemporalYears:
195  return start.addYears( frame * interval.originalDuration() );
196  case QgsUnitTypes::TemporalUnit::TemporalDecades:
197  return start.addYears( 10 * frame * interval.originalDuration() );
198  case QgsUnitTypes::TemporalUnit::TemporalCenturies:
199  return start.addYears( 100 * frame * interval.originalDuration() );
200  case QgsUnitTypes::TemporalUnit::TemporalUnknownUnit:
201  // handled above
202  return QDateTime();
203  case QgsUnitTypes::TemporalUnit::TemporalIrregularStep:
204  // not supported by this method
205  return QDateTime();
206  }
207  }
208  return QDateTime();
209 }
210 
211 QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok, bool &maxValuesExceeded, int maxValues )
212 {
213  ok = false;
214  const QgsTimeDuration timeDuration( QgsTimeDuration::fromString( duration, ok ) );
215  if ( !ok )
216  return {};
217 
218  if ( timeDuration.years == 0 && timeDuration.months == 0 && timeDuration.weeks == 0 && timeDuration.days == 0
219  && timeDuration.hours == 0 && timeDuration.minutes == 0 && timeDuration.seconds == 0 )
220  {
221  ok = false;
222  return {};
223  }
224  return calculateDateTimesUsingDuration( start, end, timeDuration, maxValuesExceeded, maxValues );
225 }
226 
227 QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QgsTimeDuration &timeDuration, bool &maxValuesExceeded, int maxValues )
228 {
229  QList<QDateTime> res;
230  QDateTime current = start;
231  maxValuesExceeded = false;
232  while ( current <= end )
233  {
234  res << current;
235 
236  if ( maxValues >= 0 && res.size() > maxValues )
237  {
238  maxValuesExceeded = true;
239  break;
240  }
241 
242  if ( timeDuration.years )
243  current = current.addYears( timeDuration.years );
244  if ( timeDuration.months )
245  current = current.addMonths( timeDuration.months );
246  if ( timeDuration.weeks || timeDuration.days )
247  current = current.addDays( timeDuration.weeks * 7 + timeDuration.days );
248  if ( timeDuration.hours || timeDuration.minutes || timeDuration.seconds )
249  current = current.addSecs( timeDuration.hours * 60LL * 60 + timeDuration.minutes * 60 + timeDuration.seconds );
250  }
251  return res;
252 }
253 
254 QList<QDateTime> QgsTemporalUtils::calculateDateTimesFromISO8601( const QString &string, bool &ok, bool &maxValuesExceeded, int maxValues )
255 {
256  ok = false;
257  maxValuesExceeded = false;
258  const QStringList parts = string.split( '/' );
259  if ( parts.length() != 3 )
260  {
261  return {};
262  }
263 
264  const QDateTime start = QDateTime::fromString( parts.at( 0 ), Qt::ISODate );
265  if ( !start.isValid() )
266  return {};
267  const QDateTime end = QDateTime::fromString( parts.at( 1 ), Qt::ISODate );
268  if ( !end.isValid() )
269  return {};
270 
271  return calculateDateTimesUsingDuration( start, end, parts.at( 2 ), ok, maxValuesExceeded, maxValues );
272 }
273 
274 //
275 // QgsTimeDuration
276 //
277 
279 {
281 }
282 
284 {
285  QString text( "P" );
286 
287  if ( years )
288  {
289  text.append( QString::number( years ) );
290  text.append( 'Y' );
291  }
292  if ( months )
293  {
294  text.append( QString::number( months ) );
295  text.append( 'M' );
296  }
297  if ( days )
298  {
299  text.append( QString::number( days ) );
300  text.append( 'D' );
301  }
302 
303  if ( hours )
304  {
305  if ( !text.contains( 'T' ) )
306  text.append( 'T' );
307  text.append( QString::number( hours ) );
308  text.append( 'H' );
309  }
310  if ( minutes )
311  {
312  if ( !text.contains( 'T' ) )
313  text.append( 'T' );
314  text.append( QString::number( minutes ) );
315  text.append( 'M' );
316  }
317  if ( seconds )
318  {
319  if ( !text.contains( 'T' ) )
320  text.append( 'T' );
321  text.append( QString::number( seconds ) );
322  text.append( 'S' );
323  }
324  return text;
325 }
326 
327 long long QgsTimeDuration::toSeconds() const
328 {
329  long long secs = 0.0;
330 
331  if ( years )
332  secs += years * QgsInterval::YEARS;
333  if ( months )
334  secs += months * QgsInterval::MONTHS;
335  if ( days )
336  secs += days * QgsInterval::DAY;
337  if ( hours )
338  secs += hours * QgsInterval::HOUR;
339  if ( minutes )
340  secs += minutes * QgsInterval::MINUTE;
341  if ( seconds )
342  secs += seconds;
343 
344  return secs;
345 }
346 
347 QDateTime QgsTimeDuration::addToDateTime( const QDateTime &dateTime )
348 {
349  QDateTime resultDateTime = dateTime;
350 
351  if ( years )
352  resultDateTime = resultDateTime.addYears( years );
353  if ( months )
354  resultDateTime = resultDateTime.addMonths( months );
355  if ( weeks || days )
356  resultDateTime = resultDateTime.addDays( weeks * 7 + days );
357  if ( hours || minutes || seconds )
358  resultDateTime = resultDateTime.addSecs( hours * 60LL * 60 + minutes * 60 + seconds );
359 
360  return resultDateTime;
361 }
362 
363 QgsTimeDuration QgsTimeDuration::fromString( const QString &string, bool &ok )
364 {
365  ok = false;
366  thread_local const QRegularExpression sRx( QStringLiteral( R"(P(?:([\d]+)Y)?(?:([\d]+)M)?(?:([\d]+)W)?(?:([\d]+)D)?(?:T(?:([\d]+)H)?(?:([\d]+)M)?(?:([\d\.]+)S)?)?$)" ) );
367 
368  const QRegularExpressionMatch match = sRx.match( string );
369  QgsTimeDuration duration;
370  if ( match.hasMatch() )
371  {
372  ok = true;
373  duration.years = match.captured( 1 ).toInt();
374  duration.months = match.captured( 2 ).toInt();
375  duration.weeks = match.captured( 3 ).toInt();
376  duration.days = match.captured( 4 ).toInt();
377  duration.hours = match.captured( 5 ).toInt();
378  duration.minutes = match.captured( 6 ).toInt();
379  duration.seconds = match.captured( 7 ).toDouble();
380  }
381  return duration;
382 }
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.
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.
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:101
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:1504
Contains settings relating to exporting animations.
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.