[FEATURE] Georeference outputs (eg PDF) from composer

This commit makes composer automatically georeference outputs
(where output format makes this possible, eg TIF and PDF).

The existing option to create a world file has been separated
from the map selection for georeferencing. The new behaviour
is to always georeference outputs, and only create the
separate world file if that option is checked.
This commit is contained in:
Nyall Dawson 2016-05-28 07:16:28 +10:00
parent bc779a06ed
commit bae8a0e5e0
7 changed files with 305 additions and 68 deletions

View File

@ -678,6 +678,18 @@ class QgsComposition : QGraphicsScene
*/
void renderRect( QPainter* p, const QRectF& rect );
/** Georeferences a file (image of PDF) exported from the composition.
* @param file filename of exported file
* @param referenceMap map item to use for georeferencing, or leave as nullptr to use the
* currently defined worldFileMap().
* @param exportRegion set to a valid rectangle to indicate that only part of the composition was
* exported
* @param dpi set to DPI of exported file, or leave as -1 to use composition's DPI.
* @note added in QGIS 2.16
*/
void georeferenceOutput( const QString& file, QgsComposerMap* referenceMap = nullptr,
const QRectF& exportRegion = QRectF(), double dpi = -1 ) const;
/** Compute world file parameters. Assumes the whole page containing the associated map item
* will be exported.
*/

View File

@ -1785,6 +1785,7 @@ void QgsComposer::exportCompositionAsPDF( QgsComposer::OutputMode mode )
}
mComposition->doPrint( multiFilePrinter, painter );
painter.end();
mComposition->georeferenceOutput( outputFileName );
}
else
{
@ -1801,6 +1802,8 @@ void QgsComposer::exportCompositionAsPDF( QgsComposer::OutputMode mode )
else
{
bool exportOk = mComposition->exportAsPDF( outputFileName );
mComposition->georeferenceOutput( outputFileName );
if ( !exportOk )
{
QMessageBox::warning( this, tr( "Atlas processing error" ),
@ -2049,7 +2052,7 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode )
mView->setPaintingEnabled( false );
int worldFilePageNo = -1;
if ( mComposition->generateWorldFile() && mComposition->worldFileMap() )
if ( mComposition->worldFileMap() )
{
worldFilePageNo = mComposition->worldFileMap()->page() - 1;
}
@ -2129,20 +2132,25 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode )
if ( i == worldFilePageNo )
{
// should generate world file for this page
double a, b, c, d, e, f;
if ( bounds.isValid() )
mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f );
else
mComposition->computeWorldFileParameters( a, b, c, d, e, f );
mComposition->georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() );
QFileInfo fi( outputFilePath );
// build the world file name
QString outputSuffix = fi.suffix();
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
if ( mComposition->generateWorldFile() )
{
// should generate world file for this page
double a, b, c, d, e, f;
if ( bounds.isValid() )
mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f );
else
mComposition->computeWorldFileParameters( a, b, c, d, e, f );
writeWorldFile( worldFileName, a, b, c, d, e, f );
QFileInfo fi( outputFilePath );
// build the world file name
QString outputSuffix = fi.suffix();
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
writeWorldFile( worldFileName, a, b, c, d, e, f );
}
}
}
@ -2282,7 +2290,7 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode )
QString filename = QDir( dir ).filePath( atlasMap->currentFilename() ) + fileExt;
int worldFilePageNo = -1;
if ( mComposition->generateWorldFile() && mComposition->worldFileMap() )
if ( mComposition->worldFileMap() )
{
worldFilePageNo = mComposition->worldFileMap()->page() - 1;
}
@ -2350,20 +2358,25 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode )
if ( i == worldFilePageNo )
{
// should generate world file for this page
double a, b, c, d, e, f;
if ( bounds.isValid() )
mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f );
else
mComposition->computeWorldFileParameters( a, b, c, d, e, f );
mComposition->georeferenceOutput( imageFilename, nullptr, bounds, imageDlg.resolution() );
QFileInfo fi( imageFilename );
// build the world file name
QString outputSuffix = fi.suffix();
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
if ( mComposition->generateWorldFile() )
{
// should generate world file for this page
double a, b, c, d, e, f;
if ( bounds.isValid() )
mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f );
else
mComposition->computeWorldFileParameters( a, b, c, d, e, f );
writeWorldFile( worldFileName, a, b, c, d, e, f );
QFileInfo fi( imageFilename );
// build the world file name
QString outputSuffix = fi.suffix();
QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
+ outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
writeWorldFile( worldFileName, a, b, c, d, e, f );
}
}
}
}

