Skip to content

Commit db1062c

Browse files
committedMay 15, 2020
[FEATURE][temporal] Add ability to export animation frames
Allows exporting of temporal animation frames to successive images, for later stitching together in an external application. Users have precise control over the image size and map extent.
1 parent 00ec8df commit db1062c

14 files changed

+915
-7
lines changed
 

‎python/core/auto_generated/qgstemporalutils.sip.in

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ This method considers the temporal range available from layers contained within
3131
returns the maximal combined temporal extent of these layers.
3232
%End
3333

34+
static bool exportAnimation( const QgsMapSettings &mapSettings,
35+
const QgsDateTimeRange &animationRange,
36+
QgsInterval frameDuration,
37+
const QString &outputDirectory,
38+
const QString &fileNameTemplate,
39+
QString &error /Out/,
40+
QgsFeedback *feedback );
41+
%Docstring
42+
Exports animation frames by rendering the map to multiple destination images.
43+
44+
The ``mapSettings`` argument dictates the overall map settings such as extent
45+
and size.
46+
47+
The ``animationRange`` argument specifies the overall temporal range of the animation.
48+
Temporal duration of individual frames is given by ``frameDuration``.
49+
50+
An ``outputDirectory`` must be set, which controls where the created image files are
51+
stored. ``fileNameTemplate`` gives the template for exporting the frames.
52+
This must be in format prefix####.format, where number of
53+
# represents how many 0 should be left-padded to the frame number
54+
e.g. my###.jpg will create frames my001.jpg, my002.jpg, etc
55+
%End
3456
};
3557

3658

‎python/gui/auto_generated/qgstemporalcontrollerwidget.sip.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Returns the temporal controller object used by this object in navigation.
3636
The dock widget retains ownership of the returned object.
3737
%End
3838

39+
3940
};
4041

