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