Skip to content

Commit 70d2ae2

Browse files
authored
Merge pull request #4730 from Zverik/atlas_rotate
Rotate geometry before calculating bounding box in atlas
2 parents 1aad689 + 391f76b commit 70d2ae2

File tree

2 files changed

+92
-4
lines changed

2 files changed

+92
-4
lines changed

src/core/composer/qgsatlascomposition.cpp

+25-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* (at your option) any later version. *
1515
* *
1616
***************************************************************************/
17+
#include <algorithm>
1718
#include <stdexcept>
1819
#include <QtAlgorithms>
1920

@@ -439,7 +440,30 @@ void QgsAtlasComposition::computeExtent( QgsComposerMap *map )
439440
// QgsGeometry::boundingBox is expressed in the geometry"s native CRS
440441
// We have to transform the geometry to the destination CRS and ask for the bounding box
441442
// Note: we cannot directly take the transformation of the bounding box, since transformations are not linear
442-
mTransformedFeatureBounds = currentGeometry( map->crs() ).boundingBox();
443+
QgsGeometry g = currentGeometry( map->crs() );
444+
// Rotating the geometry, so the bounding box is correct wrt map rotation
445+
if ( map->mapRotation() != 0.0 )
446+
{
447+
QgsPointXY prevCenter = g.boundingBox().center();
448+
g.rotate( map->mapRotation(), g.boundingBox().center() );
449+
// Rotation center will be still the bounding box center of an unrotated geometry.
450+
// Which means, if the center of bbox moves after rotation, the viewport will
451+
// also be offset, and part of the geometry will fall out of bounds.
452+
// Here we compensate for that roughly: by extending the rotated bounds
453+
// so that its center is the same as the original.
454+
QgsRectangle bounds = g.boundingBox();
455+
double dx = std::max( std::abs( prevCenter.x() - bounds.xMinimum() ),
456+
std::abs( prevCenter.x() - bounds.xMaximum() ) );
457+
double dy = std::max( std::abs( prevCenter.y() - bounds.yMinimum() ),
458+
std::abs( prevCenter.y() - bounds.yMaximum() ) );
459+
QgsPointXY center = g.boundingBox().center();
460+
mTransformedFeatureBounds = QgsRectangle( center.x() - dx, center.y() - dy,
461+
center.x() + dx, center.y() + dy );
462+
}
463+
else
464+
{
465+
mTransformedFeatureBounds = g.boundingBox();
466+
}
443467
}
444468

445469
void QgsAtlasComposition::prepareMap( QgsComposerMap *map )

tests/src/python/test_qgsatlascomposition.py

+67-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,25 @@
2222
from qgis.testing import start_app, unittest
2323
from utilities import unitTestDataPath
2424
from qgis.PyQt.QtCore import QFileInfo, QRectF, qWarning
25-
from qgis.core import QgsVectorLayer, QgsProject, QgsCoordinateReferenceSystem, \
26-
QgsComposition, QgsFillSymbol, QgsSingleSymbolRenderer, QgsComposerLabel, QgsComposerMap, QgsFontUtils, \
27-
QgsRectangle, QgsComposerLegend, QgsFeature, QgsGeometry, QgsPointXY, QgsRendererCategory, QgsCategorizedSymbolRenderer, QgsMarkerSymbol
25+
from qgis.core import (
26+
QgsCategorizedSymbolRenderer,
27+
QgsComposerLabel,
28+
QgsComposerLegend,
29+
QgsComposerMap,
30+
QgsComposition,
31+
QgsCoordinateReferenceSystem,
32+
QgsFeature,
33+
QgsFillSymbol,
34+
QgsFontUtils,
35+
QgsGeometry,
36+
QgsMarkerSymbol,
37+
QgsPointXY,
38+
QgsProject,
39+
QgsRectangle,
40+
QgsRendererCategory,
41+
QgsSingleSymbolRenderer,
42+
QgsVectorLayer,
43+
)
2844
from qgscompositionchecker import QgsCompositionChecker
2945

3046
start_app()
@@ -113,6 +129,7 @@ def testCase(self):
113129
self.predefinedscales_render_test()
114130
self.hidden_render_test()
115131
self.legend_test()
132+
self.rotation_test()
116133

117134
shutil.rmtree(tmppath, True)
118135

@@ -311,6 +328,53 @@ def legend_test(self):
311328
self.mComposition.removeComposerItem(legend)
312329
QgsProject.instance().removeMapLayer(ptLayer.id())
313330

331+
def rotation_test(self):
332+
# We will create a polygon layer with a rotated rectangle.
333+
# Then we will make it the object layer for the atlas,
334+
# rotate the map and test that the bounding rectangle
335+
# is smaller than the bounds without rotation.
336+
polygonLayer = QgsVectorLayer('Polygon', 'test_polygon', 'memory')
337+
poly = QgsFeature(polygonLayer.pendingFields())
338+
points = [(10, 15), (15, 10), (45, 40), (40, 45)]
339+
poly.setGeometry(QgsGeometry.fromPolygon([[QgsPointXY(x[0], x[1]) for x in points]]))
340+
polygonLayer.dataProvider().addFeatures([poly])
341+
QgsProject.instance().addMapLayer(polygonLayer)
342+
343+
# Recreating the composer locally
344+
composition = QgsComposition(QgsProject.instance())
345+
composition.setPaperSize(297, 210)
346+
347+
# the atlas map
348+
atlasMap = QgsComposerMap(composition, 20, 20, 130, 130)
349+
atlasMap.setFrameEnabled(True)
350+
atlasMap.setLayers([polygonLayer])
351+
atlasMap.setNewExtent(QgsRectangle(0, 0, 100, 50))
352+
composition.addComposerMap(atlasMap)
353+
354+
# the atlas
355+
atlas = composition.atlasComposition()
356+
atlas.setCoverageLayer(polygonLayer)
357+
atlas.setEnabled(True)
358+
composition.setAtlasMode(QgsComposition.ExportAtlas)
359+
360+
atlasMap.setAtlasDriven(True)
361+
atlasMap.setAtlasScalingMode(QgsComposerMap.Auto)
362+
atlasMap.setAtlasMargin(0.0)
363+
364+
# Testing
365+
atlasMap.setMapRotation(0.0)
366+
atlas.firstFeature()
367+
nonRotatedExtent = QgsRectangle(atlasMap.currentMapExtent())
368+
369+
atlasMap.setMapRotation(45.0)
370+
atlas.firstFeature()
371+
rotatedExtent = QgsRectangle(atlasMap.currentMapExtent())
372+
373+
assert rotatedExtent.width() < nonRotatedExtent.width() * 0.9
374+
assert rotatedExtent.height() < nonRotatedExtent.height() * 0.9
375+
376+
QgsProject.instance().removeMapLayer(polygonLayer)
377+
314378

315379
if __name__ == '__main__':
316380
unittest.main()

0 commit comments

Comments
 (0)