diff --git a/python/core/auto_generated/geometry/qgssphere.sip.in b/python/core/auto_generated/geometry/qgssphere.sip.in new file mode 100644 index 00000000000..3a5f441dbef --- /dev/null +++ b/python/core/auto_generated/geometry/qgssphere.sip.in @@ -0,0 +1,182 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgssphere.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsSphere +{ +%Docstring(signature="appended") +A spherical geometry object. + +Represents a simple 3-dimensional sphere. + +.. versionadded:: 3.34 +%End + +%TypeHeaderCode +#include "qgssphere.h" +%End + public: + + QgsSphere() /HoldGIL/; +%Docstring +Constructor for an invalid QgsSphere. +%End + + QgsSphere( double x, double y, double z, double radius ) /HoldGIL/; +%Docstring +Constructor for QgsSphere with the specified center (``x``, ``y``, ``z``) and ``radius``. +%End + + bool operator==( const QgsSphere &other ) const; + bool operator!=( const QgsSphere &other ) const; + + bool isNull() const /HoldGIL/; +%Docstring +Returns ``True`` if the sphere is a null (default constructed) sphere. +%End + + bool isEmpty() const /HoldGIL/; +%Docstring +Returns ``True`` if the sphere is considered empty, i.e. it has a radius of 0. +%End + + QgsPoint center() const /HoldGIL/; +%Docstring +Returns the center point of the sphere. + +.. seealso:: :py:func:`centerX` + +.. seealso:: :py:func:`centerY` + +.. seealso:: :py:func:`centerZ` + +.. seealso:: :py:func:`setCenter` +%End + + double centerX() const; +%Docstring +Returns the x-coordinate of the center of the sphere. + +.. seealso:: :py:func:`center` + +.. seealso:: :py:func:`centerY` + +.. seealso:: :py:func:`centerZ` + +.. seealso:: :py:func:`setCenter` +%End + + double centerY() const; +%Docstring +Returns the y-coordinate of the center of the sphere. + +.. seealso:: :py:func:`center` + +.. seealso:: :py:func:`centerX` + +.. seealso:: :py:func:`centerZ` + +.. seealso:: :py:func:`setCenter` +%End + + double centerZ() const; +%Docstring +Returns the z-coordinate of the center of the sphere. + +.. seealso:: :py:func:`center` + +.. seealso:: :py:func:`centerX` + +.. seealso:: :py:func:`centerY` + +.. seealso:: :py:func:`setCenter` +%End + + void setCenter( const QgsPoint ¢er ) /HoldGIL/; +%Docstring +Sets the center point of the sphere. + +.. seealso:: :py:func:`center` +%End + + void setCenter( double x, double y, double z ) /HoldGIL/; +%Docstring +Sets the center point of the sphere to (``x``, ``y``, ``z``). + +.. seealso:: :py:func:`center` +%End + + double radius() const /HoldGIL/; +%Docstring +Returns the radius of the sphere. + +.. seealso:: :py:func:`setRadius` + +.. seealso:: :py:func:`diameter` +%End + + void setRadius( double radius ) /HoldGIL/; +%Docstring +Sets the ``radius`` of the sphere. + +.. seealso:: :py:func:`radius` +%End + + double diameter() const /HoldGIL/; +%Docstring +Returns the diameter of the sphere. + +.. seealso:: :py:func:`radius` +%End + + double volume() const /HoldGIL/; +%Docstring +Returns the volume of the sphere. +%End + + double surfaceArea() const /HoldGIL/; +%Docstring +Returns the surface area of the sphere. +%End + + QgsCircle toCircle() const /HoldGIL/; +%Docstring +Converts the sphere to a 2-dimensional circle. +%End + + QgsBox3d boundingBox() const /HoldGIL/; +%Docstring +Returns the 3-dimensional bounding box containing the sphere. +%End + + SIP_PYOBJECT __repr__(); +%MethodCode + QString str; + if ( sipCpp->isNull() ) + { + str = QStringLiteral( "" ).arg( sipCpp->centerX() ).arg( sipCpp->centerY() ).arg( sipCpp->centerZ() ).arg( sipCpp->radius() ); + } + else + { + str = QStringLiteral( "" ).arg( sipCpp->centerX() ).arg( sipCpp->centerY() ).arg( sipCpp->centerZ() ).arg( sipCpp->radius() ); + } + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgssphere.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index a65cf532da2..c833d000cf1 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -351,6 +351,7 @@ %Include auto_generated/geometry/qgsrectangle.sip %Include auto_generated/geometry/qgsreferencedgeometry.sip %Include auto_generated/geometry/qgsregularpolygon.sip +%Include auto_generated/geometry/qgssphere.sip %Include auto_generated/geometry/qgssurface.sip %Include auto_generated/geometry/qgstriangle.sip %Include auto_generated/geometry/qgsvertexid.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0a2f9c4b835..c950a464a5c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -853,6 +853,7 @@ set(QGIS_CORE_SRCS geometry/qgsrectangle.cpp geometry/qgsreferencedgeometry.cpp geometry/qgsregularpolygon.cpp + geometry/qgssphere.cpp geometry/qgssurface.cpp geometry/qgstriangle.cpp geometry/qgsvertexid.cpp @@ -1436,6 +1437,7 @@ set(QGIS_CORE_HDRS geometry/qgsrectangle.h geometry/qgsreferencedgeometry.h geometry/qgsregularpolygon.h + geometry/qgssphere.h geometry/qgssurface.h geometry/qgstriangle.h geometry/qgsvertexid.h diff --git a/src/core/geometry/qgssphere.cpp b/src/core/geometry/qgssphere.cpp new file mode 100644 index 00000000000..d5dc65d17fd --- /dev/null +++ b/src/core/geometry/qgssphere.cpp @@ -0,0 +1,80 @@ +/*************************************************************************** + qgssphere.cpp + -------------- + begin : July 2023 + copyright : (C) 2023 by 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 "qgssphere.h" +#include "qgspoint.h" +#include "qgscircle.h" +#include "qgsbox3d.h" + +QgsSphere::QgsSphere( double x, double y, double z, double radius ) + : mCenterX( x ) + , mCenterY( y ) + , mCenterZ( z ) + , mRadius( radius ) +{ + +} + +bool QgsSphere::isNull() const +{ + return std::isnan( mCenterX ) || std::isnan( mCenterY ) || std::isnan( mCenterZ ); +} + +bool QgsSphere::isEmpty() const +{ + return qgsDoubleNear( mRadius, 0 ); +} + +QgsPoint QgsSphere::center() const +{ + return QgsPoint( mCenterX, mCenterY, mCenterZ ); +} + +void QgsSphere::setCenter( const QgsPoint ¢er ) +{ + mCenterX = center.x(); + mCenterY = center.y(); + mCenterZ = center.z(); +} + +double QgsSphere::volume() const +{ + return 4.0 / 3.0 * M_PI * std::pow( mRadius, 3 ); +} + +double QgsSphere::surfaceArea() const +{ + return 4.0 * M_PI * std::pow( mRadius, 2 ); +} + +QgsCircle QgsSphere::toCircle() const +{ + if ( isNull() ) + return QgsCircle(); + + return QgsCircle( QgsPoint( mCenterX, mCenterY ), mRadius ); +} + +QgsBox3d QgsSphere::boundingBox() const +{ + if ( isNull() ) + return QgsBox3d(); + + return QgsBox3d( mCenterX - mRadius, mCenterY - mRadius, mCenterZ - mRadius, + mCenterX + mRadius, mCenterY + mRadius, mCenterZ + mRadius ); +} + diff --git a/src/core/geometry/qgssphere.h b/src/core/geometry/qgssphere.h new file mode 100644 index 00000000000..b97396c1290 --- /dev/null +++ b/src/core/geometry/qgssphere.h @@ -0,0 +1,188 @@ +/*************************************************************************** + qgssphere.h + -------------- + begin : July 2023 + copyright : (C) 2023 by 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 QGSSPHERE_H +#define QGSSPHERE_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgis.h" +#include + +class QgsPoint; +class QgsCircle; +class QgsBox3d; + +/** + * \ingroup core + * \class QgsSphere + * \brief A spherical geometry object. + * + * Represents a simple 3-dimensional sphere. + * + * \since QGIS 3.34 + */ +class CORE_EXPORT QgsSphere +{ + + public: + + /** + * Constructor for an invalid QgsSphere. + */ + QgsSphere() SIP_HOLDGIL = default; + + /** + * Constructor for QgsSphere with the specified center (\a x, \a y, \a z) and \a radius. + */ + QgsSphere( double x, double y, double z, double radius ) SIP_HOLDGIL; + + bool operator==( const QgsSphere &other ) const + { + return qgsDoubleNear( mCenterX, other.mCenterX ) && qgsDoubleNear( mCenterY, other.mCenterY ) && qgsDoubleNear( mCenterZ, other.mCenterZ ) && qgsDoubleNear( mRadius, other.mRadius ); + } + bool operator!=( const QgsSphere &other ) const { return !( *this == other ); } + + /** + * Returns TRUE if the sphere is a null (default constructed) sphere. + */ + bool isNull() const SIP_HOLDGIL; + + /** + * Returns TRUE if the sphere is considered empty, i.e. it has a radius of 0. + */ + bool isEmpty() const SIP_HOLDGIL; + + /** + * Returns the center point of the sphere. + * + * \see centerX() + * \see centerY() + * \see centerZ() + * \see setCenter() + */ + QgsPoint center() const SIP_HOLDGIL; + + /** + * Returns the x-coordinate of the center of the sphere. + * + * \see center() + * \see centerY() + * \see centerZ() + * \see setCenter() + */ + double centerX() const { return mCenterX; } + + /** + * Returns the y-coordinate of the center of the sphere. + * + * \see center() + * \see centerX() + * \see centerZ() + * \see setCenter() + */ + double centerY() const { return mCenterY; } + + /** + * Returns the z-coordinate of the center of the sphere. + * + * \see center() + * \see centerX() + * \see centerY() + * \see setCenter() + */ + double centerZ() const { return mCenterZ; } + + /** + * Sets the center point of the sphere. + * \see center() + */ + void setCenter( const QgsPoint ¢er ) SIP_HOLDGIL; + + /** + * Sets the center point of the sphere to (\a x, \a y, \a z). + * \see center() + */ + void setCenter( double x, double y, double z ) SIP_HOLDGIL { mCenterX = x; mCenterY = y; mCenterZ = z; } + + /** + * Returns the radius of the sphere. + * + * \see setRadius() + * \see diameter() + */ + double radius() const SIP_HOLDGIL { return mRadius; } + + /** + * Sets the \a radius of the sphere. + * + * \see radius() + */ + void setRadius( double radius ) SIP_HOLDGIL{ mRadius = radius; } + + /** + * Returns the diameter of the sphere. + * + * \see radius() + */ + double diameter() const SIP_HOLDGIL { return mRadius * 2; } + + /** + * Returns the volume of the sphere. + */ + double volume() const SIP_HOLDGIL; + + /** + * Returns the surface area of the sphere. + */ + double surfaceArea() const SIP_HOLDGIL; + + /** + * Converts the sphere to a 2-dimensional circle. + */ + QgsCircle toCircle() const SIP_HOLDGIL; + + /** + * Returns the 3-dimensional bounding box containing the sphere. + */ + QgsBox3d boundingBox() const SIP_HOLDGIL; + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + QString str; + if ( sipCpp->isNull() ) + { + str = QStringLiteral( "" ).arg( sipCpp->centerX() ).arg( sipCpp->centerY() ).arg( sipCpp->centerZ() ).arg( sipCpp->radius() ); + } + else + { + str = QStringLiteral( "" ).arg( sipCpp->centerX() ).arg( sipCpp->centerY() ).arg( sipCpp->centerZ() ).arg( sipCpp->radius() ); + } + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif + + private: + double mCenterX = std::numeric_limits< double >::quiet_NaN(); + double mCenterY = std::numeric_limits< double >::quiet_NaN(); + double mCenterZ = std::numeric_limits< double >::quiet_NaN(); + double mRadius = 1; + +}; + +#endif // QGSSPHERE_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 67c0f7ce3b5..d78478a0e0b 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -347,6 +347,7 @@ ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) ADD_PYTHON_TEST(PyQgsScaleWidget test_qgsscalewidget.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) +ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) ADD_PYTHON_TEST(PyQgsSvgCache test_qgssvgcache.py) ADD_PYTHON_TEST(PyQgsSymbolButton test_qgssymbolbutton.py) ADD_PYTHON_TEST(PyQgsSymbolLayerRegistry test_qgssymbollayerregistry.py) diff --git a/tests/src/python/test_qgssphere.py b/tests/src/python/test_qgssphere.py new file mode 100644 index 00000000000..694c4bfdec8 --- /dev/null +++ b/tests/src/python/test_qgssphere.py @@ -0,0 +1,109 @@ +"""QGIS Unit tests for QgsSphere + +From build dir, run: ctest -R QgsSphere -V + +.. 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__ = '(C) 2023 by Nyall Dawson' +__date__ = '14/07/2023' +__copyright__ = 'Copyright 2023, The QGIS Project' + +import math +import qgis # NOQA +from qgis.core import ( + QgsSphere, + QgsPoint, + QgsCircle, + QgsBox3d +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsSphere(QgisTestCase): + + def test_null(self): + sphere = QgsSphere() + self.assertTrue(sphere.isNull()) + self.assertEqual(str(sphere), '') + + def test_sphere(self): + sphere = QgsSphere(1, 2, 3, 4) + self.assertFalse(sphere.isNull()) + self.assertEqual(sphere.centerX(), 1) + self.assertEqual(sphere.centerY(), 2) + self.assertEqual(sphere.centerZ(), 3) + self.assertEqual(sphere.center(), QgsPoint(1, 2, 3)) + self.assertEqual(sphere.radius(), 4) + self.assertEqual(str(sphere), '') + + def test_setters(self): + sphere = QgsSphere(1, 2, 3, 4) + sphere.setCenter(11, 12, 13) + self.assertEqual(sphere.centerX(), 11) + self.assertEqual(sphere.centerY(), 12) + self.assertEqual(sphere.centerZ(), 13) + self.assertEqual(sphere.radius(), 4) + sphere.setCenter(QgsPoint(21, 22, 23)) + self.assertEqual(sphere.centerX(), 21) + self.assertEqual(sphere.centerY(), 22) + self.assertEqual(sphere.centerZ(), 23) + self.assertEqual(sphere.radius(), 4) + sphere.setRadius(5) + self.assertEqual(sphere.centerX(), 21) + self.assertEqual(sphere.centerY(), 22) + self.assertEqual(sphere.centerZ(), 23) + self.assertEqual(sphere.radius(), 5) + + def test_empty(self): + sphere = QgsSphere(1, 2, 3, 4) + self.assertFalse(sphere.isEmpty()) + sphere = QgsSphere(1, 2, 3, 0) + self.assertTrue(sphere.isEmpty()) + + def test_equality(self): + self.assertEqual(QgsSphere(), QgsSphere()) + self.assertNotEqual(QgsSphere(1, 2, 3, 4), QgsSphere()) + self.assertNotEqual(QgsSphere(), QgsSphere(1, 2, 3, 4)) + self.assertEqual(QgsSphere(1, 2, 3, 4), QgsSphere(1, 2, 3, 4)) + self.assertNotEqual(QgsSphere(1, 2, 3, 4), QgsSphere(11, 2, 3, 4)) + self.assertNotEqual(QgsSphere(1, 2, 3, 4), QgsSphere(1, 12, 3, 4)) + self.assertNotEqual(QgsSphere(1, 2, 3, 4), QgsSphere(1, 2, 13, 4)) + self.assertNotEqual(QgsSphere(1, 2, 3, 4), QgsSphere(1, 2, 3, 14)) + + def test_volume(self): + self.assertEqual(QgsSphere(1, 1, 1, 3).volume(), 113.09733552923254) + self.assertEqual(QgsSphere(1, 1, 1, 0).volume(), 0) + + def test_surface_area(self): + self.assertEqual(QgsSphere(1, 1, 1, 7).surfaceArea(), 615.7521601035994) + self.assertEqual(QgsSphere(1, 1, 1, 0).surfaceArea(), 0) + + def test_to_circle(self): + circle = QgsSphere().toCircle() + self.assertEqual(circle, QgsCircle()) + circle = QgsSphere(1, 2, 3, 4).toCircle() + self.assertEqual(circle, QgsCircle(QgsPoint(1, 2), 4)) + + def test_bounding_box(self): + box = QgsSphere().boundingBox() + self.assertTrue(box.isNull()) + box = QgsSphere(1, 2, 3, 4).boundingBox() + self.assertEqual(box.xMinimum(), -3) + self.assertEqual(box.yMinimum(), -2) + self.assertEqual(box.zMinimum(), -1) + self.assertEqual(box.xMaximum(), 5) + self.assertEqual(box.yMaximum(), 6) + self.assertEqual(box.zMaximum(), 7) + + +if __name__ == '__main__': + unittest.main()