Skip to content

Commit

Permalink
[pointclouds] Add API to retrieve precalculated attribute statistics
Browse files Browse the repository at this point in the history
from the provider metadata

This method only returns existing statistics, it cannot be used
to calculated new ones!
  • Loading branch information
nyalldawson committed Dec 2, 2020
1 parent 72d829c commit 3df0469
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 1 deletion.
Expand Up @@ -75,6 +75,35 @@ Only providers which report the CreateRenderer capability will return a 2D rende
providers will return ``None``.
%End


SIP_PYOBJECT metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const;
%Docstring
Returns a statistic for the specified ``attribute``, taken only from the metadata of the point cloud
data source.

This method will not perform any statistical calculations, rather it will return only precomputed attribute
statistics which are included in the data source's metadata. Not all data sources include this information
in the metadata, and even for sources with statistical metadata only some ``statistic`` values may be available.

:raises ValueError: if no matching precalculated statistic is available for the attribute.
%End
%MethodCode
{
const QVariant res = sipCpp->metadataStatistic( *a0, a1 );
if ( !res.isValid() )
{
PyErr_SetString( PyExc_ValueError, QStringLiteral( "Statistic is not available" ).toUtf8().constData() );
sipIsErr = 1;
}
else
{
QVariant *v = new QVariant( res );
sipRes = sipConvertFromNewType( v, sipType_QVariant, Py_None );
}
}
%End


};

/************************************************************************
Expand Down
5 changes: 5 additions & 0 deletions src/core/pointcloud/qgspointclouddataprovider.cpp
Expand Up @@ -38,3 +38,8 @@ QgsPointCloudRenderer *QgsPointCloudDataProvider::createRenderer( const QVariant
{
return nullptr;
}

QVariant QgsPointCloudDataProvider::metadataStatistic( const QString &, QgsStatisticalSummary::Statistic ) const
{
return QVariant();
}
45 changes: 45 additions & 0 deletions src/core/pointcloud/qgspointclouddataprovider.h
Expand Up @@ -21,6 +21,7 @@
#include "qgis_core.h"
#include "qgsdataprovider.h"
#include "qgspointcloudattribute.h"
#include "qgsstatisticalsummary.h"
#include <memory>

class QgsPointCloudIndex;
Expand Down Expand Up @@ -95,6 +96,50 @@ class CORE_EXPORT QgsPointCloudDataProvider: public QgsDataProvider
*/
virtual QgsPointCloudRenderer *createRenderer( const QVariantMap &configuration = QVariantMap() ) const SIP_FACTORY;

#ifndef SIP_RUN

/**
* Returns a statistic for the specified \a attribute, taken only from the metadata of the point cloud
* data source.
*
* This method will not perform any statistical calculations, rather it will return only precomputed attribute
* statistics which are included in the data source's metadata. Not all data sources include this information
* in the metadata, and even for sources with statistical metadata only some \a statistic values may be available.
*
* If no matching precalculated statistic is available then an invalid variant will be returned.
*/
virtual QVariant metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const;
#else

/**
* Returns a statistic for the specified \a attribute, taken only from the metadata of the point cloud
* data source.
*
* This method will not perform any statistical calculations, rather it will return only precomputed attribute
* statistics which are included in the data source's metadata. Not all data sources include this information
* in the metadata, and even for sources with statistical metadata only some \a statistic values may be available.
*
* \throws ValueError if no matching precalculated statistic is available for the attribute.
*/
SIP_PYOBJECT metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const;
% MethodCode
{
const QVariant res = sipCpp->metadataStatistic( *a0, a1 );
if ( !res.isValid() )
{
PyErr_SetString( PyExc_ValueError, QStringLiteral( "Statistic is not available" ).toUtf8().constData() );
sipIsErr = 1;
}
else
{
QVariant *v = new QVariant( res );
sipRes = sipConvertFromNewType( v, sipType_QVariant, Py_None );
}
}
% End
#endif


};

