Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Use some heuristics to guess when a raster layer looks like a DEM
and auto-set the "represents elevation" flag for these

We do this when:

- the layer contains only one band
- the data type is int or float (not complex, rgb or byte)
- the layer doesn't have an attribute table
- there's something "dem-like" in the layer's name (ie 'elevation',
  'dem', 'height', 'srtm')

This should help make the elevation profile tool more user-friendly,
as these layers will be included in the profiles by default
  • Loading branch information
nyalldawson committed May 19, 2023
1 parent 928bbf9 commit 55226f5
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 2 deletions.
Expand Up @@ -10,6 +10,7 @@




class QgsRasterLayerElevationProperties : QgsMapLayerElevationProperties
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -118,6 +119,23 @@ Returns the symbology option used to render the raster profile in elevation prof
Sets the ``symbology`` option used to render the raster profile in elevation profile plots.

.. seealso:: :py:func:`setProfileSymbology`
%End

static bool layerLooksLikeDem( QgsRasterLayer *layer );
%Docstring
Returns ``True`` if a raster ``layer`` looks like a DEM.

This method applies some heuristics to ``layer`` to determine whether it looks like a candidate
for a DEM layer.

Specifically, it checks:

- the layer's name for DEM-like wording hints
- whether the layer contains a single band
- whether the layer contains an attribute table (if so, it's unlikely to be a DEM)
- the layer's data type

.. versionadded:: 3.32
%End

};
Expand Down
15 changes: 13 additions & 2 deletions src/app/layers/qgsapplayerhandling.cpp
Expand Up @@ -58,6 +58,7 @@
#include "qgsmaplayerutils.h"
#include "qgsfieldformatter.h"
#include "qgsabstractdatabaseproviderconnection.h"
#include "qgsrasterlayerelevationproperties.h"

#include <QObject>
#include <QMessageBox>
Expand All @@ -70,6 +71,7 @@ void QgsAppLayerHandling::postProcessAddedLayer( QgsMapLayer *layer )
{
case Qgis::LayerType::Raster:
{
QgsRasterLayer *rasterLayer = qobject_cast< QgsRasterLayer *>( layer );
bool ok = false;
layer->loadDefaultStyle( ok );
layer->loadDefaultMetadata( ok );
Expand All @@ -81,15 +83,24 @@ void QgsAppLayerHandling::postProcessAddedLayer( QgsMapLayer *layer )
&& QgsProject::instance()->elevationProperties()->terrainProvider()->offset() == 0
&& QgsProject::instance()->elevationProperties()->terrainProvider()->scale() == 1 ) )
{
if ( layer->elevationProperties()->hasElevation() )
if ( rasterLayer->elevationProperties()->hasElevation() )
{
std::unique_ptr< QgsRasterDemTerrainProvider > terrain = std::make_unique<QgsRasterDemTerrainProvider>();
terrain->setLayer( qobject_cast< QgsRasterLayer *>( layer ) );
terrain->setLayer( rasterLayer );
QgsProject::instance()->elevationProperties()->setTerrainProvider(
terrain.release()
);
}
}

// another bit of (hopefully!) user friendly logic -- while we aren't definitely sure that these layers ARE dems,
// we can take a good guess that they are...
// (but in this case we aren't sure, so don't apply the above logic which was only for layers we know are DEFINITELY dems)
if ( QgsRasterLayerElevationProperties::layerLooksLikeDem( rasterLayer ) )
{
qgis::down_cast< QgsRasterLayerElevationProperties * >( rasterLayer->elevationProperties() )->setEnabled( true );
}

break;
}

Expand Down
62 changes: 62 additions & 0 deletions src/core/raster/qgsrasterlayerelevationproperties.cpp
Expand Up @@ -170,6 +170,68 @@ void QgsRasterLayerElevationProperties::setProfileSymbology( Qgis::ProfileSurfac
mSymbology = symbology;
}

