QGIS API Documentation 3.41.0-Master (af5edcb665c)
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"
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 );
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.devicePixelRatio(), ms.outputImageFormat() );
135 img.setDevicePixelRatio( ms.devicePixelRatio() );
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
165QDateTime 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() == Qgis::TemporalUnit::Unknown )
172 {
173 const double duration = interval.seconds();
174 return start.addMSecs( frame * duration * 1000 );
175 }
176 else
177 {
178 switch ( interval.originalUnit() )
179 {
181 return start.addMSecs( frame * interval.originalDuration() );
183 return start.addSecs( frame * interval.originalDuration() );
185 return start.addSecs( 60 * frame * interval.originalDuration() );
187 return start.addSecs( 3600 * frame * interval.originalDuration() );
189 return start.addDays( frame * interval.originalDuration() );
191 return start.addDays( 7 * frame * interval.originalDuration() );
193 return start.addMonths( frame * interval.originalDuration() );
195 return start.addYears( frame * interval.originalDuration() );
197 return start.addYears( 10 * frame * interval.originalDuration() );
199 return start.addYears( 100 * frame * interval.originalDuration() );
201 // handled above
202 return QDateTime();
204 // not supported by this method
205 return QDateTime();
206 }
207 }
208 return QDateTime();
209}
210
211QList<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
227QList<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
254QList<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
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
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 )
341 if ( seconds )
342 secs += seconds;
343
344 return secs;
345}
346
347QDateTime 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
363QgsTimeDuration 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}
@ IrregularStep
Special 'irregular step' time unit, used for temporal data which uses irregular, non-real-world unit ...
@ Milliseconds
Milliseconds.
@ Unknown
Unknown time unit.
@ Centuries
Centuries.
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:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
A representation of the interval between two datetime values.
Definition qgsinterval.h:46
static const int MINUTE
Seconds per minute.
Definition qgsinterval.h:62
double originalDuration() const
Returns the original interval duration.
static const int MONTHS
Seconds per month, based on 30 day month.
Definition qgsinterval.h:54
double seconds() const
Returns the interval duration in seconds.
Qgis::TemporalUnit originalUnit() const
Returns the original interval temporal unit.
static const int HOUR
Seconds per hour.
Definition qgsinterval.h:60
static const int DAY
Seconds per day.
Definition qgsinterval.h:58
static const int YEARS
Seconds per year (average)
Definition qgsinterval.h:52
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:76
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.
float devicePixelRatio() const
Returns the device pixel ratio.
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:107
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:444
T end() const
Returns the upper bound of the range.
Definition qgsrange.h:451
static QList< QgsTemporalRange< QDateTime > > mergeRanges(const QList< QgsTemporalRange< QDateTime > > &ranges)
Merges a list of temporal ranges.
Definition qgsrange.h:667
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:6066
QgsTemporalRange< QDateTime > QgsDateTimeRange
QgsRange which stores a range of date times.
Definition qgsrange.h:742
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.