Skip to content

Commit

Permalink
Move logic to calculate combined extent of a set of map layers
Browse files Browse the repository at this point in the history
to new QgsMapLayerUtils class
  • Loading branch information
nyalldawson committed May 21, 2021
1 parent 700390a commit dc50988
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 60 deletions.
42 changes: 42 additions & 0 deletions python/core/auto_generated/qgsmaplayerutils.sip.in
@@ -0,0 +1,42 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgsmaplayerutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/




class QgsMapLayerUtils
{
%Docstring(signature="appended")
Contains utility functions for working with map layers.

.. versionadded:: 3.20
%End

%TypeHeaderCode
#include "qgsmaplayerutils.h"
%End
public:

static QgsRectangle combinedExtent( const QList<QgsMapLayer *> &layers, const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &transformContext );
%Docstring
Returns the combined extent of a list of ``layers``.

The ``crs`` argument specifies the desired coordinate reference system for the combined extent.
%End

};



/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgsmaplayerutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
1 change: 1 addition & 0 deletions python/core/core_auto.sip
Expand Up @@ -104,6 +104,7 @@
%Include auto_generated/qgsmaplayerstylemanager.sip
%Include auto_generated/qgsmaplayerelevationproperties.sip
%Include auto_generated/qgsmaplayertemporalproperties.sip
%Include auto_generated/qgsmaplayerutils.sip
%Include auto_generated/qgsmapsettings.sip
%Include auto_generated/qgsmapsettingsutils.sip
%Include auto_generated/qgsmapthemecollection.sip
Expand Down
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Expand Up @@ -367,6 +367,7 @@ set(QGIS_CORE_SRCS
qgsmaplayerstyle.cpp
qgsmaplayerstylemanager.cpp
qgsmaplayertemporalproperties.cpp
qgsmaplayerutils.cpp
qgsmapsettings.cpp
qgsmapsettingsutils.cpp
qgsmaptopixel.cpp
Expand Down Expand Up @@ -984,6 +985,7 @@ set(QGIS_CORE_HDRS
qgsmaplayerstylemanager.h
qgsmaplayerelevationproperties.h
qgsmaplayertemporalproperties.h
qgsmaplayerutils.h
qgsmapsettings.h
qgsmapsettingsutils.h
qgsmapthemecollection.h
Expand Down
63 changes: 3 additions & 60 deletions src/core/project/qgsprojectviewsettings.cpp
Expand Up @@ -17,6 +17,7 @@
#include "qgis.h"
#include "qgsproject.h"
#include "qgslogger.h"
#include "qgsmaplayerutils.h"
#include <QDomElement>

QgsProjectViewSettings::QgsProjectViewSettings( QgsProject *project )
Expand Down Expand Up @@ -80,66 +81,8 @@ QgsReferencedRectangle QgsProjectViewSettings::fullExtent() const
}
else
{
// reset the map canvas extent since the extent may now be smaller
// We can't use a constructor since QgsRectangle normalizes the rectangle upon construction
QgsRectangle fullExtent;
fullExtent.setMinimal();

// iterate through the map layers and test each layers extent
// against the current min and max values
const QMap<QString, QgsMapLayer *> layers = mProject->mapLayers( true );
QgsDebugMsgLevel( QStringLiteral( "Layer count: %1" ).arg( layers.count() ), 5 );
for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
{
QgsDebugMsgLevel( "Updating extent using " + it.value()->name(), 5 );
QgsDebugMsgLevel( "Input extent: " + it.value()->extent().toString(), 5 );

if ( it.value()->extent().isNull() )
continue;

// Layer extents are stored in the coordinate system (CS) of the
// layer. The extent must be projected to the canvas CS
QgsCoordinateTransform ct( it.value()->crs(), mProject->crs(), mProject->transformContext() );
ct.setBallparkTransformsAreAppropriate( true );
try
{
const QgsRectangle extent = ct.transformBoundingBox( it.value()->extent() );

QgsDebugMsgLevel( "Output extent: " + extent.toString(), 5 );
fullExtent.combineExtentWith( extent );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not reproject layer extent" ) );
}
}

if ( fullExtent.width() == 0.0 || fullExtent.height() == 0.0 )
{
// If all of the features are at the one point, buffer the
// rectangle a bit. If they are all at zero, do something a bit
// more crude.

if ( fullExtent.xMinimum() == 0.0 && fullExtent.xMaximum() == 0.0 &&
fullExtent.yMinimum() == 0.0 && fullExtent.yMaximum() == 0.0 )
{
fullExtent.set( -1.0, -1.0, 1.0, 1.0 );
}
else
{
const double padFactor = 1e-8;
double widthPad = fullExtent.xMinimum() * padFactor;
double heightPad = fullExtent.yMinimum() * padFactor;
double xmin = fullExtent.xMinimum() - widthPad;
double xmax = fullExtent.xMaximum() + widthPad;
double ymin = fullExtent.yMinimum() - heightPad;
double ymax = fullExtent.yMaximum() + heightPad;
fullExtent.set( xmin, ymin, xmax, ymax );
}
}

QgsDebugMsgLevel( "Full extent: " + fullExtent.toString(), 5 );
return QgsReferencedRectangle( fullExtent, mProject->crs() );
const QList< QgsMapLayer * > layers = mProject->mapLayers( true ).values();
return QgsReferencedRectangle( QgsMapLayerUtils::combinedExtent( layers, mProject->crs(), mProject->transformContext() ), mProject->crs() );
}
}