4142
/************************************************************************

‎src/app/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ SET(QGIS_APP_SRCS
1818
qgsapplayertreeviewmenuprovider.cpp
1919
qgsappwindowmanager.cpp
2020
qgsappscreenshots.cpp
21+
qgsanimationexportdialog.cpp
2122
qgsannotationwidget.cpp
2223
qgsappsslerrorhandler.cpp
2324
qgsattributetabledialog.cpp

‎src/app/qgsanimationexportdialog.cpp

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/***************************************************************************
2+
qgsanimationexportdialog.cpp
3+
-------------------------------------
4+
begin : May 2020
5+
copyright : (C) 2020 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgsanimationexportdialog.h"
19+
#include "qgsmapcanvas.h"
20+
#include "qgsexpressioncontextutils.h"
21+
#include "qgstemporalnavigationobject.h"
22+
#include "qgsprojecttimesettings.h"
23+
#include "qgstemporalutils.h"
24+
25+
Q_GUI_EXPORT extern int qt_defaultDpiX();
26+
27+
QgsAnimationExportDialog::QgsAnimationExportDialog( QWidget *parent, QgsMapCanvas *mapCanvas )
28+
: QDialog( parent )
29+
, mMapCanvas( mapCanvas )
30+
{
31+
setupUi( this );
32+
33+
// Use unrotated visible extent to insure output size and scale matches canvas
34+
QgsMapSettings ms = mMapCanvas->mapSettings();
35+
ms.setRotation( 0 );
36+
mExtent = ms.visibleExtent();
37+
mSize = ms.outputSize();
38+
39+
mExtentGroupBox->setOutputCrs( ms.destinationCrs() );
40+
mExtentGroupBox->setCurrentExtent( mExtent, ms.destinationCrs() );
41+
mExtentGroupBox->setOutputExtentFromCurrent();
42+
mExtentGroupBox->setMapCanvas( mapCanvas );
43+
44+
mStartDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" );
45+
mEndDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" );
46+
47+
QgsSettings settings;
48+
49+
const QString templateText = settings.value( QStringLiteral( "ExportAnimation/fileNameTemplate" ),
50+
QStringLiteral( "%1####.png" ).arg( QgsProject::instance()->baseName() )
51+
, QgsSettings::App ).toString();
52+
mTemplateLineEdit->setText( templateText );
53+
QRegExp rx( QStringLiteral( "\\w+#+\\.{1}\\w+" ) ); //e.g. anyprefix#####.png
54+
QValidator *validator = new QRegExpValidator( rx, this );
55+
mTemplateLineEdit->setValidator( validator );
56+
57+
connect( mTemplateLineEdit, &QLineEdit::textChanged, this, [ = ]
58+
{
59+
QgsSettings settings;
60+
settings.setValue( QStringLiteral( "ExportAnimation/fileNameTemplate" ), mTemplateLineEdit->text() );
61+
} );
62+
63+
mOutputDirFileWidget->setStorageMode( QgsFileWidget::GetDirectory );
64+
mOutputDirFileWidget->setDialogTitle( tr( "Select Directory for Animation Frames" ) );
65+
mOutputDirFileWidget->lineEdit()->setShowClearButton( false );
66+
mOutputDirFileWidget->setDefaultRoot( settings.value( QStringLiteral( "ExportAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );
67+
mOutputDirFileWidget->setFilePath( settings.value( QStringLiteral( "ExportAnimation/lastDir" ), QString(), QgsSettings::App ).toString() );
68+
69+
connect( mOutputDirFileWidget, &QgsFileWidget::fileChanged, this, [ = ]
70+
{
71+
QgsSettings settings;
72+
settings.setValue( QStringLiteral( "ExportAnimation/lastDir" ), mOutputDirFileWidget->filePath(), QgsSettings::App );
73+
} );
74+
75+
for ( QgsUnitTypes::TemporalUnit u :
76+
{
77+
QgsUnitTypes::TemporalMilliseconds,
78+
QgsUnitTypes::TemporalSeconds,
79+
QgsUnitTypes::TemporalMinutes,
80+
QgsUnitTypes::TemporalHours,
81+
QgsUnitTypes::TemporalDays,
82+
QgsUnitTypes::TemporalWeeks,
83+
QgsUnitTypes::TemporalMonths,
84+
QgsUnitTypes::TemporalYears,
85+
QgsUnitTypes::TemporalDecades,
86+
QgsUnitTypes::TemporalCenturies
87+
} )
88+
{
89+
mTimeStepsComboBox->addItem( QgsUnitTypes::toString( u ), u );
90+
}
91+
92+
if ( const QgsTemporalNavigationObject *controller = qobject_cast< const QgsTemporalNavigationObject * >( mMapCanvas->temporalController() ) )
93+
{
94+
mStartDateTime->setDateTime( controller->temporalExtents().begin() );
95+
mEndDateTime->setDateTime( controller->temporalExtents().end() );
96+
}
97+
mFrameDurationSpinBox->setClearValue( 1 );
98+
mFrameDurationSpinBox->setValue( QgsProject::instance()->timeSettings()->timeStep() );
99+
mTimeStepsComboBox->setCurrentIndex( QgsProject::instance()->timeSettings()->timeStepUnit() );
100+
101+
connect( mOutputWidthSpinBox, &QSpinBox::editingFinished, this, [ = ] { updateOutputWidth( mOutputWidthSpinBox->value() );} );
102+
connect( mOutputHeightSpinBox, &QSpinBox::editingFinished, this, [ = ] { updateOutputHeight( mOutputHeightSpinBox->value() );} );
103+
connect( mExtentGroupBox, &QgsExtentGroupBox::extentChanged, this, &QgsAnimationExportDialog::updateExtent );
104+
connect( mLockAspectRatio, &QgsRatioLockButton::lockChanged, this, &QgsAnimationExportDialog::lockChanged );
105+
106+
connect( mSetToProjectTimeButton, &QPushButton::clicked, this, &QgsAnimationExportDialog::setToProjectTime );
107+
108+
connect( buttonBox, &QDialogButtonBox::accepted, this, [ = ]
109+
{
110+
emit startExport();
111+
accept();
112+
} );
113+
114+
updateOutputSize();
115+
}
116+
117+
void QgsAnimationExportDialog::updateOutputWidth( int width )
118+
{
119+
double scale = static_cast<double>( width ) / mSize.width();
120+
double adjustment = ( ( mExtent.width() * scale ) - mExtent.width() ) / 2;
121+
122+
mSize.setWidth( width );
123+
124+
mExtent.setXMinimum( mExtent.xMinimum() - adjustment );
125+
mExtent.setXMaximum( mExtent.xMaximum() + adjustment );
126+
127+
if ( mLockAspectRatio->locked() )
128+
{
129+
int height = width * mExtentGroupBox->ratio().height() / mExtentGroupBox->ratio().width();
130+
double scale = static_cast<double>( height ) / mSize.height();
131+
double adjustment = ( ( mExtent.height() * scale ) - mExtent.height() ) / 2;
132+
133+
whileBlocking( mOutputHeightSpinBox )->setValue( height );
134+
mSize.setHeight( height );
135+
136+
mExtent.setYMinimum( mExtent.yMinimum() - adjustment );
137+
mExtent.setYMaximum( mExtent.yMaximum() + adjustment );
138+
}
139+
140+
whileBlocking( mExtentGroupBox )->setOutputExtentFromUser( mExtent, mExtentGroupBox->currentCrs() );
141+
}
142+
143+
void QgsAnimationExportDialog::updateOutputHeight( int height )
144+
{
145+
double scale = static_cast<double>( height ) / mSize.height();
146+
double adjustment = ( ( mExtent.height() * scale ) - mExtent.height() ) / 2;
147+
148+
mSize.setHeight( height );
149+
150+
mExtent.setYMinimum( mExtent.yMinimum() - adjustment );
151+
mExtent.setYMaximum( mExtent.yMaximum() + adjustment );
152+
153+
if ( mLockAspectRatio->locked() )
154+
{
155+
int width = height * mExtentGroupBox->ratio().width() / mExtentGroupBox->ratio().height();
156+
double scale = static_cast<double>( width ) / mSize.width();
157+
double adjustment = ( ( mExtent.width() * scale ) - mExtent.width() ) / 2;
158+
159+
whileBlocking( mOutputWidthSpinBox )->setValue( width );
160+
mSize.setWidth( width );
161+
162+
mExtent.setXMinimum( mExtent.xMinimum() - adjustment );
163+
mExtent.setXMaximum( mExtent.xMaximum() + adjustment );
164+
}
165+
166+
whileBlocking( mExtentGroupBox )->setOutputExtentFromUser( mExtent, mExtentGroupBox->currentCrs() );
167+
}
168+
169+
void QgsAnimationExportDialog::updateExtent( const QgsRectangle &extent )
170+
{
171+
// leave width as is, update height
172+
mSize.setHeight( mSize.width() * extent.height() / extent.width() );
173+
updateOutputSize();
174+
175+
mExtent = extent;
176+
if ( mLockAspectRatio->locked() )
177+
{
178+
mExtentGroupBox->setRatio( QSize( mSize.width(), mSize.height() ) );
179+
}
180+
}
181+
182+
void QgsAnimationExportDialog::updateOutputSize()
183+
{
184+
whileBlocking( mOutputWidthSpinBox )->setValue( mSize.width() );
185+
whileBlocking( mOutputHeightSpinBox )->setValue( mSize.height() );
186+
}
187+
188+
QgsRectangle QgsAnimationExportDialog::extent() const
189+
{
190+
return mExtentGroupBox->outputExtent();
191+
}
192+
193+
QSize QgsAnimationExportDialog::size() const
194+
{
195+
return mSize;
196+
}
197+
198+
QString QgsAnimationExportDialog::outputDirectory() const
199+
{
200+
return mOutputDirFileWidget->filePath();
201+
}
202+
203+
QString QgsAnimationExportDialog::fileNameExpression() const
204+
{
205+
return mTemplateLineEdit->text();
206+
}
207+
208+
QgsDateTimeRange QgsAnimationExportDialog::animationRange() const
209+
{
210+
return QgsDateTimeRange( mStartDateTime->dateTime(), mEndDateTime->dateTime() );
211+
}
212+
213+
QgsInterval QgsAnimationExportDialog::frameInterval() const
214+
{
215+
return QgsInterval( mFrameDurationSpinBox->value(), static_cast< QgsUnitTypes::TemporalUnit>( mTimeStepsComboBox->currentData().toInt() ) );
216+
}
217+
218+
void QgsAnimationExportDialog::applyMapSettings( QgsMapSettings &mapSettings )
219+
{
220+
QgsSettings settings;
221+
222+
mapSettings.setFlag( QgsMapSettings::Antialiasing, settings.value( QStringLiteral( "qgis/enable_anti_aliasing" ), true ).toBool() );
223+
mapSettings.setFlag( QgsMapSettings::DrawEditingInfo, false );
224+
mapSettings.setFlag( QgsMapSettings::DrawSelection, false );
225+
mapSettings.setSelectionColor( mMapCanvas->mapSettings().selectionColor() );
226+
mapSettings.setDestinationCrs( mMapCanvas->mapSettings().destinationCrs() );
227+
mapSettings.setExtent( extent() );
228+
mapSettings.setOutputSize( size() );
229+
mapSettings.setBackgroundColor( mMapCanvas->canvasColor() );
230+
mapSettings.setRotation( mMapCanvas->rotation() );
231+
mapSettings.setEllipsoid( QgsProject::instance()->ellipsoid() );
232+
mapSettings.setLayers( mMapCanvas->layers() );
233+
mapSettings.setLabelingEngineSettings( mMapCanvas->mapSettings().labelingEngineSettings() );
234+
mapSettings.setTransformContext( QgsProject::instance()->transformContext() );
235+
mapSettings.setPathResolver( QgsProject::instance()->pathResolver() );
236+
237+
//build the expression context
238+
QgsExpressionContext expressionContext;
239+
expressionContext << QgsExpressionContextUtils::globalScope()
240+
<< QgsExpressionContextUtils::projectScope( QgsProject::instance() )
241+
<< QgsExpressionContextUtils::mapSettingsScope( mapSettings );
242+
243+
mapSettings.setExpressionContext( expressionContext );
244+
}
245+
246+
void QgsAnimationExportDialog::setToProjectTime()
247+
{
248+
QgsDateTimeRange range;
249+
250+
// by default try taking the project's fixed temporal extent
251+
if ( QgsProject::instance()->timeSettings() )
252+
range = QgsProject::instance()->timeSettings()->temporalRange();
253+
254+
// if that's not set, calculate the extent from the project's layers
255+
if ( !range.begin().isValid() || !range.end().isValid() )
256+
{
257+
range = QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject::instance() );
258+
}
259+
260+
if ( range.begin().isValid() && range.end().isValid() )
261+
{
262+
whileBlocking( mStartDateTime )->setDateTime( range.begin() );
263+
whileBlocking( mEndDateTime )->setDateTime( range.end() );
264+
}
265+
}
266+
267+
void QgsAnimationExportDialog::lockChanged( const bool locked )
268+
{
269+
if ( locked )
270+
{
271+
mExtentGroupBox->setRatio( QSize( mOutputWidthSpinBox->value(), mOutputHeightSpinBox->value() ) );
272+
}
273+
else
274+
{
275+
mExtentGroupBox->setRatio( QSize( 0, 0 ) );
276+
}
277+
}

‎src/app/qgsanimationexportdialog.h

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/***************************************************************************
2+
qgsanimationexportdialog.h
3+
-------------------------------------
4+
begin : May 2020
5+
copyright : (C) 2020 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#ifndef QGSANIMATIONEXPORTDIALOG_H
19+
#define QGSANIMATIONEXPORTDIALOG_H
20+
21+
#include "ui_qgsanimationexportdialogbase.h"
22+
23+
#include "qgisapp.h"
24+
#include "qgsrectangle.h"
25+
#include "qgshelp.h"
26+
27+
#include <QDialog>
28+
#include <QSize>
29+
30+
class QgsMapCanvas;
31+
32+
33+
/**
34+
* \ingroup app
35+
* \brief A dialog for specifying map animation export settings.
36+
* \since QGIS 3.14
37+
*/
38+
class APP_EXPORT QgsAnimationExportDialog: public QDialog, private Ui::QgsAnimationExportDialogBase
39+
{
40+
Q_OBJECT
41+
42+
public:
43+
44+
/**
45+
* Constructor for QgsAnimationExportDialog
46+
*/
47+
QgsAnimationExportDialog( QWidget *parent = nullptr, QgsMapCanvas *mapCanvas = nullptr );
48+
49+
//! Returns extent rectangle
50+
QgsRectangle extent() const;
51+
52+
//! Returns the output size
53+
QSize size() const;
54+
55+
//! Returns output directory for frames
56+
QString outputDirectory( ) const;
57+
58+
//! Returns filename template for frames
59+
QString fileNameExpression( ) const;
60+
61+
//! Returns the overall animation range
62+
QgsDateTimeRange animationRange() const;
63+
64+
//! Returns the duration of each individual frame
65+
QgsInterval frameInterval() const;
66+
67+
//! configure a map settings object
68+
void applyMapSettings( QgsMapSettings &mapSettings );
69+
70+
signals:
71+
72+
void startExport();
73+
74+
private slots:
75+
76+
void setToProjectTime();
77+
78+
private:
79+
80+
void lockChanged( bool locked );
81+
82+
void updateOutputWidth( int width );
83+
void updateOutputHeight( int height );
84+
void updateExtent( const QgsRectangle &extent );
85+
void updateOutputSize();
86+
87+
QgsMapCanvas *mMapCanvas = nullptr;
88+
89+
QgsRectangle mExtent;
90+
QSize mSize;
91+
92+
QString mInfoDetails;
93+
94+
};
95+
96+
#endif // QGSANIMATIONEXPORTDIALOG_H

