Skip to content

Commit

Permalink
[feature] Add a QgsScaleCalculator function to calculate canvas width…
Browse files Browse the repository at this point in the history
… from a given extent, scale, and dpi
  • Loading branch information
nirvn committed Dec 30, 2021
1 parent 6e60731 commit 83b5183
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 15 deletions.
12 changes: 12 additions & 0 deletions python/core/auto_generated/qgsscalecalculator.sip.in
Expand Up @@ -65,6 +65,18 @@ Calculate the scale denominator
:param canvasWidth: Width of the map canvas in pixel (physical) units

:return: scale denominator of current map view, e.g. 1000.0 for a 1:1000 map.
%End

QSizeF calculateImageSize( const QgsRectangle &mapExtent, double scale ) const;
%Docstring
Calculate the image size in pixel (physical) units

:param mapExtent: :py:class:`QgsRectangle` containing the current map extent
:param scale: Scale denominator, e.g. 1000.0 for a 1:1000 map

:return: image size

.. versionadded:: 3.24
%End

double calculateGeographicDistance( const QgsRectangle &mapExtent ) const;
Expand Down
52 changes: 37 additions & 15 deletions src/core/qgsscalecalculator.cpp
Expand Up @@ -49,45 +49,67 @@ QgsUnitTypes::DistanceUnit QgsScaleCalculator::mapUnits() const

double QgsScaleCalculator::calculate( const QgsRectangle &mapExtent, double canvasWidth ) const
{
if ( qgsDoubleNear( canvasWidth, 0. ) || qgsDoubleNear( mDpi, 0.0 ) )
{
QgsDebugMsg( QStringLiteral( "Can't calculate scale from the input values" ) );
return 0;
}

double conversionFactor = 0;
double delta = 0;
calculateMetrics( mapExtent, delta, conversionFactor );

const double scale = ( delta * conversionFactor ) / ( static_cast< double >( canvasWidth ) / mDpi );
QgsDebugMsgLevel( QStringLiteral( "scale = %1 conversionFactor = %2" ).arg( scale ).arg( conversionFactor ), 4 );
return scale;
}

QSizeF QgsScaleCalculator::calculateImageSize( const QgsRectangle &mapExtent, double scale ) const
{
if ( qgsDoubleNear( scale, 0.0 ) || qgsDoubleNear( mDpi, 0.0 ) )
{
QgsDebugMsg( QStringLiteral( "Can't calculate image size from the input values" ) );
return QSizeF();
}
double conversionFactor = 0;
double delta = 0;
// calculation is based on the map units and extent, the dpi of the
// users display, and the canvas width

calculateMetrics( mapExtent, delta, conversionFactor );
const double imageWidth = ( delta * conversionFactor ) / ( static_cast< double >( scale ) ) * mDpi;
const double deltaHeight = ( mapExtent.yMaximum() - mapExtent.yMinimum() ) * delta / ( mapExtent.xMaximum() - mapExtent.xMinimum() );
const double imageHeight = ( deltaHeight * conversionFactor ) / ( static_cast< double >( scale ) ) * mDpi;

QgsDebugMsgLevel( QStringLiteral( "imageWidth = %1 imageHeight = %2 conversionFactor = %3" )
.arg( imageWidth ).arg( imageHeight ).arg( conversionFactor ), 4 );

return QSizeF( imageWidth, imageHeight );
}