Expand Down
86 changes: 86 additions & 0 deletions src/core/qgsmaplayerutils.cpp
@@ -0,0 +1,86 @@
/***************************************************************************
qgsmaplayerutils.cpp
-------------------
begin : May 2021
copyright : (C) 2021 Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/

/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgsmaplayerutils.h"
#include "qgsrectangle.h"
#include "qgscoordinatereferencesystem.h"
#include "qgscoordinatetransformcontext.h"
#include "qgsreferencedgeometry.h"
#include "qgslogger.h"
#include "qgsmaplayer.h"

QgsRectangle QgsMapLayerUtils::combinedExtent( const QList<QgsMapLayer *> &layers, const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &transformContext )
{
// We can't use a constructor since QgsRectangle normalizes the rectangle upon construction
QgsRectangle fullExtent;
fullExtent.setMinimal();

// iterate through the map layers and test each layers extent
// against the current min and max values
QgsDebugMsgLevel( QStringLiteral( "Layer count: %1" ).arg( layers.count() ), 5 );
for ( const QgsMapLayer *layer : layers )
{
QgsDebugMsgLevel( "Updating extent using " + layer->name(), 5 );
QgsDebugMsgLevel( "Input extent: " + layer->extent().toString(), 5 );

if ( layer->extent().isNull() )
continue;

// Layer extents are stored in the coordinate system (CS) of the
// layer. The extent must be projected to the canvas CS
QgsCoordinateTransform ct( layer->crs(), crs, transformContext );
ct.setBallparkTransformsAreAppropriate( true );
try
{
const QgsRectangle extent = ct.transformBoundingBox( layer->extent() );

QgsDebugMsgLevel( "Output extent: " + extent.toString(), 5 );
fullExtent.combineExtentWith( extent );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not reproject layer extent" ) );
}
}

if ( fullExtent.width() == 0.0 || fullExtent.height() == 0.0 )
{
// If all of the features are at the one point, buffer the
// rectangle a bit. If they are all at zero, do something a bit
// more crude.

if ( fullExtent.xMinimum() == 0.0 && fullExtent.xMaximum() == 0.0 &&
fullExtent.yMinimum() == 0.0 && fullExtent.yMaximum() == 0.0 )
{
fullExtent.set( -1.0, -1.0, 1.0, 1.0 );
}
else
{
const double padFactor = 1e-8;
double widthPad = fullExtent.xMinimum() * padFactor;
double heightPad = fullExtent.yMinimum() * padFactor;
double xmin = fullExtent.xMinimum() - widthPad;
double xmax = fullExtent.xMaximum() + widthPad;
double ymin = fullExtent.yMinimum() - heightPad;
double ymax = fullExtent.yMaximum() + heightPad;
fullExtent.set( xmin, ymin, xmax, ymax );
}
}

QgsDebugMsgLevel( "Full extent: " + fullExtent.toString(), 5 );
return fullExtent;
}
50 changes: 50 additions & 0 deletions src/core/qgsmaplayerutils.h
@@ -0,0 +1,50 @@
/***************************************************************************
qgsmaplayerutils.h
-------------------
begin : May 2021
copyright : (C) 2021 Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/

/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSMAPLAYERUTILS_H
#define QGSMAPLAYERUTILS_H

#include "qgis_sip.h"
#include "qgis_core.h"
#include "qgis.h"

class QgsMapLayer;
class QgsRectangle;
class QgsCoordinateReferenceSystem;
class QgsCoordinateTransformContext;

/**
* \ingroup core
* \brief Contains utility functions for working with map layers.
* \since QGIS 3.20
*/
class CORE_EXPORT QgsMapLayerUtils
{

public:

/**
* Returns the combined extent of a list of \a layers.
*
* The \a crs argument specifies the desired coordinate reference system for the combined extent.
*/
static QgsRectangle combinedExtent( const QList<QgsMapLayer *> &layers, const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &transformContext );

};