‎src/app/qgstemporalcontrollerdockwidget.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
#include "qgstemporalcontrollerdockwidget.h"
1919
#include "qgstemporalcontrollerwidget.h"
2020
#include "qgspanelwidgetstack.h"
21+
#include "qgsanimationexportdialog.h"
22+
#include "qgsmapcanvas.h"
23+
24+
#include "qgstemporalutils.h"
25+
#include "qgstaskmanager.h"
26+
#include "qgsproxyprogresstask.h"
27+
28+
#include <QProgressDialog>
29+
#include <QMessageBox>
30+
2131

2232
QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString &name, QWidget *parent )
2333
: QgsDockWidget( parent )
@@ -29,9 +39,63 @@ QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString
2939
QgsPanelWidgetStack *stack = new QgsPanelWidgetStack();
3040
stack->setMainPanel( mControllerWidget );
3141
setWidget( stack );
42+
43+
connect( mControllerWidget, &QgsTemporalControllerWidget::exportAnimation, this, &QgsTemporalControllerDockWidget::exportAnimation );
3244
}
3345

3446
QgsTemporalController *QgsTemporalControllerDockWidget::temporalController()
3547
{
3648
return mControllerWidget->temporalController();
3749
}
50+
51+
void QgsTemporalControllerDockWidget::exportAnimation()
52+
{
53+
QgsAnimationExportDialog *dlg = new QgsAnimationExportDialog( this, QgisApp::instance()->mapCanvas() );
54+
connect( dlg, &QgsAnimationExportDialog::startExport, this, [ = ]
55+
{
56+
QgsMapSettings s = QgisApp::instance()->mapCanvas()->mapSettings();
57+
dlg->applyMapSettings( s );
58+
59+
const QgsDateTimeRange animationRange = dlg->animationRange();
60+
const QgsInterval frameDuration = dlg->frameInterval();
61+
const QString outputDir = dlg->outputDirectory();
62+
const QString fileNameExpression = dlg->fileNameExpression();
63+
64+
dlg->hide();
65+
66+
QgsFeedback progressFeedback;
67+
QgsScopedProxyProgressTask task( tr( "Exporting animation" ) );
68+
69+
QProgressDialog progressDialog( tr( "Exporting animation…" ), tr( "Abort" ), 0, 100, this );
70+
progressDialog.setWindowTitle( tr( "Exporting Animation" ) );
71+
progressDialog.setWindowModality( Qt::WindowModal );
72+
QString error;
73+
74+
connect( &progressFeedback, &QgsFeedback::progressChanged, this,
75+
[&progressDialog, &progressFeedback, &task]
76+
{
77+
progressDialog.setValue( static_cast<int>( progressFeedback.progress() ) );
78+
task.setProgress( progressFeedback.progress() );
79+
QCoreApplication::processEvents();
80+
} );
81+
82+
connect( &progressDialog, &QProgressDialog::canceled, &progressFeedback, &QgsFeedback::cancel );
83+
84+
bool success = QgsTemporalUtils::exportAnimation(
85+
s,
86+
animationRange,
87+
frameDuration,
88+
outputDir,
89+
fileNameExpression,
90+
error,
91+
&progressFeedback );
92+
93+
progressDialog.hide();
94+
if ( !success )
95+
{
96+
QMessageBox::warning( this, tr( "Export Animation" ), error );
97+
}
98+
} );
99+
dlg->setAttribute( Qt::WA_DeleteOnClose );
100+
dlg->show();
101+
}

