QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
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"
20#include "qgsmapdecoration.h"
21#include "qgsmapsettings.h"
24
25#include <QRegularExpression>
26
28{
29 const QMap<QString, QgsMapLayer *> mapLayers = project->mapLayers();
30 QDateTime minDate;
31 QDateTime maxDate;
32
33 for ( auto it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it )
34 {
35 QgsMapLayer *currentLayer = it.value();
36
37 if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
38 continue;
39 const QgsDateTimeRange layerRange = currentLayer->temporalProperties()->calculateTemporalExtent( currentLayer );
40
41 if ( layerRange.begin().isValid() && ( !minDate.isValid() || layerRange.begin() < minDate ) )
42 minDate = layerRange.begin();
43 if ( layerRange.end().isValid() && ( !maxDate.isValid() || layerRange.end() > maxDate ) )
44 maxDate = layerRange.end();
45 }
46
47 return QgsDateTimeRange( minDate, maxDate );
48}
49
51{
52 const QMap<QString, QgsMapLayer *> mapLayers = project->mapLayers();
53
54 QList< QgsDateTimeRange > ranges;
55 for ( auto it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it )
56 {
57 QgsMapLayer *currentLayer = it.value();
58
59 if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() )
60 continue;
61
62 ranges.append( currentLayer->temporalProperties()->allTemporalRanges( currentLayer ) );
63 }
64
65 return QgsDateTimeRange::mergeRanges( ranges );
66}
67
68bool QgsTemporalUtils::exportAnimation( const QgsMapSettings &mapSettings, const QgsTemporalUtils::AnimationExportSettings &settings, QString &error, QgsFeedback *feedback )
69{
70 if ( settings.fileNameTemplate.isEmpty() )
71 {
72 error = QObject::tr( "Filename template is empty" );
73 return false;
74 }
75 const int numberOfDigits = settings.fileNameTemplate.count( QLatin1Char( '#' ) );
76 if ( numberOfDigits < 0 )
77 {
78 error = QObject::tr( "Wrong filename template format (must contain #)" );
79 return false;
80 }
81 const QString token( numberOfDigits, QLatin1Char( '#' ) );
82 if ( !settings.fileNameTemplate.contains( token ) )
83 {
84 error = QObject::tr( "Filename template must contain all # placeholders in one continuous group." );
85 return false;
86 }
87 if ( !QDir().mkpath( settings.outputDirectory ) )
88 {
89 error = QObject::tr( "Output directory creation failure." );
90 return false;
91 }
92
94 navigator.setTemporalExtents( settings.animationRange );
95 navigator.setFrameDuration( settings.frameDuration );
96 if ( settings.frameDuration.originalUnit() == Qgis::TemporalUnit::IrregularStep )
98
99 QgsMapSettings ms = mapSettings;
100 const QgsExpressionContext context = ms.expressionContext();
101 ms.setFrameRate( settings.frameRate );
102
103 const long long totalFrames = navigator.totalFrameCount();
104 long long currentFrame = 0;
105
106 while ( currentFrame < totalFrames )
107 {
108 if ( feedback )
109 {
110 if ( feedback->isCanceled() )
111 {
112 error = QObject::tr( "Export canceled" );
113 return false;
114 }
115 feedback->setProgress( currentFrame / static_cast<double>( totalFrames ) * 100 );
116 }
117
118 navigator.setCurrentFrameNumber( currentFrame );
119
120 ms.setIsTemporal( true );
121 ms.setTemporalRange( navigator.dateTimeRangeForFrameNumber( currentFrame ) );
122 ms.setCurrentFrame( currentFrame );
123
124 QgsExpressionContext frameContext = context;
125 frameContext.appendScope( navigator.createExpressionContextScope() );
127 ms.setExpressionContext( frameContext );
128
129 QString fileName( settings.fileNameTemplate );
130 const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( currentFrame, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
131 fileName.replace( token, frameNoPaddedLeft );
132 const QString path = QDir( settings.outputDirectory ).filePath( fileName );
133
134 QImage img = QImage( ms.outputSize(), ms.outputImageFormat() );
135 img.setDotsPerMeterX( 1000 * ms.outputDpi() / 25.4 );
136 img.setDotsPerMeterY( 1000 * ms.outputDpi() / 25.4 );
137 img.fill( ms.backgroundColor().rgb() );
138
139 QPainter p( &img );
140 QgsMapRendererCustomPainterJob job( ms, &p );
141 job.start();
142 job.waitForFinished();
143
145 context.setPainter( &p );
146
147 const auto constMDecorations = settings.decorations;
148 for ( QgsMapDecoration *decoration : constMDecorations )
149 {
150 decoration->render( ms, context );
151 }
152
153 p.end();
154
155 img.save( path );
156
157 ++currentFrame;
158 }
159
160 return true;
161}
162
163
164QDateTime QgsTemporalUtils::calculateFrameTime( const QDateTime &start, const long long frame, const QgsInterval &interval )
165{
166
167 double unused;
168 const bool isFractional = !qgsDoubleNear( fabs( modf( interval.originalDuration(), &unused ) ), 0.0 );
169
170 if ( isFractional || interval.originalUnit() == Qgis::TemporalUnit::Unknown )
171 {
172 const double duration = interval.seconds();
173 return start.addMSecs( frame * duration * 1000 );
174 }
175 else
176 {
177 switch ( interval.originalUnit() )
178 {
179 case Qgis::TemporalUnit::Milliseconds:
180 return start.addMSecs( frame * interval.originalDuration() );
181 case Qgis::TemporalUnit::Seconds:
182 return start.addSecs( frame * interval.originalDuration() );
183 case Qgis::TemporalUnit::Minutes:
184 return start.addSecs( 60 * frame * interval.originalDuration() );
185 case Qgis::TemporalUnit::Hours:
186 return start.addSecs( 3600 * frame * interval.originalDuration() );
187 case Qgis::TemporalUnit::Days:
188 return start.addDays( frame * interval.originalDuration() );
189 case Qgis::TemporalUnit::Weeks:
190 return start.addDays( 7 * frame * interval.originalDuration() );
191 case Qgis::TemporalUnit::Months:
192 return start.addMonths( frame * interval.originalDuration() );
193 case Qgis::TemporalUnit::Years:
194 return start.addYears( frame * interval.originalDuration() );
195 case Qgis::TemporalUnit::Decades:
196 return start.addYears( 10 * frame * interval.originalDuration() );
197 case Qgis::TemporalUnit::Centuries:
198 return start.addYears( 100 * frame * interval.originalDuration() );
199 case Qgis::TemporalUnit::Unknown:
200 // handled above
201 return QDateTime();
202 case Qgis::TemporalUnit::IrregularStep:
203 // not supported by this method
204 return QDateTime();
205 }
206 }
207 return QDateTime();
208}
209
210QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok, bool &maxValuesExceeded, int maxValues )
211{
212 ok = false;
213 const QgsTimeDuration timeDuration( QgsTimeDuration::fromString( duration, ok ) );
214 if ( !ok )
215 return {};
216
217 if ( timeDuration.years == 0 && timeDuration.months == 0 && timeDuration.weeks == 0 && timeDuration.days == 0
218 && timeDuration.hours == 0 && timeDuration.minutes == 0 && timeDuration.seconds == 0 )
219 {
220 ok = false;
221 return {};
222 }
223 return calculateDateTimesUsingDuration( start, end, timeDuration, maxValuesExceeded, maxValues );
224}
225
226QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QgsTimeDuration &timeDuration, bool &maxValuesExceeded, int maxValues )
227{
228 QList<QDateTime> res;
229 QDateTime current = start;
230 maxValuesExceeded = false;
231 while ( current <= end )
232 {
233 res << current;
234
235 if ( maxValues >= 0 && res.size() > maxValues )
236 {
237 maxValuesExceeded = true;
238 break;
239 }
240
241 if ( timeDuration.years )
242 current = current.addYears( timeDuration.years );
243 if ( timeDuration.months )
244 current = current.addMonths( timeDuration.months );
245 if ( timeDuration.weeks || timeDuration.days )
246 current = current.addDays( timeDuration.weeks * 7 + timeDuration.days );
247 if ( timeDuration.hours || timeDuration.minutes || timeDuration.seconds )
248 current = current.addSecs( timeDuration.hours * 60LL * 60 + timeDuration.minutes * 60 + timeDuration.seconds );
249 }
250 return res;
251}
252
253QList<QDateTime> QgsTemporalUtils::calculateDateTimesFromISO8601( const QString &string, bool &ok, bool &maxValuesExceeded, int maxValues )
254{
255 ok = false;
256 maxValuesExceeded = false;
257 const QStringList parts = string.split( '/' );
258 if ( parts.length() != 3 )
259 {
260 return {};
261 }
262
263 const QDateTime start = QDateTime::fromString( parts.at( 0 ), Qt::ISODate );
264 if ( !start.isValid() )
265 return {};
266 const QDateTime end = QDateTime::fromString( parts.at( 1 ), Qt::ISODate );
267 if ( !end.isValid() )
268 return {};
269
270 return calculateDateTimesUsingDuration( start, end, parts.at( 2 ), ok, maxValuesExceeded, maxValues );
271}
272
273//
274// QgsTimeDuration
275//
276
278{
280}
281
283{
284 QString text( "P" );
285
286 if ( years )
287 {
288 text.append( QString::number( years ) );
289 text.append( 'Y' );
290 }
291 if ( months )
292 {
293 text.append( QString::number( months ) );
294 text.append( 'M' );
295 }
296 if ( days )
297 {
298 text.append( QString::number( days ) );
299 text.append( 'D' );
300 }
301
302 if ( hours )
303 {
304 if ( !text.contains( 'T' ) )
305 text.append( 'T' );
306 text.append( QString::number( hours ) );
307 text.append( 'H' );
308 }
309 if ( minutes )
310 {
311 if ( !text.contains( 'T' ) )
312 text.append( 'T' );
313 text.append( QString::number( minutes ) );
314 text.append( 'M' );
315 }
316 if ( seconds )
317 {
318 if ( !text.contains( 'T' ) )
319 text.append( 'T' );
320 text.append( QString::number( seconds ) );
321 text.append( 'S' );
322 }
323 return text;
324}
325
327{
328 long long secs = 0.0;
329
330 if ( years )
331 secs += years * QgsInterval::YEARS;
332 if ( months )
333 secs += months * QgsInterval::MONTHS;
334 if ( days )
335 secs += days * QgsInterval::DAY;
336 if ( hours )
337 secs += hours * QgsInterval::HOUR;
338 if ( minutes )
340 if ( seconds )
341 secs += seconds;
342
343 return secs;
344}
345
346QDateTime QgsTimeDuration::addToDateTime( const QDateTime &dateTime )
347{
348 QDateTime resultDateTime = dateTime;
349
350 if ( years )
351 resultDateTime = resultDateTime.addYears( years );
352 if ( months )
353 resultDateTime = resultDateTime.addMonths( months );
354 if ( weeks || days )
355 resultDateTime = resultDateTime.addDays( weeks * 7 + days );
356 if ( hours || minutes || seconds )
357 resultDateTime = resultDateTime.addSecs( hours * 60LL * 60 + minutes * 60 + seconds );
358
359 return resultDateTime;
360}
361
362QgsTimeDuration QgsTimeDuration::fromString( const QString &string, bool &ok )
363{
364 ok = false;
365 thread_local const QRegularExpression sRx( QStringLiteral( R"(P(?:([\d]+)Y)?(?:([\d]+)M)?(?:([\d]+)W)?(?:([\d]+)D)?(?:T(?:([\d]+)H)?(?:([\d]+)M)?(?:([\d\.]+)S)?)?$)" ) );
366
367 const QRegularExpressionMatch match = sRx.match( string );
368 QgsTimeDuration duration;
369 if ( match.hasMatch() )
370 {
371 ok = true;
372 duration.years = match.captured( 1 ).toInt();
373 duration.months = match.captured( 2 ).toInt();
374 duration.weeks = match.captured( 3 ).toInt();
375 duration.days = match.captured( 4 ).toInt();
376 duration.hours = match.captured( 5 ).toInt();
377 duration.minutes = match.captured( 6 ).toInt();
378 duration.seconds = match.captured( 7 ).toDouble();
379 }
380 return duration;
381}
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:286
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:242
Qgis::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
Definition: qgsinterval.h:301
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
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:1523
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:105
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 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.
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:3509
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.
QList< QgsDateTimeRange > availableTemporalRanges
Contains the list of all available temporal ranges which have data available.
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.