#endif // QGSMAPLAYERUTILS_H


1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -181,6 +181,7 @@ ADD_PYTHON_TEST(PyQgsMapLayerFactory test_qgsmaplayerfactory.py)
ADD_PYTHON_TEST(PyQgsMapLayerModel test_qgsmaplayermodel.py)
ADD_PYTHON_TEST(PyQgsMapLayerProxyModel test_qgsmaplayerproxymodel.py)
ADD_PYTHON_TEST(PyQgsMapLayerStore test_qgsmaplayerstore.py)
ADD_PYTHON_TEST(PyQgsMapLayerUtils test_qgsmaplayerutils.py)
ADD_PYTHON_TEST(PyQgsMapRenderer test_qgsmaprenderer.py)
ADD_PYTHON_TEST(PyQgsMapRendererCache test_qgsmaprenderercache.py)
ADD_PYTHON_TEST(PyQgsMapThemeCollection test_qgsmapthemecollection.py)
Expand Down
69 changes: 69 additions & 0 deletions tests/src/python/test_qgsmaplayerutils.py
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsMapLayerUtils.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '2021-05'
__copyright__ = 'Copyright 2021, The QGIS Project'


import qgis # NOQA

from qgis.testing import unittest
from qgis.core import (
QgsMapLayerUtils,
QgsCoordinateReferenceSystem,
QgsCoordinateTransformContext,
QgsVectorLayer,
QgsRasterLayer,
QgsRectangle
)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath

start_app()


class TestQgsMapLayerUtils(unittest.TestCase):

def testCombinedExtent(self):
extent = QgsMapLayerUtils.combinedExtent([], QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext())
self.assertTrue(extent.isEmpty())

layer1 = QgsVectorLayer(unitTestDataPath() + '/points.shp', 'l1')
self.assertTrue(layer1.isValid())

# one layer
extent = QgsMapLayerUtils.combinedExtent([layer1], QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext())
self.assertEqual(extent.toString(3), '-118.889,22.800 : -83.333,46.872')

extent = QgsMapLayerUtils.combinedExtent([layer1], QgsCoordinateReferenceSystem('EPSG:4326'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(3), '-118.889,22.800 : -83.333,46.872')
extent = QgsMapLayerUtils.combinedExtent([layer1], QgsCoordinateReferenceSystem('EPSG:3857'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(0), '-13234651,2607875 : -9276624,5921203')

# two layers
layer2 = QgsRasterLayer(unitTestDataPath() + '/landsat-f32-b1.tif', 'l2')
self.assertTrue(layer2.isValid())
extent = QgsMapLayerUtils.combinedExtent([layer1, layer2], QgsCoordinateReferenceSystem('EPSG:4326'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(3), '-118.889,22.800 : 18.046,46.872')
extent = QgsMapLayerUtils.combinedExtent([layer2, layer1], QgsCoordinateReferenceSystem('EPSG:4326'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(3), '-118.889,22.800 : 18.046,46.872')
extent = QgsMapLayerUtils.combinedExtent([layer1, layer2], QgsCoordinateReferenceSystem('EPSG:3857'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(0), '-13234651,2607875 : 2008833,5921203')
extent = QgsMapLayerUtils.combinedExtent([layer2, layer1], QgsCoordinateReferenceSystem('EPSG:3857'),
QgsCoordinateTransformContext())
self.assertEqual(extent.toString(0), '-13234651,2607875 : 2008833,5921203')


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

0 comments on commit dc50988

Please sign in to comment.