‎src/app/qgstemporalcontrollerdockwidget.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class APP_EXPORT QgsTemporalControllerDockWidget : public QgsDockWidget
4747
*/
4848
QgsTemporalController *temporalController();
4949

50+
private slots:
51+
52+
void exportAnimation();
53+
5054
private:
5155

5256
QgsTemporalControllerWidget *mControllerWidget = nullptr;

‎src/core/qgstemporalutils.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include "qgsvectorlayertemporalproperties.h"
2323
#include "qgsrasterlayertemporalproperties.h"
2424
#include "qgsmeshlayertemporalproperties.h"
25+
#include "qgstemporalnavigationobject.h"
26+
#include "qgsmapsettings.h"
27+
#include "qgsmaprenderercustompainterjob.h"
28+
#include "qgsexpressioncontextutils.h"
2529

2630
QgsDateTimeRange QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject *project )
2731
{
@@ -47,3 +51,77 @@ QgsDateTimeRange QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject
4751

4852
return QgsDateTimeRange( minDate, maxDate );
4953
}
54+
55+
bool QgsTemporalUtils::exportAnimation( const QgsMapSettings &mapSettings, const QgsDateTimeRange &animationRange, QgsInterval frameDuration, const QString &outputDirectory, const QString &fileNameTemplate, QString &error, QgsFeedback *feedback )
56+
{
57+
if ( fileNameTemplate.isEmpty() )
58+
{
59+
error = QObject::tr( "Filename template is empty" );
60+
return false;
61+
}
62+
int numberOfDigits = fileNameTemplate.count( QLatin1Char( '#' ) );
63+
if ( numberOfDigits < 0 )
64+
{
65+
error = QObject::tr( "Wrong filename template format (must contain #)" );
66+
return false;
67+
}
68+
const QString token( numberOfDigits, QLatin1Char( '#' ) );
69+
if ( !fileNameTemplate.contains( token ) )
70+
{
71+
error = QObject::tr( "Filename template must contain all # placeholders in one continuous group." );
72+
return false;
73+
}
74+
75+
QgsTemporalNavigationObject navigator;
76+
navigator.setTemporalExtents( animationRange );
77+
navigator.setFrameDuration( frameDuration );
78+
QgsMapSettings settings = mapSettings;
79+
const QgsExpressionContext context = settings.expressionContext();
80+
81+
const long long totalFrames = navigator.totalFrameCount();
82+
long long currentFrame = 0;
83+
84+
while ( currentFrame < totalFrames )
85+
{
86+
if ( feedback )
87+
{
88+
if ( feedback->isCanceled() )
89+
{
90+
error = QObject::tr( "Export canceled" );
91+
return false;
92+
}
93+
feedback->setProgress( currentFrame / static_cast<double>( totalFrames ) * 100 );
94+
}
95+
++currentFrame;
96+
97+
navigator.setCurrentFrameNumber( currentFrame );
98+
99+
settings.setIsTemporal( true );
100+
settings.setTemporalRange( navigator.dateTimeRangeForFrameNumber( currentFrame ) );
101+
102+
QgsExpressionContext frameContext = context;
103+
frameContext.appendScope( navigator.createExpressionContextScope() );
104+
frameContext.appendScope( QgsExpressionContextUtils::mapSettingsScope( settings ) );
105+
settings.setExpressionContext( frameContext );
106+
107+
QString fileName( fileNameTemplate );
108+
const QString frameNoPaddedLeft( QStringLiteral( "%1" ).arg( currentFrame, numberOfDigits, 10, QChar( '0' ) ) ); // e.g. 0001
109+
fileName.replace( token, frameNoPaddedLeft );
110+
const QString path = QDir( outputDirectory ).filePath( fileName );
111+
112+
QImage img = QImage( settings.outputSize(), settings.outputImageFormat() );
113+
img.setDotsPerMeterX( 1000 * settings.outputDpi() / 25.4 );
114+
img.setDotsPerMeterY( 1000 * settings.outputDpi() / 25.4 );
115+
img.fill( settings.backgroundColor().rgb() );
116+
117+
QPainter p( &img );
118+
QgsMapRendererCustomPainterJob job( settings, &p );
119+
job.start();
120+
job.waitForFinished();
121+
p.end();
122+
123+
img.save( path );
124+
}
125+
126+
return true;
127+
}