#endif // QGSMESHDATAPROVIDER_H
61 changes: 60 additions & 1 deletion src/core/providers/ept/qgseptpointcloudindex.cpp
Expand Up @@ -149,7 +149,22 @@ bool QgsEptPointCloudIndex::load( const QString &fileName )
mOffset.set( mOffset.x(), mOffset.y(), offset );
mScale.set( mScale.x(), mScale.y(), scale );
}
// TODO: can parse also stats: "count", "minimum", "maximum", "mean", "stddev", "variance"

// store any metadata stats which are present for the attribute
AttributeStatistics stats;
if ( schemaObj.contains( "count" ) )
stats.count = schemaObj.value( QLatin1String( "count" ) ).toInt();
if ( schemaObj.contains( "minimum" ) )
stats.minimum = schemaObj.value( QLatin1String( "minimum" ) ).toDouble();
if ( schemaObj.contains( "maximum" ) )
stats.maximum = schemaObj.value( QLatin1String( "maximum" ) ).toDouble();
if ( schemaObj.contains( "count" ) )
stats.mean = schemaObj.value( QLatin1String( "mean" ) ).toDouble();
if ( schemaObj.contains( "stddev" ) )
stats.stDev = schemaObj.value( QLatin1String( "stddev" ) ).toDouble();
if ( schemaObj.contains( "variance" ) )
stats.variance = schemaObj.value( QLatin1String( "variance" ) ).toDouble();
mMetadataStats.insert( name, stats );
}
setAttributes( attributes );

Expand Down Expand Up @@ -216,6 +231,50 @@ QgsCoordinateReferenceSystem QgsEptPointCloudIndex::crs() const
return QgsCoordinateReferenceSystem::fromWkt( mWkt );
}

QVariant QgsEptPointCloudIndex::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const
{
if ( !mMetadataStats.contains( attribute ) )
return QVariant();

const AttributeStatistics &stats = mMetadataStats[ attribute ];
switch ( statistic )
{
case QgsStatisticalSummary::Count:
return stats.count >= 0 ? QVariant( stats.count ) : QVariant();

case QgsStatisticalSummary::Mean:
return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );

case QgsStatisticalSummary::StDev:
return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );

case QgsStatisticalSummary::Min:
return stats.minimum;

case QgsStatisticalSummary::Max:
return stats.maximum;

case QgsStatisticalSummary::Range:
return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();

case QgsStatisticalSummary::CountMissing:
case QgsStatisticalSummary::Sum:
case QgsStatisticalSummary::Median:
case QgsStatisticalSummary::StDevSample:
case QgsStatisticalSummary::Minority:
case QgsStatisticalSummary::Majority:
case QgsStatisticalSummary::Variety:
case QgsStatisticalSummary::FirstQuartile:
case QgsStatisticalSummary::ThirdQuartile:
case QgsStatisticalSummary::InterQuartileRange:
case QgsStatisticalSummary::First:
case QgsStatisticalSummary::Last:
case QgsStatisticalSummary::All:
return QVariant();
}
return QVariant();
}

bool QgsEptPointCloudIndex::loadHierarchy()
{
QQueue<QString> queue;
Expand Down
15 changes: 15 additions & 0 deletions src/core/providers/ept/qgseptpointcloudindex.h
Expand Up @@ -27,6 +27,7 @@

#include "qgspointcloudindex.h"
#include "qgspointcloudattribute.h"
#include "qgsstatisticalsummary.h"
#include "qgis_sip.h"

///@cond PRIVATE
Expand All @@ -47,12 +48,26 @@ class QgsEptPointCloudIndex: public QgsPointCloudIndex
QgsPointCloudBlock *nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request ) override;

QgsCoordinateReferenceSystem crs() const;
QVariant metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const;

private:
bool loadHierarchy();

QString mDataType;
QString mDirectory;
QString mWkt;