View File

@ -78,7 +78,6 @@ QgsCompositionWidget::QgsCompositionWidget( QWidget* parent, QgsComposition* c )
// world file generation
mGenerateWorldFileCheckBox->setChecked( mComposition->generateWorldFile() );
mWorldFileMapComboBox->setEnabled( mComposition->generateWorldFile() );
// populate the map list
mWorldFileMapComboBox->setComposition( mComposition );
@ -658,7 +657,6 @@ void QgsCompositionWidget::on_mGenerateWorldFileCheckBox_toggled( bool state )
}
mComposition->setGenerateWorldFile( state );
mWorldFileMapComboBox->setEnabled( state );
}
void QgsCompositionWidget::worldFileMapChanged( QgsComposerItem* item )

View File

@ -57,6 +57,8 @@
#include <QDir>
#include <limits>
#include "gdal.h"
#include "cpl_conv.h"
QgsComposition::QgsComposition( QgsMapRenderer* mapRenderer )
: QGraphicsScene( nullptr )
@ -2886,6 +2888,34 @@ bool QgsComposition::exportAsPDF( const QString& file )
return print( printer );
}
void QgsComposition::georeferenceOutput( const QString& file, QgsComposerMap* map,
const QRectF& exportRegion, double dpi ) const
{
if ( dpi < 0 )
dpi = printResolution();
double* t = computeGeoTransform( map, exportRegion, dpi );
if ( !t )
return;
// important - we need to manually specify the DPI in advance, as GDAL will otherwise
// assume a DPI of 150
CPLSetConfigOption( "GDAL_PDF_DPI", QString::number( dpi ).toLocal8Bit().constData() );
GDALDatasetH outputDS = GDALOpen( file.toLocal8Bit().constData(), GA_Update );
if ( outputDS )
{
GDALSetGeoTransform( outputDS, t );
#if 0
//TODO - metadata can be set here, eg:
GDALSetMetadataItem( outputDS, "AUTHOR", "me", nullptr );
#endif
GDALSetProjection( outputDS, mMapSettings.destinationCrs().toWkt().toLocal8Bit().constData() );
GDALClose( outputDS );
}
CPLSetConfigOption( "GDAL_PDF_DPI", nullptr );
delete[] t;
}
void QgsComposition::doPrint( QPrinter& printer, QPainter& p, bool startNewPage )
{
if ( ddPageSizeActive() )
@ -3086,6 +3116,87 @@ void QgsComposition::renderRect( QPainter* p, const QRectF& rect )
mPlotStyle = savedPlotStyle;
}
double* QgsComposition::computeGeoTransform( const QgsComposerMap* map, const QRectF& region , double dpi ) const
{
if ( !map )
map = worldFileMap();
if ( !map )
return nullptr;
if ( dpi < 0 )
dpi = printResolution();
// calculate region of composition to export (in mm)
QRectF exportRegion = region;
if ( !exportRegion.isValid() )
{
int pageNumber = map->page() - 1;
double pageY = pageNumber * ( mPageHeight + mSpaceBetweenPages );
exportRegion = QRectF( 0, pageY, mPageWidth, mPageHeight );
}
// map rectangle (in mm)
QRectF mapItemSceneRect = map->mapRectToScene( map->rect() );
// destination width/height in mm
double outputHeightMM = exportRegion.height();
double outputWidthMM = exportRegion.width();
// map properties
QgsRectangle mapExtent = *map->currentMapExtent();
double mapXCenter = mapExtent.center().x();
double mapYCenter = mapExtent.center().y();
double alpha = - map->mapRotation() / 180 * M_PI;
double sinAlpha = sin( alpha );
double cosAlpha = cos( alpha );
// get the extent (in map units) for the exported region
QPointF mapItemPos = map->pos();
//adjust item position so it is relative to export region
mapItemPos.rx() -= exportRegion.left();
mapItemPos.ry() -= exportRegion.top();
// calculate extent of entire page in map units
double xRatio = mapExtent.width() / mapItemSceneRect.width();
double yRatio = mapExtent.height() / mapItemSceneRect.height();
double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio;
double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio;
QgsRectangle paperExtent( xmin, ymax - outputHeightMM * yRatio, xmin + outputWidthMM * xRatio, ymax );
// calculate origin of page
double X0 = paperExtent.xMinimum();
double Y0 = paperExtent.yMaximum();
if ( !qgsDoubleNear( alpha, 0.0 ) )
{
// translate origin to account for map rotation
double X1 = X0 - mapXCenter;
double Y1 = Y0 - mapYCenter;
double X2 = X1 * cosAlpha + Y1 * sinAlpha;
double Y2 = -X1 * sinAlpha + Y1 * cosAlpha;
X0 = X2 + mapXCenter;
Y0 = Y2 + mapYCenter;
}
// calculate scaling of pixels
int pageWidthPixels = static_cast< int >( dpi * outputWidthMM / 25.4 );
int pageHeightPixels = static_cast< int >( dpi * outputHeightMM / 25.4 );
double pixelWidthScale = paperExtent.width() / pageWidthPixels;
double pixelHeightScale = paperExtent.height() / pageHeightPixels;
// transform matrix
double* t = new double[6];
t[0] = X0;
t[1] = cosAlpha * pixelWidthScale;
t[2] = -sinAlpha * pixelWidthScale;
t[3] = Y0;
t[4] = -sinAlpha * pixelHeightScale;
t[5] = -cosAlpha * pixelHeightScale;
return t;
}
QString QgsComposition::encodeStringForXML( const QString& str )
{
QString modifiedStr( str );

View File

@ -743,6 +743,18 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene
*/
void renderRect( QPainter* p, const QRectF& rect );
/** Georeferences a file (image of PDF) exported from the composition.
* @param file filename of exported file
* @param referenceMap map item to use for georeferencing, or leave as nullptr to use the
* currently defined worldFileMap().
* @param exportRegion set to a valid rectangle to indicate that only part of the composition was
* exported
* @param dpi set to DPI of exported file, or leave as -1 to use composition's DPI.
* @note added in QGIS 2.16
*/
void georeferenceOutput( const QString& file, QgsComposerMap* referenceMap = nullptr,
const QRectF& exportRegion = QRectF(), double dpi = -1 ) const;
/** Compute world file parameters. Assumes the whole page containing the associated map item
* will be exported.
*/
@ -1075,6 +1087,17 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene
*/
bool ddPageSizeActive() const;
/** Computes a GDAL style geotransform for georeferencing a composition.
* @param referenceMap map item to use for georeferencing, or leave as nullptr to use the
* currently defined worldFileMap().
* @param exportRegion set to a valid rectangle to indicate that only part of the composition is
* being exported
* @param dpi allows overriding the default composition DPI, or leave as -1 to use composition's DPI.
* @note added in QGIS 2.16
*/
double* computeGeoTransform( const QgsComposerMap* referenceMap = nullptr, const QRectF& exportRegion = QRectF(), double dpi = -1 ) const;
private slots:
/*Prepares all data defined expressions*/
void prepareAllDataDefinedExpressions();
@ -1123,6 +1146,7 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene
friend class QgsComposerObject; //for accessing dataDefinedEvaluate, readDataDefinedPropertyMap and writeDataDefinedPropertyMap
friend class QgsComposerModel; //for accessing updateZValues (should not be public)
friend class TestQgsComposition;
};
template<class T> void QgsComposition::composerItems( QList<T*>& itemList )

