diff --git a/python/core/composer/qgscomposition.sip b/python/core/composer/qgscomposition.sip index 62ba81d1103..4f721d41cea 100644 --- a/python/core/composer/qgscomposition.sip +++ b/python/core/composer/qgscomposition.sip @@ -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. */ diff --git a/src/app/composer/qgscomposer.cpp b/src/app/composer/qgscomposer.cpp index d7653ff1bc6..c92f1b585f3 100644 --- a/src/app/composer/qgscomposer.cpp +++ b/src/app/composer/qgscomposer.cpp @@ -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 ); + } } } } diff --git a/src/app/composer/qgscompositionwidget.cpp b/src/app/composer/qgscompositionwidget.cpp index 03a3fa5185f..74207717d31 100644 --- a/src/app/composer/qgscompositionwidget.cpp +++ b/src/app/composer/qgscompositionwidget.cpp @@ -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 ) diff --git a/src/core/composer/qgscomposition.cpp b/src/core/composer/qgscomposition.cpp index 4464d76e03e..bbd3cc61245 100644 --- a/src/core/composer/qgscomposition.cpp +++ b/src/core/composer/qgscomposition.cpp @@ -57,6 +57,8 @@ #include #include +#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 ); diff --git a/src/core/composer/qgscomposition.h b/src/core/composer/qgscomposition.h index e9eef770dff..6b10232cdd5 100644 --- a/src/core/composer/qgscomposition.h +++ b/src/core/composer/qgscomposition.h @@ -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 void QgsComposition::composerItems( QList& itemList ) diff --git a/src/ui/composer/qgscompositionwidgetbase.ui b/src/ui/composer/qgscompositionwidgetbase.ui index be63f0186dc..a605201619e 100644 --- a/src/ui/composer/qgscompositionwidgetbase.ui +++ b/src/ui/composer/qgscompositionwidgetbase.ui @@ -59,9 +59,9 @@ 0 - -445 - 326 - 949 + -311 + 327 + 1113 @@ -432,6 +432,35 @@ Export settings + + + + Print as raster + + + true + + + false + + + + + + + + 0 + 0 + + + + If checked, a seperate world file which georeferences exported images will be created + + + Save world file + + + @@ -448,42 +477,6 @@ - - - - Print as raster - - - true - - - false - - - - - - - false - - - false - - - - - - - - 0 - 0 - - - - World file on - - - @@ -491,6 +484,26 @@ + + + + Reference map + + + + + + + true + + + Specifies the map which is used to georeference composer exports + + + false + + + @@ -709,7 +722,6 @@ mResolutionSpinBox mPrintAsRasterCheckBox mGenerateWorldFileCheckBox - mWorldFileMapComboBox mSnapToGridGroupCheckBox mGridResolutionSpinBox mOffsetXSpinBox diff --git a/tests/src/core/testqgscomposition.cpp b/tests/src/core/testqgscomposition.cpp index b5fcdf65840..f627054c95a 100644 --- a/tests/src/core/testqgscomposition.cpp +++ b/tests/src/core/testqgscomposition.cpp @@ -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"