struct AttributeStatistics
{
int count = -1;
QVariant minimum;
QVariant maximum;
double mean = std::numeric_limits< double >::quiet_NaN();
double stDev = std::numeric_limits< double >::quiet_NaN();
double variance = std::numeric_limits< double >::quiet_NaN();
};

QMap< QString, AttributeStatistics > mMetadataStats;
};

///@endcond
Expand Down
5 changes: 5 additions & 0 deletions src/core/providers/ept/qgseptprovider.cpp
Expand Up @@ -78,6 +78,11 @@ QgsPointCloudIndex *QgsEptProvider::index() const
return mIndex.get();
}

QVariant QgsEptProvider::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const
{
return mIndex->metadataStatistic( attribute, statistic );
}

QgsEptProviderMetadata::QgsEptProviderMetadata():
QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION )
{
Expand Down
1 change: 1 addition & 0 deletions src/core/providers/ept/qgseptprovider.h
Expand Up @@ -52,6 +52,7 @@ class QgsEptProvider: public QgsPointCloudDataProvider
QString description() const override;

QgsPointCloudIndex *index() const override;
QVariant metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const override;

private:
std::unique_ptr<QgsEptPointCloudIndex> mIndex;
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -209,6 +209,7 @@ ADD_PYTHON_TEST(PyQgsPoint test_qgspoint.py)
ADD_PYTHON_TEST(PyQgsPointCloudAttributeByRampRenderer test_qgspointcloudattributebyramprenderer.py)
ADD_PYTHON_TEST(PyQgsPointCloudAttributeComboBox test_qgspointcloudattributecombobox.py)
ADD_PYTHON_TEST(PyQgsPointCloudAttributeModel test_qgspointcloudattributemodel.py)
ADD_PYTHON_TEST(PyQgsPointCloudDataProvider test_qgspointcloudprovider.py)
ADD_PYTHON_TEST(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.py)
ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)
ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py)
Expand Down
69 changes: 69 additions & 0 deletions tests/src/python/test_qgspointcloudprovider.py
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsPointCloudDataProvider
.. 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__ = '09/11/2020'
__copyright__ = 'Copyright 2020, The QGIS Project'

import qgis # NOQA

from qgis.core import (
QgsProviderRegistry,
QgsPointCloudLayer,
QgsStatisticalSummary
)

from qgis.PyQt.QtCore import QDir

from qgis.testing import start_app, unittest
from utilities import unitTestDataPath

start_app()


class TestQgsPointCloudDataProvider(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.report = "<h1>Python QgsPointCloudDataProvider Tests</h1>\n"

@classmethod
def tearDownClass(cls):
report_file_path = "%s/qgistest.html" % QDir.tempPath()
with open(report_file_path, 'a') as report_file:
report_file.write(cls.report)

@unittest.skipIf('ept' not in QgsProviderRegistry.instance().providerList(), 'EPT provider not available')
def testStatistics(self):
layer = QgsPointCloudLayer(unitTestDataPath() + '/point_clouds/ept/sunshine-coast/ept.json', 'test', 'ept')
self.assertTrue(layer.isValid())

self.assertEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Count), 253)
self.assertEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Min), 498062.0)
self.assertEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Max), 498067.39)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Range), 5.39000000001397, 5)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Mean), 498064.7342292491, 5)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.StDev),
1.5636647117681046, 5)
with self.assertRaises(ValueError):
layer.dataProvider().metadataStatistic('X', QgsStatisticalSummary.Majority)

with self.assertRaises(ValueError):
layer.dataProvider().metadataStatistic('Xxxxx', QgsStatisticalSummary.Count)

self.assertEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.Count), 253)
self.assertEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.Min), 199)
self.assertEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.Max), 2086.0)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.Range), 1887.0, 5)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.Mean), 728.521739130435, 5)
self.assertAlmostEqual(layer.dataProvider().metadataStatistic('Intensity', QgsStatisticalSummary.StDev),
440.9652417017358, 5)


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

0 comments on commit 3df0469

Please sign in to comment.