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