Skip to content
Permalink
Browse files
Merge pull request #46683 from elpaso/overlay_intersects_sort_by_inte…
…rsection_size

Overlay intersects sort by intersection size
  • Loading branch information
elpaso committed Jan 13, 2022
2 parents 1d9da9d + 8379995 commit 18979e579c31ddcb9cd2bec916f0607760d94173
Showing with 378 additions and 26 deletions.
  1. +18 −0 resources/function_help/json/overlay_intersects
  2. +175 −24 src/core/expression/qgsexpressionfunction.cpp
  3. +185 −2 tests/src/core/testqgsoverlayexpression.cpp
@@ -38,6 +38,16 @@
"arg": "min_inscribed_circle_radius",
"description": "defines an optional exclusion filter (for polygons only): minimum radius in current feature units for the maximum inscribed circle of the intersection (if the intersection results in multiple polygons the intersection will be returned if at least one of the polygons has a radius for the maximum inscribed circle greater or equal to the value).<br>Read more on the underlying GEOS predicate, as described in PostGIS <a href='https://postgis.net/docs/ST_MaximumInscribedCircle.html'>ST_MaximumInscribedCircle</a> function.<br>This argument requires GEOS >= 3.9.",
"optional": true
},
{
"arg": "return_details",
"description": "Set this to true to return a list of maps containing (key names in quotes) the feature 'id', the expression 'result' and the 'overlap' value. The 'radius' of the maximum inscribed circle is also returned when the target layer is a polygon. Only valid when used with the expression parameter",
"optional": true
},
{
"arg": "sort_by_intersection_size",
"description": "only valid when used with an expression, set this to 'des' to return the results ordered by the overlap value in descending order or set this to 'asc' for ascending order.",
"optional": true
}
],
"examples": [
@@ -72,6 +82,14 @@
{
"expression": "overlay_intersects(layer:='regions', min_inscribed_circle_radius:=0.54)",
"returns": "true if the current feature spatially intersects a region and the intersection area maximum inscribed circle's radius (of at least one of the parts in case of multipart) is greater or equal to the 0.54"
},
{
"expression": "overlay_intersects(layer:='regions', expression:= geom_to_wkt($geometry), return_details:=true)",
"returns": "an array of maps containing 'id', 'result', 'overlap' and 'radius'"
},
{
"expression": "overlay_intersects(layer:='regions', expression:= geom_to_wkt($geometry), sort_by_intersection_size:='des')",
"returns": "an array of geometries (in WKT) ordered by the overlap value in descending order"
}
]
}
@@ -6740,8 +6740,15 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress

// Sixth parameter (for intersects only) is the min overlap (area or length)
// Seventh parameter (for intersects only) is the min inscribed circle radius
// Eighth parameter (for intersects only) is the return_details
// Ninth parameter (for intersects only) is the sort_by_intersection_size flag
double minOverlap { -1 };
double minInscribedCircleRadius { -1 };
bool returnDetails = false; //#spellok
bool sortByMeasure = false;
bool sortAscending = false;
bool requireMeasures = false;
bool overlapOrRadiusFilter = false;
if ( isIntersectsFunc )
{

@@ -6762,6 +6769,16 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress
return QVariant();
}
#endif
node = QgsExpressionUtils::getNode( values.at( 7 ), parent );
// Return measures is only effective when an expression is set
returnDetails = !testOnly && node->eval( parent, context ).toBool(); //#spellok
node = QgsExpressionUtils::getNode( values.at( 8 ), parent );
// Sort by measures is only effective when an expression is set
const QString sorting { node->eval( parent, context ).toString().toLower() };
sortByMeasure = !testOnly && ( sorting.startsWith( "asc" ) || sorting.startsWith( "des" ) );
sortAscending = sorting.startsWith( "asc" );
requireMeasures = sortByMeasure || returnDetails; //#spellok
overlapOrRadiusFilter = minInscribedCircleRadius != -1 || minOverlap != -1;
}


@@ -6879,53 +6896,83 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress
QVariantList results;