‎src/core/qgstemporalutils.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
#include "qgis_core.h"
2020
#include "qgsrange.h"
21+
#include "qgsinterval.h"
2122

2223
class QgsProject;
24+
class QgsMapSettings;
25+
class QgsFeedback;
2326

2427
/**
2528
* \ingroup core
@@ -41,6 +44,28 @@ class CORE_EXPORT QgsTemporalUtils
4144
*/
4245
static QgsDateTimeRange calculateTemporalRangeForProject( QgsProject *project );
4346

47+
/**
48+
* Exports animation frames by rendering the map to multiple destination images.
49+
*
50+
* The \a mapSettings argument dictates the overall map settings such as extent
51+
* and size.
52+
*
53+
* The \a animationRange argument specifies the overall temporal range of the animation.
54+
* Temporal duration of individual frames is given by \a frameDuration.
55+
*
56+
* An \a outputDirectory must be set, which controls where the created image files are
57+
* stored. \a fileNameTemplate gives the template for exporting the frames.
58+
* This must be in format prefix####.format, where number of
59+
* # represents how many 0 should be left-padded to the frame number
60+
* e.g. my###.jpg will create frames my001.jpg, my002.jpg, etc
61+
*/
62+
static bool exportAnimation( const QgsMapSettings &mapSettings,
63+
const QgsDateTimeRange &animationRange,
64+
QgsInterval frameDuration,
65+
const QString &outputDirectory,
66+
const QString &fileNameTemplate,
67+
QString &error SIP_OUT,
68+
QgsFeedback *feedback );
4469
};
4570