View File

@ -59,9 +59,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-445</y>
<width>326</width>
<height>949</height>
<y>-311</y>
<width>327</width>
<height>1113</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@ -432,6 +432,35 @@
<string>Export settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="mPrintAsRasterCheckBox">
<property name="text">
<string>Print as raster</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="mGenerateWorldFileCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>If checked, a seperate world file which georeferences exported images will be created</string>
</property>
<property name="text">
<string>Save world file</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QgsSpinBox" name="mResolutionSpinBox">
<property name="suffix">
@ -448,42 +477,6 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="mPrintAsRasterCheckBox">
<property name="text">
<string>Print as raster</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QgsComposerItemComboBox" name="mWorldFileMapComboBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mGenerateWorldFileCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>World file on</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
@ -491,6 +484,26 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Reference map</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QgsComposerItemComboBox" name="mWorldFileMapComboBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Specifies the map which is used to georeference composer exports</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -709,7 +722,6 @@
<tabstop>mResolutionSpinBox</tabstop>
<tabstop>mPrintAsRasterCheckBox</tabstop>
<tabstop>mGenerateWorldFileCheckBox</tabstop>
<tabstop>mWorldFileMapComboBox</tabstop>
<tabstop>mSnapToGridGroupCheckBox</tabstop>
<tabstop>mGridResolutionSpinBox</tabstop>
<tabstop>mOffsetXSpinBox</tabstop>

