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