bool QgsRasterLayerElevationProperties::layerLooksLikeDem( QgsRasterLayer *layer )
{
// multiple bands => unlikely to be a DEM
if ( layer->bandCount() > 1 )
return false;

// raster attribute table => unlikely to be a DEM
if ( layer->attributeTable( 1 ) )
return false;

if ( QgsRasterDataProvider *dataProvider = layer->dataProvider() )
{
// filter out data types which aren't likely to be DEMs
switch ( dataProvider->dataType( 1 ) )
{
case Qgis::DataType::Byte:
case Qgis::DataType::UnknownDataType:
case Qgis::DataType::CInt16:
case Qgis::DataType::CInt32:
case Qgis::DataType::CFloat32:
case Qgis::DataType::CFloat64:
case Qgis::DataType::ARGB32:
case Qgis::DataType::ARGB32_Premultiplied:
return false;

case Qgis::DataType::Int8:
case Qgis::DataType::UInt16:
case Qgis::DataType::Int16:
case Qgis::DataType::UInt32:
case Qgis::DataType::Int32:
case Qgis::DataType::Float32:
case Qgis::DataType::Float64:
break;
}
}

// Check the layer's name for DEM-ish hints.
// See discussion at https://github.com/qgis/QGIS/pull/30245 - this list must NOT be translated,
// but adding hardcoded localized variants of the strings is encouraged.
static const QStringList sPartialCandidates{ QStringLiteral( "dem" ),
QStringLiteral( "height" ),
QStringLiteral( "elev" ),
QStringLiteral( "srtm" ) };
const QString layerName = layer->name();
for ( const QString &candidate : sPartialCandidates )
{
if ( layerName.contains( candidate, Qt::CaseInsensitive ) )
return true;
}

// these candidates must occur with word boundaries (we don't want to find "aster" in "raster"!)
static const QStringList sWordCandidates{ QStringLiteral( "aster" ) };
for ( const QString &candidate : sWordCandidates )
{
const QRegularExpression re( QStringLiteral( "\\b%1\\b" ).arg( candidate ) );
if ( re.match( layerName, Qt::CaseInsensitive ).hasMatch() )
return true;
}

return false;
}

void QgsRasterLayerElevationProperties::setDefaultProfileLineSymbol( const QColor &color )
{
std::unique_ptr< QgsSimpleLineSymbolLayer > profileLineLayer = std::make_unique< QgsSimpleLineSymbolLayer >( color, 0.6 );
Expand Down
19 changes: 19 additions & 0 deletions src/core/raster/qgsrasterlayerelevationproperties.h
Expand Up @@ -24,6 +24,8 @@
#include "qgsmaplayerelevationproperties.h"
#include "qgslinesymbol.h"

class QgsRasterLayer;

/**
* \class QgsRasterLayerElevationProperties
* \ingroup core
Expand Down Expand Up @@ -127,6 +129,23 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio
*/
void setProfileSymbology( Qgis::ProfileSurfaceSymbology symbology );

/**
* Returns TRUE if a raster \a layer looks like a DEM.
*
* This method applies some heuristics to \a layer to determine whether it looks like a candidate
* for a DEM layer.
*
* Specifically, it checks:
*
* - the layer's name for DEM-like wording hints
* - whether the layer contains a single band
* - whether the layer contains an attribute table (if so, it's unlikely to be a DEM)
* - the layer's data type
*
* \since QGIS 3.32
*/
static bool layerLooksLikeDem( QgsRasterLayer *layer );

private:

void setDefaultProfileLineSymbol( const QColor &color );
Expand Down
38 changes: 38 additions & 0 deletions tests/src/python/test_qgsrasterlayerelevationproperties.py
Expand Up @@ -10,16 +10,21 @@
__copyright__ = 'Copyright 2020, The QGIS Project'

import qgis # NOQA
import os

from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (
Qgis,
QgsFillSymbol,
QgsLineSymbol,
QgsRasterLayerElevationProperties,
QgsReadWriteContext,
QgsRasterLayer
)
from qgis.testing import start_app, unittest

from utilities import unitTestDataPath

start_app()


Expand Down Expand Up @@ -79,6 +84,39 @@ def testBasic(self):
self.assertEqual(props2.profileFillSymbol().color().name(), '#ff44ff')
self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow)

def test_looks_like_dem(self):
layer = QgsRasterLayer(
os.path.join(unitTestDataPath(), 'landsat.tif'), 'i am not a dem')
self.assertTrue(layer.isValid())

# not like a dem, the layer has multiple bands
self.assertFalse(
QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))

# layer data type doesn't look like a dem
layer = QgsRasterLayer(
os.path.join(unitTestDataPath(), 'raster/band1_byte_ct_epsg4326.tif'), 'i am not a dem')
self.assertTrue(layer.isValid())
self.assertFalse(
QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))

layer = QgsRasterLayer(
os.path.join(unitTestDataPath(), 'landsat-f32-b1.tif'), 'my layer')
self.assertTrue(layer.isValid())

# not like a dem, the layer name doesn't hint this to
self.assertFalse(QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))
layer.setName('i am a DEM')
self.assertTrue(
QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))
layer.setName('i am a raster')
self.assertFalse(
QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))

layer.setName('i am a aster satellite layer')
self.assertTrue(
QgsRasterLayerElevationProperties.layerLooksLikeDem(layer))


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

0 comments on commit 55226f5

Please sign in to comment.