View File

@ -22,6 +22,7 @@
#include "qgscomposerarrow.h"
#include "qgscomposerhtml.h"
#include "qgscomposerframe.h"
#include "qgscomposermap.h"
#include "qgsmapsettings.h"
#include "qgsmultirenderchecker.h"
#include "qgsfillsymbollayerv2.h"
@ -51,6 +52,7 @@ class TestQgsComposition : public QObject
void resizeToContents();
void resizeToContentsMargin();
void resizeToContentsMultiPage();
void georeference();
private:
QgsComposition *mComposition;
@ -511,5 +513,70 @@ void TestQgsComposition::resizeToContentsMultiPage()
delete composition;
}
void TestQgsComposition::georeference()
{
QgsRectangle extent( 2000, 2800, 2500, 2900 );
QgsMapSettings ms;
ms.setExtent( extent );
QgsComposition* composition = new QgsComposition( ms );
// no map
double* t = composition->computeGeoTransform( nullptr );
QVERIFY( !t );
QgsComposerMap* map = new QgsComposerMap( composition );
map->setNewExtent( extent );
map->setSceneRect( QRectF( 30, 60, 200, 100 ) );
composition->addComposerMap( map );
t = composition->computeGeoTransform( map );
QVERIFY( qgsDoubleNear( t[0], 1925.0, 1.0 ) );
QVERIFY( qgsDoubleNear( t[1], 0.211719, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[2], 0.0 ) );
QVERIFY( qgsDoubleNear( t[3], 3200, 1 ) );
QVERIFY( qgsDoubleNear( t[4], 0.0 ) );
QVERIFY( qgsDoubleNear( t[5], -0.211694, 0.0001 ) );
// don't specify map
composition->setWorldFileMap( map );
t = composition->computeGeoTransform();
QVERIFY( qgsDoubleNear( t[0], 1925.0, 1.0 ) );
QVERIFY( qgsDoubleNear( t[1], 0.211719, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[2], 0.0 ) );
QVERIFY( qgsDoubleNear( t[3], 3200, 1 ) );
QVERIFY( qgsDoubleNear( t[4], 0.0 ) );
QVERIFY( qgsDoubleNear( t[5], -0.211694, 0.0001 ) );
// specify extent
t = composition->computeGeoTransform( map, QRectF( 70, 100, 50, 60 ) );
QVERIFY( qgsDoubleNear( t[0], 2100.0, 1.0 ) );
QVERIFY( qgsDoubleNear( t[1], 0.211864, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[2], 0.0 ) );
QVERIFY( qgsDoubleNear( t[3], 2950, 1 ) );
QVERIFY( qgsDoubleNear( t[4], 0.0 ) );
QVERIFY( qgsDoubleNear( t[5], -0.211864, 0.0001 ) );
// specify dpi
t = composition->computeGeoTransform( map, QRectF(), 75 );
QVERIFY( qgsDoubleNear( t[0], 1925.0, 1 ) );
QVERIFY( qgsDoubleNear( t[1], 0.847603, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[2], 0.0 ) );
QVERIFY( qgsDoubleNear( t[3], 3200.0, 1 ) );
QVERIFY( qgsDoubleNear( t[4], 0.0 ) );
QVERIFY( qgsDoubleNear( t[5], -0.846774, 0.0001 ) );
// rotation
map->setMapRotation( 45 );
t = composition->computeGeoTransform( map );
QVERIFY( qgsDoubleNear( t[0], 1825.7, 1 ) );
QVERIFY( qgsDoubleNear( t[1], 0.149708, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[2], 0.149708, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[3], 2889.64, 1 ) );
QVERIFY( qgsDoubleNear( t[4], 0.14969, 0.0001 ) );
QVERIFY( qgsDoubleNear( t[5], -0.14969, 0.0001 ) );
delete composition;
}
QTEST_MAIN( TestQgsComposition )
#include "testqgscomposition.moc"