QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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
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
74bool 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
167QDateTime 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
213QList<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
229QList<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
256QList<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
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 )
343 if ( seconds )
344 secs += seconds;
345
346 return secs;
347}
348
349QDateTime 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
365QgsTimeDuration 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:1503
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.
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.
const QgsExpressionContext & expressionContext() const
Gets the expression context.
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:2527
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.