4671

‎src/gui/qgstemporalcontrollerwidget.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ QgsTemporalControllerWidget::QgsTemporalControllerWidget( QWidget *parent )
6060
connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked );
6161
connect( mSetToProjectTimeButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::mSetToProjectTimeButton_clicked );
6262

63+
connect( mExportAnimationButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::exportAnimation );
64+
6365
QgsDateTimeRange range;
6466

6567
if ( QgsProject::instance()->timeSettings() )

‎src/gui/qgstemporalcontrollerwidget.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ class GUI_EXPORT QgsTemporalControllerWidget : public QgsPanelWidget, private Ui
5151
*/
5252
QgsTemporalController *temporalController();
5353

54+
#ifndef SIP_RUN
55+
56+
signals:
57+
58+
/**
59+
* Triggered when an animation should be exported
60+
*/
61+
void exportAnimation();
62+
63+
#endif
64+
5465
private:
5566

5667
/**

‎src/ui/3d/animationexport3ddialog.ui

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<item row="4" column="0">
2424
<widget class="QLabel" name="mHeightLabel">
2525
<property name="text">
26-
<string>Output Height</string>
26+
<string>Output height</string>
2727
</property>
2828
<property name="buddy">
2929
<cstring>mHeightSpinBox</cstring>
@@ -33,7 +33,7 @@
3333
<item row="2" column="0">
3434
<widget class="QLabel" name="mFpsLabel">
3535
<property name="text">
36-
<string>Frames Per Second</string>
36+
<string>Frames per second</string>
3737
</property>
3838
</widget>
3939
</item>
@@ -47,7 +47,7 @@
4747
<item row="1" column="0">
4848
<widget class="QLabel" name="mOutputDirLabel">
4949
<property name="text">
50-
<string>Output Directory</string>
50+
<string>Output directory</string>
5151
</property>
5252
</widget>
5353
</item>
@@ -74,7 +74,7 @@
7474
<item row="3" column="0">
7575
<widget class="QLabel" name="mWidthLabel">
7676
<property name="text">
77-
<string>Output Width</string>
77+
<string>Output width</string>
7878
</property>
7979
<property name="buddy">
8080
<cstring>mWidthSpinBox</cstring>
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>QgsAnimationExportDialogBase</class>
4+
<widget class="QDialog" name="QgsAnimationExportDialogBase">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>600</width>
10+
<height>629</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Export Map Animation</string>
15+
</property>
16+
<layout class="QGridLayout" name="gridLayout_5" columnstretch="0,0">
17+
<item row="1" column="0">
18+
<widget class="QLabel" name="mOutputDirLabel_2">
19+
<property name="text">
20+
<string>Output directory</string>
21+
</property>
22+
</widget>
23+
</item>
24+
<item row="4" column="0" colspan="2">
25+
<widget class="QGroupBox" name="groupBox">
26+
<property name="title">
27+
<string>Temporal Settings</string>
28+
</property>
29+
<layout class="QGridLayout" name="gridLayout">
30+
<item row="0" column="0">
31+
<widget class="QLabel" name="label_6">
32+
<property name="text">
33+
<string>Range</string>
34+
</property>
35+
</widget>
36+
</item>
37+
<item row="0" column="4">
38+
<widget class="QDateTimeEdit" name="mEndDateTime">
39+
<property name="sizePolicy">
40+
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
41+
<horstretch>0</horstretch>
42+
<verstretch>0</verstretch>
43+
</sizepolicy>
44+
</property>
45+
<property name="displayFormat">
46+
<string>M/d/yyyy h:mm AP</string>
47+
</property>
48+
<property name="timeSpec">
49+
<enum>Qt::UTC</enum>
50+
</property>
51+
</widget>
52+
</item>
53+
<item row="0" column="3">
54+
<widget class="QLabel" name="label_5">
55+
<property name="text">
56+
<string>to </string>
57+
</property>
58+
</widget>
59+
</item>
60+
<item row="0" column="2">
61+
<widget class="QDateTimeEdit" name="mStartDateTime">
62+
<property name="sizePolicy">
63+
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
64+
<horstretch>0</horstretch>
65+
<verstretch>0</verstretch>
66+
</sizepolicy>
67+
</property>
68+
<property name="displayFormat">
69+
<string>M/d/yyyy h:mm AP</string>
70+
</property>
71+
<property name="timeSpec">
72+
<enum>Qt::UTC</enum>
73+
</property>
74+
</widget>
75+
</item>
76+
<item row="0" column="5">
77+
<widget class="QToolButton" name="mSetToProjectTimeButton">
78+
<property name="text">
79+
<string/>
80+
</property>
81+
<property name="icon">
82+
<iconset resource="../../images/images.qrc">
83+
<normaloff>:/images/themes/default/mActionRefresh.svg</normaloff>:/images/themes/default/mActionRefresh.svg</iconset>
84+
</property>
85+
<property name="autoRaise">
86+
<bool>true</bool>
87+
</property>
88+
</widget>
89+
</item>
90+
<item row="1" column="0">
91+
<widget class="QLabel" name="label_2">
92+
<property name="text">
93+
<string>Step (frame length)</string>
94+
</property>
95+
</widget>
96+
</item>
97+
<item row="1" column="2">
98+
<widget class="QgsDoubleSpinBox" name="mFrameDurationSpinBox">
99+
<property name="decimals">
100+
<number>3</number>
101+
</property>
102+
<property name="maximum">
103+
<double>9999999999.000000000000000</double>
104+
</property>
105+
</widget>
106+
</item>
107+
<item row="1" column="3" colspan="3">
108+
<widget class="QComboBox" name="mTimeStepsComboBox">
109+
<property name="editable">
110+
<bool>false</bool>
111+
</property>
112+
<property name="currentText">
113+
<string/>
114+
</property>
115+
</widget>
116+
</item>
117+
</layout>
118+
</widget>
119+
</item>
120+
<item row="6" column="0">
121+
<spacer name="verticalSpacer">
122+
<property name="orientation">
123+
<enum>Qt::Vertical</enum>
124+
</property>
125+
<property name="sizeHint" stdset="0">
126+
<size>
127+
<width>20</width>
128+
<height>40</height>
129+
</size>
130+
</property>
131+
</spacer>
132+
</item>
133+
<item row="0" column="0">
134+
<widget class="QLabel" name="mTemplateLabel_2">
135+
<property name="text">
136+
<string>Template</string>
137+
</property>
138+
</widget>
139+
</item>
140+
<item row="0" column="1">
141+
<widget class="QLineEdit" name="mTemplateLineEdit">
142+
<property name="toolTip">
143+
<string>Number of # represents number of digits (e.g. frame###.png -&gt; frame001.png)</string>
144+
</property>
145+
</widget>
146+
</item>
147+
<item row="2" column="0" colspan="2">
148+
<widget class="QGroupBox" name="groupBox_2">
149+
<property name="title">
150+
<string>Map Settings</string>
151+
</property>
152+
<layout class="QGridLayout" name="gridLayout_3">
153+
<item row="1" column="0">
154+
<widget class="QLabel" name="label_3">
155+
<property name="text">
156+
<string>Output width</string>
157+
</property>
158+
</widget>
159+
</item>
160+
<item row="2" column="1">
161+
<widget class="QgsSpinBox" name="mOutputHeightSpinBox">
162+
<property name="suffix">
163+
<string> px</string>
164+
</property>
165+
<property name="prefix">
166+
<string/>
167+
</property>
168+
<property name="minimum">
169+
<number>1</number>
170+
</property>
171+
<property name="maximum">
172+
<number>99999</number>
173+
</property>
174+
<property name="showClearButton" stdset="0">
175+
<bool>false</bool>
176+
</property>
177+
</widget>
178+
</item>
179+
<item row="1" column="1">
180+
<widget class="QgsSpinBox" name="mOutputWidthSpinBox">
181+
<property name="sizePolicy">
182+
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
183+
<horstretch>0</horstretch>
184+
<verstretch>0</verstretch>
185+
</sizepolicy>
186+
</property>
187+
<property name="suffix">
188+
<string> px</string>
189+
</property>
190+
<property name="prefix">
191+
<string/>
192+
</property>
193+
<property name="minimum">
194+
<number>1</number>
195+
</property>
196+
<property name="maximum">
197+
<number>99999</number>
198+
</property>
199+
<property name="showClearButton" stdset="0">
200+
<bool>false</bool>
201+
</property>
202+
</widget>
203+
</item>
204+
<item row="1" column="2" rowspan="2">
205+
<widget class="QgsRatioLockButton" name="mLockAspectRatio">
206+
<property name="sizePolicy">
207+
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
208+
<horstretch>0</horstretch>
209+
<verstretch>0</verstretch>
210+
</sizepolicy>
211+
</property>
212+
<property name="toolTip">
213+
<string>Lock aspect ratio (including while drawing extent onto canvas)</string>
214+
</property>
215+
<property name="leftMargin" stdset="0">
216+
<number>13</number>
217+
</property>
218+
</widget>
219+
</item>
220+
<item row="0" column="0" colspan="3">
221+
<widget class="QgsExtentGroupBox" name="mExtentGroupBox">
222+
<property name="focusPolicy">
223+
<enum>Qt::StrongFocus</enum>
224+
</property>
225+
<property name="title">
226+
<string>Extent</string>
227+
</property>
228+
</widget>
229+
</item>
230+
<item row="2" column="0">
231+
<widget class="QLabel" name="label_4">
232+
<property name="text">
233+
<string>Output height</string>
234+
</property>
235+
</widget>
236+
</item>
237+
</layout>
238+
</widget>
239+
</item>
240+
<item row="1" column="1">
241+
<widget class="QgsFileWidget" name="mOutputDirFileWidget" native="true"/>
242+
</item>
243+
<item row="7" column="0" colspan="2">
244+
<widget class="QDialogButtonBox" name="buttonBox">
245+
<property name="orientation">
246+
<enum>Qt::Horizontal</enum>
247+
</property>
248+
<property name="standardButtons">
249+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
250+
</property>
251+
</widget>
252+
</item>
253+
</layout>
254+
</widget>
255+
<customwidgets>
256+
<customwidget>
257+
<class>QgsCollapsibleGroupBox</class>
258+
<extends>QGroupBox</extends>
259+
<header>qgscollapsiblegroupbox.h</header>
260+
<container>1</container>
261+
</customwidget>
262+
<customwidget>
263+
<class>QgsSpinBox</class>
264+
<extends>QSpinBox</extends>
265+
<header>qgsspinbox.h</header>
266+
</customwidget>
267+
<customwidget>
268+
<class>QgsExtentGroupBox</class>
269+
<extends>QgsCollapsibleGroupBox</extends>
270+
<header>qgsextentgroupbox.h</header>
271+
<container>1</container>
272+
</customwidget>
273+
<customwidget>
274+
<class>QgsRatioLockButton</class>
275+
<extends>QToolButton</extends>
276+
<header>qgsratiolockbutton.h</header>
277+
<container>1</container>
278+
</customwidget>
279+
<customwidget>
280+
<class>QgsFileWidget</class>
281+
<extends>QWidget</extends>
282+
<header>qgsfilewidget.h</header>
283+
<container>1</container>
284+
</customwidget>
285+
<customwidget>
286+
<class>QgsDoubleSpinBox</class>
287+
<extends>QDoubleSpinBox</extends>
288+
<header>qgsdoublespinbox.h</header>
289+
</customwidget>
290+
</customwidgets>
291+
<resources>
292+
<include location="../../images/images.qrc"/>
293+
</resources>
294+
<connections>
295+
<connection>
296+
<sender>buttonBox</sender>
297+
<signal>rejected()</signal>
298+
<receiver>QgsAnimationExportDialogBase</receiver>
299+
<slot>reject()</slot>
300+
<hints>
301+
<hint type="sourcelabel">
302+
<x>316</x>
303+
<y>260</y>
304+
</hint>
305+
<hint type="destinationlabel">
306+
<x>286</x>
307+
<y>274</y>
308+
</hint>
309+
</hints>
310+
</connection>
311+
</connections>
312+
</ui>

‎src/ui/qgstemporalcontrollerwidgetbase.ui

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<x>0</x>
88
<y>0</y>
99
<width>747</width>
10-
<height>93</height>
10+
<height>100</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -284,6 +284,23 @@
284284
</property>
285285
</spacer>
286286
</item>
287+
<item>
288+
<widget class="QToolButton" name="mExportAnimationButton">
289+
<property name="toolTip">
290+
<string>Export Animation</string>
291+
</property>
292+
<property name="text">
293+
<string/>
294+
</property>
295+
<property name="icon">
296+
<iconset resource="../../images/images.qrc">
297+
<normaloff>:/images/themes/default/mActionFileSave.svg</normaloff>:/images/themes/default/mActionFileSave.svg</iconset>
298+
</property>
299+
<property name="autoRaise">
300+
<bool>true</bool>
301+
</property>
302+
</widget>
303+
</item>
287304
</layout>
288305
</item>
289306
<item>
@@ -316,8 +333,6 @@
316333
</customwidgets>
317334
<resources>
318335
<include location="../../images/images.qrc"/>
319-
<include location="../../images/images.qrc"/>
320-
<include location="../../images/images.qrc"/>
321336
</resources>
322337
<connections/>
323338
</ui>

0 commit comments

Comments
 (0)
Please sign in to comment.