void QgsScaleCalculator::calculateMetrics( const QgsRectangle &mapExtent, double &delta, double &conversionFactor ) const
{
delta = mapExtent.xMaximum() - mapExtent.xMinimum();
switch ( mMapUnits )
{
case QgsUnitTypes::DistanceMeters:
// convert meters to inches
conversionFactor = 39.3700787;
delta = mapExtent.xMaximum() - mapExtent.xMinimum();
break;
case QgsUnitTypes::DistanceFeet:
conversionFactor = 12.0;
delta = mapExtent.xMaximum() - mapExtent.xMinimum();
break;
case QgsUnitTypes::DistanceNauticalMiles:
// convert nautical miles to inches
conversionFactor = 72913.4;
delta = mapExtent.xMaximum() - mapExtent.xMinimum();
break;

default:
case QgsUnitTypes::DistanceDegrees:
// degrees require conversion to meters first
conversionFactor = 39.3700787;
delta = calculateGeographicDistance( mapExtent );
break;
}
if ( qgsDoubleNear( canvasWidth, 0. ) || qgsDoubleNear( mDpi, 0.0 ) )
{
QgsDebugMsg( QStringLiteral( "Can't calculate scale from the input values" ) );
return 0;
}
const double scale = ( delta * conversionFactor ) / ( static_cast< double >( canvasWidth ) / mDpi );
QgsDebugMsgLevel( QStringLiteral( "scale = %1 conversionFactor = %2" ).arg( scale ).arg( conversionFactor ), 4 );
return scale;
}


double QgsScaleCalculator::calculateGeographicDistance( const QgsRectangle &mapExtent ) const
{
// need to calculate the x distance in meters
Expand Down
12 changes: 12 additions & 0 deletions src/core/qgsscalecalculator.h
Expand Up @@ -72,6 +72,15 @@ class CORE_EXPORT QgsScaleCalculator
*/
double calculate( const QgsRectangle &mapExtent, double canvasWidth ) const;

/**
* Calculate the image size in pixel (physical) units
* \param mapExtent QgsRectangle containing the current map extent
* \param scale Scale denominator, e.g. 1000.0 for a 1:1000 map
* \returns image size
* \since QGIS 3.24
*/
QSizeF calculateImageSize( const QgsRectangle &mapExtent, double scale ) const;

/**
* Calculate the distance between two points in geographic coordinates.
* Used to calculate scale for map views with geographic (decimal degree)
Expand All @@ -82,6 +91,9 @@ class CORE_EXPORT QgsScaleCalculator

private:

//! Calculate the \a delta and \a conversionFactor values based on the provided \a mapExtent.
void calculateMetrics( const QgsRectangle &mapExtent, double &delta, double &conversionFactor ) const;

//! dpi member
double mDpi;

Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -291,6 +291,7 @@ ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py)
ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py)
ADD_PYTHON_TEST(PyQgsRulebasedRenderer test_qgsrulebasedrenderer.py)
ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py)
ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py)
ADD_PYTHON_TEST(PyQgsScaleWidget test_qgsscalewidget.py)
ADD_PYTHON_TEST(PyQgsServerWMSGetFeatureInfo test_qgsserver_wms_getfeatureinfo.py)
ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py)
Expand Down
39 changes: 39 additions & 0 deletions tests/src/python/test_qgsscalecalculator.py
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsScaleCalculator
.. 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__ = 'Mathieu Pellerin'
__date__ = '30/12/2021'
__copyright__ = 'Copyright 2021, The QGIS Project'

import qgis # NOQA
import math
from qgis.PyQt.QtCore import Qt, QSizeF
from qgis.PyQt.QtTest import QSignalSpy

from qgis.core import QgsScaleCalculator, QgsRectangle, QgsUnitTypes
from qgis.testing import start_app, unittest

start_app()


class TestQgsScaleWidget(unittest.TestCase):

def testCalculateImageSize(self):
calculator = QgsScaleCalculator()

calculator.setDpi(96)
calculator.setMapUnits(QgsUnitTypes.DistanceMeters)

extent = QgsRectangle(336609, 1162304, 354942, 1168151)
image_size = calculator.calculateImageSize(extent, 65000)
self.assertAlmostEqual(image_size.width(), 1066.001, 3)
self.assertAlmostEqual(image_size.height(), 339.983, 3)


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

0 comments on commit 83b5183

Please sign in to comment.