QListIterator<QgsFeature> i( features );
while ( i.hasNext() && ( limit == -1 || foundCount < limit ) )
while ( i.hasNext() && ( sortByMeasure || limit == -1 || foundCount < limit ) )
{
QgsFeature feat2 = i.next();

if ( ! relationFunction || ( geometry.*relationFunction )( feat2.geometry() ) ) // Calls the method provided as template argument for the function (e.g. QgsGeometry::intersects)
{

if ( isIntersectsFunc && ( minOverlap != -1 || minInscribedCircleRadius != -1 ) )
double overlapValue = -1;
double radiusValue = -1;

if ( isIntersectsFunc && ( requireMeasures || overlapOrRadiusFilter ) )
{
const QgsGeometry intersection { geometry.intersection( feat2.geometry() ) };

// overlap and inscribed circle tests must be checked both (if the valuea are != -1)
// Depending on the intersection geometry type and on the geometry type of
// the tested geometry we can run different tests and collect different measures
// that can be used for sorting (if required).
switch ( intersection.type() )
{

case QgsWkbTypes::GeometryType::PolygonGeometry:
{

// overlap and inscribed circle tests must be checked both (if the values are != -1)
bool testResult { false };
for ( auto it = intersection.const_parts_begin(); ! testResult && it != intersection.const_parts_end(); ++it )
// For return measures:
QVector<double> overlapValues;
QVector<double> radiusValues;
for ( auto it = intersection.const_parts_begin(); ( ! testResult || requireMeasures ) && it != intersection.const_parts_end(); ++it )
{
const QgsCurvePolygon *geom = qgsgeometry_cast< const QgsCurvePolygon * >( *it );
// Check min overlap for intersection (if set)
if ( minOverlap != -1 )
if ( minOverlap != -1 || requireMeasures )
{
if ( geom->area() >= minOverlap )
overlapValue = geom->area();
overlapValues.append( geom->area() );
if ( minOverlap != - 1 )
{
testResult = true;
}
else
{
continue;
if ( overlapValue >= minOverlap )
{
testResult = true;
}
else
{
continue;
}
}
}

#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=9 )
// Check min inscribed circle radius for intersection (if set)
if ( minInscribedCircleRadius != -1 )
if ( minInscribedCircleRadius != -1 || requireMeasures )
{
const QgsRectangle bbox = geom->boundingBox();
const double width = bbox.width();
const double height = bbox.height();
const double size = width > height ? width : height;
const double tolerance = size / 100.0;
testResult = QgsGeos( geom ).maximumInscribedCircle( tolerance )->length() >= minInscribedCircleRadius;
radiusValue = QgsGeos( geom ).maximumInscribedCircle( tolerance )->length();
testResult = radiusValue >= minInscribedCircleRadius;
radiusValues.append( radiusValues );
}
#endif
}

// Get the max values
if ( !radiusValues.isEmpty() )
{
radiusValue = *std::max_element( radiusValues.cbegin(), radiusValues.cend() );
}

if ( ! testResult )
if ( ! overlapValues.isEmpty() )
{
overlapValue = *std::max_element( overlapValues.cbegin(), overlapValues.cend() );
}

if ( ! testResult && overlapOrRadiusFilter )
{
continue;
}
@@ -6935,30 +6982,83 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress
case QgsWkbTypes::GeometryType::LineGeometry:
{
bool testResult { false };
// For return measures:
QVector<double> overlapValues;
for ( auto it = intersection.const_parts_begin(); ! testResult && it != intersection.const_parts_end(); ++it )
{
const QgsCurve *geom = qgsgeometry_cast< const QgsCurve * >( *it );
// Check min overlap for intersection (if set)
if ( minOverlap != -1 )
if ( minOverlap != -1 || requireMeasures )
{
if ( geom->length() >= minOverlap )
{
testResult = true;
}
else
overlapValue = geom->length();
overlapValues.append( overlapValue );
if ( minOverlap != -1 )
{
continue;
if ( overlapValue >= minOverlap )
{
testResult = true;
}
else
{
continue;
}
}
}
}

if ( ! testResult )
if ( ! overlapValues.isEmpty() )
{
overlapValue = *std::max_element( overlapValues.cbegin(), overlapValues.cend() );
}

if ( ! testResult && overlapOrRadiusFilter )
{
continue;
}

break;
}
case QgsWkbTypes::GeometryType::PointGeometry:
{
if ( minOverlap != -1 || requireMeasures )
{
// Initially set this to 0 because it's a point intersection...
overlapValue = 0;
// ... but if the target geometry is not a point and the source
// geometry is a point, we must record the length or the area
// of the intersected geometry and use that as a measure for
// sorting or reporting.
if ( geometry.type() == QgsWkbTypes::GeometryType::PointGeometry )
{
switch ( feat2.geometry().type() )
{
case QgsWkbTypes::GeometryType::UnknownGeometry:
case QgsWkbTypes::GeometryType::NullGeometry:
case QgsWkbTypes::GeometryType::PointGeometry:
{
break;
}
case QgsWkbTypes::GeometryType::LineGeometry:
{
overlapValue = feat2.geometry().length();
break;
}
case QgsWkbTypes::GeometryType::PolygonGeometry:
{
overlapValue = feat2.geometry().area();
break;
}
}
}

if ( minOverlap != -1 && overlapValue < minOverlap )
{
continue;
}

}
break;
}
case QgsWkbTypes::GeometryType::NullGeometry:
case QgsWkbTypes::GeometryType::UnknownGeometry:
{
@@ -6978,7 +7078,26 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress
{
// We want a list of attributes / geometries / other expression values, evaluate now
subContext.setFeature( feat2 );
results.append( subExpression.evaluate( &subContext ) );
const QVariant expResult = subExpression.evaluate( &subContext );

if ( requireMeasures )
{
QVariantMap resultRecord;
resultRecord.insert( QStringLiteral( "id" ), feat2.id() );
resultRecord.insert( QStringLiteral( "result" ), expResult );
// Overlap is always added because return measures was set
resultRecord.insert( QStringLiteral( "overlap" ), overlapValue );
// Radius is only added when is different than -1 (because for linestrings is not set)
if ( radiusValue != -1 )
{
resultRecord.insert( QStringLiteral( "radius" ), radiusValue );
}
results.append( resultRecord );
}
else
{
results.append( expResult );
}
}
else
{
@@ -6996,7 +7115,37 @@ static QVariant executeGeomOverlay( const QVariantList &values, const QgsExpress
}

if ( !invert )
{
if ( requireMeasures )
{
if ( sortByMeasure )
{
std::sort( results.begin(), results.end(), [ sortAscending ]( const QVariant & recordA, const QVariant & recordB ) -> bool
{
return sortAscending ?
recordB.toMap().value( QStringLiteral( "overlap" ) ).toDouble() > recordA.toMap().value( QStringLiteral( "overlap" ) ).toDouble()
: recordA.toMap().value( QStringLiteral( "overlap" ) ).toDouble() > recordB.toMap().value( QStringLiteral( "overlap" ) ).toDouble();
} );
}
// Resize
if ( limit > 0 && results.size() > limit )
{
results.erase( results.begin() + limit );
}

if ( ! returnDetails ) //#spellok
{
QVariantList expResults;
for ( auto it = results.constBegin(); it != results.constEnd(); ++it )
{
expResults.append( it->toMap().value( QStringLiteral( "result" ) ) );
}
return expResults;
}
}

return results;
}

// for disjoint condition returns the results for cached layers not intersected feats
QVariantList disjoint_results;
@@ -7467,7 +7616,9 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
<< QgsExpressionFunction::Parameter( QStringLiteral( "limit" ), true, QVariant( -1 ), true )
<< QgsExpressionFunction::Parameter( QStringLiteral( "cache" ), true, QVariant( false ), false )
<< QgsExpressionFunction::Parameter( QStringLiteral( "min_overlap" ), true, QVariant( -1 ), false )
<< QgsExpressionFunction::Parameter( QStringLiteral( "min_inscribed_circle_radius" ), true, QVariant( -1 ), false ),
<< QgsExpressionFunction::Parameter( QStringLiteral( "min_inscribed_circle_radius" ), true, QVariant( -1 ), false )
<< QgsExpressionFunction::Parameter( QStringLiteral( "return_details" ), true, false, false )
<< QgsExpressionFunction::Parameter( QStringLiteral( "sort_by_intersection_size" ), true, QString(), false ),
i.value(), QStringLiteral( "GeometryGroup" ), QString(), true, QSet<QString>() << QgsFeatureRequest::ALL_ATTRIBUTES, true );

// The current feature is accessed for the geometry, so this should not be cached

0 comments on commit 18979e5

Please sign in to comment.