From 694f375f2855d02a21d180490d38a7509e4572aa Mon Sep 17 00:00:00 2001
From: Jean Felder <jean.felder@oslandia.com>
Date: Fri, 25 Oct 2024 13:25:12 +0200
Subject: [PATCH 1/4] qgsvectorlayerelevationproperties: Add support for custom
 tolerance

This allows to define a custom tolerance. If this tolerance is
enabled, then this tolerance is used instead of the one defined in the
elevation profile widget.

This custom tolerance is enabled and set to 0 by default for Lines and
Polygons. Indeed, most of the time, only want to use the tolerance for
points.
---
 .../qgsvectorlayerelevationproperties.sip.in  | 46 +++++++++++++++++++
 .../qgsvectorlayerelevationproperties.sip.in  | 46 +++++++++++++++++++
 .../qgsvectorlayerelevationproperties.cpp     | 37 +++++++++++++++
 .../qgsvectorlayerelevationproperties.h       | 42 +++++++++++++++++
 .../python/test_qgslayoutelevationprofile.py  |  6 ++-
 .../test_qgsvectorlayerelevationproperties.py | 38 +++++++++++++++
 .../test_qgsvectorlayerprofilegenerator.py    | 37 +++++++++++----
 7 files changed, 243 insertions(+), 9 deletions(-)

diff --git a/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
index e07d53a2b29d..1c061aca8f61 100644
--- a/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
+++ b/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
@@ -147,6 +147,52 @@ Sets the feature extrusion height.
    the :py:func:`~QgsVectorLayerElevationProperties.zScale` factor is NOT applied to extrusion heights.
 
 .. seealso:: :py:func:`extrusionHeight`
+%End
+
+    bool customToleranceEnabled() const;
+%Docstring
+Returns ``True`` if custom tolerance is enabled.
+
+.. seealso:: :py:func:`setCustomToleranceEnabled`
+
+.. seealso:: :py:func:`customTolerance`
+%End
+
+    void setCustomToleranceEnabled( bool enabled );
+%Docstring
+Sets whether custom tolerance is ``enabled``.
+
+.. seealso:: :py:func:`customToleranceEnabled`
+
+.. seealso:: :py:func:`setCustomTolerance`
+%End
+
+    double customTolerance() const;
+%Docstring
+Returns the feature custom tolerance.
+
+.. warning::
+
+   custom tolerance is only applied if :py:func:`~QgsVectorLayerElevationProperties.customToleranceEnabled` is ``True``.
+
+If enabled, the profile generator will use this tolerance instead of the one
+defined in the elevation profile widget.
+
+.. seealso:: :py:func:`setCustomTolerance`
+%End
+
+    void setCustomTolerance( double tolerance );
+%Docstring
+Sets the feature custom tolerance.
+
+.. warning::
+
+   custom tolerance is only applied if :py:func:`~QgsVectorLayerElevationProperties.customToleranceEnabled` is ``True``.
+
+If enabled, the profile generator will use this tolerance instead of the one
+defined in the elevation profile widget.
+
+.. seealso:: :py:func:`customTolerance`
 %End
 
     bool respectLayerSymbology() const;
diff --git a/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in b/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
index e07d53a2b29d..1c061aca8f61 100644
--- a/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
+++ b/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in
@@ -147,6 +147,52 @@ Sets the feature extrusion height.
    the :py:func:`~QgsVectorLayerElevationProperties.zScale` factor is NOT applied to extrusion heights.
 
 .. seealso:: :py:func:`extrusionHeight`
+%End
+
+    bool customToleranceEnabled() const;
+%Docstring
+Returns ``True`` if custom tolerance is enabled.
+
+.. seealso:: :py:func:`setCustomToleranceEnabled`
+
+.. seealso:: :py:func:`customTolerance`
+%End
+
+    void setCustomToleranceEnabled( bool enabled );
+%Docstring
+Sets whether custom tolerance is ``enabled``.
+
+.. seealso:: :py:func:`customToleranceEnabled`
+
+.. seealso:: :py:func:`setCustomTolerance`
+%End
+
+    double customTolerance() const;
+%Docstring
+Returns the feature custom tolerance.
+
+.. warning::
+
+   custom tolerance is only applied if :py:func:`~QgsVectorLayerElevationProperties.customToleranceEnabled` is ``True``.
+
+If enabled, the profile generator will use this tolerance instead of the one
+defined in the elevation profile widget.
+
+.. seealso:: :py:func:`setCustomTolerance`
+%End
+
+    void setCustomTolerance( double tolerance );
+%Docstring
+Sets the feature custom tolerance.
+
+.. warning::
+
+   custom tolerance is only applied if :py:func:`~QgsVectorLayerElevationProperties.customToleranceEnabled` is ``True``.
+
+If enabled, the profile generator will use this tolerance instead of the one
+defined in the elevation profile widget.
+
+.. seealso:: :py:func:`customTolerance`
 %End
 
     bool respectLayerSymbology() const;
diff --git a/src/core/vector/qgsvectorlayerelevationproperties.cpp b/src/core/vector/qgsvectorlayerelevationproperties.cpp
index 5ec89d48a305..dfeb624367a4 100644
--- a/src/core/vector/qgsvectorlayerelevationproperties.cpp
+++ b/src/core/vector/qgsvectorlayerelevationproperties.cpp
@@ -54,6 +54,8 @@ QDomElement QgsVectorLayerElevationProperties::writeXml( QDomElement &parentElem
 
   element.setAttribute( QStringLiteral( "extrusionEnabled" ), mEnableExtrusion ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
   element.setAttribute( QStringLiteral( "extrusion" ), qgsDoubleToString( mExtrusionHeight ) );
+  element.setAttribute( QStringLiteral( "customToleranceEnabled" ), mEnableCustomTolerance ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
+  element.setAttribute( QStringLiteral( "customTolerance" ), qgsDoubleToString( mCustomTolerance ) );
   element.setAttribute( QStringLiteral( "clamping" ), qgsEnumValueToKey( mClamping ) );
   element.setAttribute( QStringLiteral( "binding" ), qgsEnumValueToKey( mBinding ) );
   element.setAttribute( QStringLiteral( "type" ), qgsEnumValueToKey( mType ) );
@@ -93,6 +95,8 @@ bool QgsVectorLayerElevationProperties::readXml( const QDomElement &element, con
   mType = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "type" ) ), Qgis::VectorProfileType::IndividualFeatures );
   mEnableExtrusion = elevationElement.attribute( QStringLiteral( "extrusionEnabled" ), QStringLiteral( "0" ) ).toInt();
   mExtrusionHeight = elevationElement.attribute( QStringLiteral( "extrusion" ), QStringLiteral( "0" ) ).toDouble();
+  mEnableCustomTolerance = elevationElement.attribute( QStringLiteral( "customToleranceEnabled" ), QStringLiteral( "0" ) ).toInt();
+  mCustomTolerance = elevationElement.attribute( QStringLiteral( "customTolerance" ), QStringLiteral( "0" ) ).toDouble();
   mSymbology = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "symbology" ) ), Qgis::ProfileSurfaceSymbology::Line );
   if ( elevationElement.hasAttribute( QStringLiteral( "elevationLimit" ) ) )
     mElevationLimit = elevationElement.attribute( QStringLiteral( "elevationLimit" ) ).toDouble();
@@ -135,6 +139,12 @@ void QgsVectorLayerElevationProperties::setDefaultsFromLayer( QgsMapLayer *layer
   mEnableExtrusion = false;
   mExtrusionHeight = 0;
 
+  // By default override default tolerance for Polygon and Line
+  // to avoid unexpected behaviors.
+  // For example, see: https://github.com/qgis/QGIS/issues/58016
+  mEnableCustomTolerance = vlayer->geometryType() != Qgis::GeometryType::Point;
+  mCustomTolerance = 0;
+
   mDataDefinedProperties.clear();
 
   mBinding = Qgis::AltitudeBinding::Centroid;
@@ -157,6 +167,8 @@ QgsVectorLayerElevationProperties *QgsVectorLayerElevationProperties::clone() co
   res->setType( mType );
   res->setExtrusionEnabled( mEnableExtrusion );
   res->setExtrusionHeight( mExtrusionHeight );
+  res->setCustomToleranceEnabled( mEnableCustomTolerance );
+  res->setCustomTolerance( mCustomTolerance );
   res->setProfileLineSymbol( mProfileLineSymbol->clone() );
   res->setProfileFillSymbol( mProfileFillSymbol->clone() );
   res->setProfileMarkerSymbol( mProfileMarkerSymbol->clone() );
@@ -228,6 +240,11 @@ QString QgsVectorLayerElevationProperties::htmlSummary() const
     }
   }
 
+  if ( mEnableCustomTolerance )
+  {
+    properties << tr( "CustomTolerance: %1" ).arg( mCustomTolerance );
+  }
+
   properties << tr( "Scale: %1" ).arg( mZScale );
 
   return QStringLiteral( "<li>%1</li>" ).arg( properties.join( QLatin1String( "</li><li>" ) ) );
@@ -305,6 +322,26 @@ void QgsVectorLayerElevationProperties::setExtrusionHeight( double height )
   emit profileGenerationPropertyChanged();
 }
 
+void QgsVectorLayerElevationProperties::setCustomTolerance( double tolerance )
+{
+  if ( mCustomTolerance == tolerance )
+    return;
+
+  mCustomTolerance = tolerance;
+  emit changed();
+  emit profileGenerationPropertyChanged();
+}
+
+void QgsVectorLayerElevationProperties::setCustomToleranceEnabled( bool enabled )
+{
+  if ( mEnableCustomTolerance == enabled )
+    return;
+
+  mEnableCustomTolerance = enabled;
+  emit changed();
+  emit profileGenerationPropertyChanged();
+}
+
 void QgsVectorLayerElevationProperties::setRespectLayerSymbology( bool enabled )
 {
   if ( mRespectLayerSymbology == enabled )
diff --git a/src/core/vector/qgsvectorlayerelevationproperties.h b/src/core/vector/qgsvectorlayerelevationproperties.h
index 7a021e1582ff..7349785449bb 100644
--- a/src/core/vector/qgsvectorlayerelevationproperties.h
+++ b/src/core/vector/qgsvectorlayerelevationproperties.h
@@ -142,6 +142,46 @@ class CORE_EXPORT QgsVectorLayerElevationProperties : public QgsMapLayerElevatio
      */
     void setExtrusionHeight( double height );
 
+    /**
+     * Returns TRUE if custom tolerance is enabled.
+     *
+     * \see setCustomToleranceEnabled()
+     * \see customTolerance()
+     */
+    bool customToleranceEnabled() const { return mEnableCustomTolerance; }
+
+    /**
+     * Sets whether custom tolerance is \a enabled.
+     *
+     * \see customToleranceEnabled()
+     * \see setCustomTolerance()
+     */
+    void setCustomToleranceEnabled( bool enabled );
+
+    /**
+     * Returns the feature custom tolerance.
+     *
+     * \warning custom tolerance is only applied if customToleranceEnabled() is TRUE.
+     *
+     * If enabled, the profile generator will use this tolerance instead of the one
+     * defined in the elevation profile widget.
+     *
+     * \see setCustomTolerance()
+     */
+    double customTolerance() const { return mCustomTolerance; }
+
+    /**
+     * Sets the feature custom tolerance.
+     *
+     * \warning custom tolerance is only applied if customToleranceEnabled() is TRUE.
+     *
+     * If enabled, the profile generator will use this tolerance instead of the one
+     * defined in the elevation profile widget.
+     *
+     * \see customTolerance()
+     */
+    void setCustomTolerance( double tolerance );
+
     /**
      * Returns TRUE if layer symbology should be respected when rendering elevation profile plots.
      *
@@ -313,6 +353,8 @@ class CORE_EXPORT QgsVectorLayerElevationProperties : public QgsMapLayerElevatio
 
     bool mEnableExtrusion = false;
     double mExtrusionHeight = 0;
+    bool mEnableCustomTolerance = false;
+    double mCustomTolerance = 0;
 
     std::unique_ptr< QgsLineSymbol > mProfileLineSymbol;
     std::unique_ptr< QgsFillSymbol > mProfileFillSymbol;
diff --git a/tests/src/python/test_qgslayoutelevationprofile.py b/tests/src/python/test_qgslayoutelevationprofile.py
index 8a709dcaa03a..8f5659a4340a 100644
--- a/tests/src/python/test_qgslayoutelevationprofile.py
+++ b/tests/src/python/test_qgslayoutelevationprofile.py
@@ -916,6 +916,10 @@ def test_draw_map_units_tolerance(self):
             f.setGeometry(QgsGeometry.fromWkt(line))
             self.assertTrue(vl.dataProvider().addFeature(f))
 
+        tolerance = 1
+
+        vl.elevationProperties().setCustomToleranceEnabled(True)
+        vl.elevationProperties().setCustomTolerance(tolerance)
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
         line_symbol = QgsLineSymbol.createSimple({"color": "#ff00ff", "width": "0.8"})
         line_symbol.setWidthUnit(Qgis.RenderUnit.MapUnits)
@@ -977,7 +981,7 @@ def test_draw_map_units_tolerance(self):
             )
         )
 
-        profile_item.setTolerance(1)
+        profile_item.setTolerance(tolerance)
         profile_item.setLayers([vl])
 
         self.assertTrue(
diff --git a/tests/src/python/test_qgsvectorlayerelevationproperties.py b/tests/src/python/test_qgsvectorlayerelevationproperties.py
index b57614823db4..7b3b0bfbdced 100644
--- a/tests/src/python/test_qgsvectorlayerelevationproperties.py
+++ b/tests/src/python/test_qgsvectorlayerelevationproperties.py
@@ -46,6 +46,8 @@ def testBasic(self):
         self.assertEqual(props.type(), Qgis.VectorProfileType.IndividualFeatures)
         self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line)
         self.assertFalse(props.showMarkerSymbolInSurfacePlots())
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
 
         props.setZOffset(0.5)
         props.setZScale(2)
@@ -135,6 +137,8 @@ def testBasic(self):
         )
         self.assertTrue(props2.showMarkerSymbolInSurfacePlots())
         self.assertEqual(props2.elevationLimit(), 909)
+        self.assertFalse(props2.customToleranceEnabled())
+        self.assertEqual(props2.customTolerance(), 0)
 
         self.assertEqual(props2.profileLineSymbol().color().name(), "#ff4433")
         self.assertEqual(props2.profileFillSymbol().color().name(), "#ff4455")
@@ -161,6 +165,8 @@ def testBasic(self):
         )
         self.assertTrue(props_clone.showMarkerSymbolInSurfacePlots())
         self.assertEqual(props2.elevationLimit(), 909)
+        self.assertFalse(props_clone.customToleranceEnabled())
+        self.assertEqual(props_clone.customTolerance(), 0)
 
         self.assertEqual(props_clone.profileLineSymbol().color().name(), "#ff4433")
         self.assertEqual(props_clone.profileFillSymbol().color().name(), "#ff4455")
@@ -210,6 +216,38 @@ def test_show_by_default(self):
         props.setClamping(Qgis.AltitudeClamping.Absolute)
         self.assertTrue(props.showByDefaultInElevationProfilePlots())
 
+    def test_defaults_from_layer(self):
+        props = QgsVectorLayerElevationProperties(None)
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+
+        point_layer = QgsVectorLayer("Point", "my layer point", "memory")
+        self.assertTrue(point_layer.isValid())
+        props = QgsVectorLayerElevationProperties(None)
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+        props.setDefaultsFromLayer(point_layer)
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+
+        line_layer = QgsVectorLayer("LineStringZ", "my layer point", "memory")
+        self.assertTrue(line_layer.isValid())
+        props = QgsVectorLayerElevationProperties(None)
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+        props.setDefaultsFromLayer(line_layer)
+        self.assertTrue(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+
+        polygon_layer = QgsVectorLayer("Polygon", "Polys", "memory")
+        self.assertTrue(polygon_layer.isValid())
+        props = QgsVectorLayerElevationProperties(None)
+        self.assertFalse(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+        props.setDefaultsFromLayer(polygon_layer)
+        self.assertTrue(props.customToleranceEnabled())
+        self.assertEqual(props.customTolerance(), 0)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/src/python/test_qgsvectorlayerprofilegenerator.py b/tests/src/python/test_qgsvectorlayerprofilegenerator.py
index a9905d151f56..3adc95573d23 100644
--- a/tests/src/python/test_qgsvectorlayerprofilegenerator.py
+++ b/tests/src/python/test_qgsvectorlayerprofilegenerator.py
@@ -690,7 +690,9 @@ def testLineGenerationTerrainTolerance(self):
         req.setCrs(QgsCoordinateReferenceSystem("EPSG:3857"))
 
         # very small tolerance
-        req.setTolerance(0.1)
+        tolerance = 0.1
+        vl.elevationProperties().setCustomTolerance(tolerance)
+        req.setTolerance(tolerance)
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
         results = generator.takeResults()
@@ -738,7 +740,9 @@ def testLineGenerationTerrainTolerance(self):
             self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2)
 
         # 1 meter tolerance
-        req.setTolerance(1)
+        tolerance = 1
+        vl.elevationProperties().setCustomTolerance(tolerance)
+        req.setTolerance(tolerance)
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
         results = generator.takeResults()
@@ -786,7 +790,9 @@ def testLineGenerationTerrainTolerance(self):
             self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2)
 
         # 15 meters tolerance
-        req.setTolerance(15)
+        tolerance = 15
+        vl.elevationProperties().setCustomTolerance(tolerance)
+        req.setTolerance(tolerance)
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
         results = generator.takeResults()
@@ -1390,11 +1396,14 @@ def testPolygonGenerationRelativeExtrusionTolerance(self):
             f.setGeometry(QgsGeometry.fromWkt(line))
             self.assertTrue(vl.dataProvider().addFeature(f))
 
+        tolerance = 2.0
+
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Relative)
         vl.elevationProperties().setZScale(2.5)
         vl.elevationProperties().setZOffset(10)
         vl.elevationProperties().setExtrusionEnabled(True)
         vl.elevationProperties().setExtrusionHeight(7)
+        vl.elevationProperties().setCustomTolerance(tolerance)
 
         curve = QgsLineString()
         curve.fromWkt(
@@ -1412,7 +1421,7 @@ def testPolygonGenerationRelativeExtrusionTolerance(self):
         req.setTerrainProvider(terrain_provider)
 
         req.setCrs(QgsCoordinateReferenceSystem("EPSG:3857"))
-        req.setTolerance(2.0)
+        req.setTolerance(tolerance)
 
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
@@ -2783,6 +2792,7 @@ def testRenderProfileAsSurfaceFillAboveLimitTolerance(self):
             f.setGeometry(QgsGeometry.fromWkt(line))
             self.assertTrue(vl.dataProvider().addFeature(f))
 
+        tolerance = 20
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
         vl.elevationProperties().setType(Qgis.VectorProfileType.ContinuousSurface)
         vl.elevationProperties().setProfileSymbology(
@@ -2796,6 +2806,7 @@ def testRenderProfileAsSurfaceFillAboveLimitTolerance(self):
         vl.elevationProperties().setProfileFillSymbol(fill_symbol)
         line_symbol = QgsLineSymbol.createSimple({"color": "#ff00ff", "width": "0.8"})
         vl.elevationProperties().setProfileLineSymbol(line_symbol)
+        vl.elevationProperties().setCustomTolerance(tolerance)
 
         curve = QgsLineString()
         curve.fromWkt(
@@ -2805,7 +2816,7 @@ def testRenderProfileAsSurfaceFillAboveLimitTolerance(self):
         req.setTransformContext(self.create_transform_context())
 
         req.setCrs(QgsCoordinateReferenceSystem())
-        req.setTolerance(20)
+        req.setTolerance(tolerance)
 
         plot_renderer = QgsProfilePlotRenderer([vl], req)
         plot_renderer.startGeneration()
@@ -2880,11 +2891,13 @@ def testRenderProfileSymbolWithMapUnitsTolerance(self):
             f.setGeometry(QgsGeometry.fromWkt(line))
             self.assertTrue(vl.dataProvider().addFeature(f))
 
+        tolerance = 10
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
         vl.elevationProperties().setRespectLayerSymbology(False)
         line_symbol = QgsLineSymbol.createSimple({"color": "#ff00ff", "width": "0.8"})
         line_symbol.setWidthUnit(Qgis.RenderUnit.MapUnits)
         vl.elevationProperties().setProfileLineSymbol(line_symbol)
+        vl.elevationProperties().setCustomTolerance(tolerance)
 
         curve = QgsLineString()
         curve.fromWkt(
@@ -2894,7 +2907,7 @@ def testRenderProfileSymbolWithMapUnitsTolerance(self):
         req.setTransformContext(self.create_transform_context())
 
         req.setCrs(QgsCoordinateReferenceSystem())
-        req.setTolerance(10)
+        req.setTolerance(tolerance)
 
         plot_renderer = QgsProfilePlotRenderer([vl], req)
         plot_renderer.startGeneration()
@@ -2997,6 +3010,8 @@ def doCheckPoint(
         expectedFeatures,
     ):
         request.setTolerance(tolerance)
+        if tolerance > 0 and layer.geometryType() != Qgis.GeometryType.Point:
+            layer.elevationProperties().setCustomTolerance(tolerance)
 
         profGen = layer.createProfileGenerator(request)
         self.assertIsNotNone(profGen)
@@ -3295,6 +3310,9 @@ def testPolyhedralSurfaceGenerationFeature(self):
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
         vl.elevationProperties().setExtrusionEnabled(False)
 
+        tolerance = 10
+        vl.elevationProperties().setCustomTolerance(tolerance)
+
         wkt_str = "POLYHEDRALSURFACE Z(((321474.91 129812.38 -20.00,322277.09 130348.29 -20.00,322631.00 129738.23 -20.00,321434.46 129266.36 -20.00,321474.91 129812.38 -20.00)),((321474.91 129812.38 30.00,321434.46 129266.36 30.00,322631.00 129738.23 30.00,322277.09 130348.29 30.00,321474.91 129812.38 30.00)),((321474.91 129812.38 -20.00,321474.91 129812.38 30.00,322277.09 130348.29 30.00,322277.09 130348.29 -20.00,321474.91 129812.38 -20.00)),((322277.09 130348.29 -20.00,322277.09 130348.29 30.00,322631.00 129738.23 30.00,322631.00 129738.23 -20.00,322277.09 130348.29 -20.00)),((322631.00 129738.23 -20.00,322631.00 129738.23 30.00,321434.46 129266.36 30.00,321434.46 129266.36 -20.00,322631.00 129738.23 -20.00)),((321434.46 129266.36 -20.00,321434.46 129266.36 30.00,321474.91 129812.38 30.00,321474.91 129812.38 -20.00,321434.46 129266.36 -20.00)))"
         vl_feature = QgsFeature()
         vl_feature.setGeometry(QgsGeometry.fromWkt(wkt_str))
@@ -3308,7 +3326,7 @@ def testPolyhedralSurfaceGenerationFeature(self):
         req = QgsProfileRequest(curve)
 
         req.setCrs(QgsCoordinateReferenceSystem("EPSG:3857"))
-        req.setTolerance(10)
+        req.setTolerance(tolerance)
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
 
@@ -3351,6 +3369,9 @@ def testVerticalLineGenerationFeatureTolerance(self):
         vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
         vl.elevationProperties().setExtrusionEnabled(False)
 
+        tolerance = 10
+        vl.elevationProperties().setCustomTolerance(tolerance)
+
         vl_feature = QgsFeature()
         vl_feature.setGeometry(
             QgsGeometry.fromWkt(
@@ -3367,7 +3388,7 @@ def testVerticalLineGenerationFeatureTolerance(self):
         req = QgsProfileRequest(curve)
 
         req.setCrs(QgsCoordinateReferenceSystem("EPSG:3857"))
-        req.setTolerance(10)
+        req.setTolerance(tolerance)
         generator = vl.createProfileGenerator(req)
         self.assertTrue(generator.generateProfile())
 

From 8c3a1ab3cf37c7dae1cfa51ef1998595b927a4ce Mon Sep 17 00:00:00 2001
From: Jean Felder <jean.felder@oslandia.com>
Date: Fri, 25 Oct 2024 14:34:00 +0200
Subject: [PATCH 2/4] qgsvectorlayerprofilegenerator: const some methods

---
 src/core/vector/qgsvectorlayerprofilegenerator.cpp | 12 ++++++------
 src/core/vector/qgsvectorlayerprofilegenerator.h   | 12 ++++++------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.cpp b/src/core/vector/qgsvectorlayerprofilegenerator.cpp
index 8c7772ed5990..d7971b0205ce 100644
--- a/src/core/vector/qgsvectorlayerprofilegenerator.cpp
+++ b/src/core/vector/qgsvectorlayerprofilegenerator.cpp
@@ -181,7 +181,7 @@ QgsProfileSnapResult QgsVectorLayerProfileResults::snapPointToIndividualFeatures
   return res;
 }
 
-void QgsVectorLayerProfileResults::visitFeaturesAtPoint( const QgsProfilePoint &point, double maximumPointDistanceDelta, double maximumPointElevationDelta, double maximumSurfaceElevationDelta,  const std::function< void( QgsFeatureId, double delta, double distance, double elevation ) > &visitor, bool visitWithin )
+void QgsVectorLayerProfileResults::visitFeaturesAtPoint( const QgsProfilePoint &point, double maximumPointDistanceDelta, double maximumPointElevationDelta, double maximumSurfaceElevationDelta,  const std::function< void( QgsFeatureId, double delta, double distance, double elevation ) > &visitor, bool visitWithin ) const
 {
   // TODO -- add spatial index if performance is an issue
 
@@ -336,7 +336,7 @@ void QgsVectorLayerProfileResults::visitFeaturesAtPoint( const QgsProfilePoint &
   }
 }
 
-void QgsVectorLayerProfileResults::visitFeaturesInRange( const QgsDoubleRange &distanceRange, const QgsDoubleRange &elevationRange, const std::function<void ( QgsFeatureId )> &visitor )
+void QgsVectorLayerProfileResults::visitFeaturesInRange( const QgsDoubleRange &distanceRange, const QgsDoubleRange &elevationRange, const std::function<void ( QgsFeatureId )> &visitor ) const
 {
   // TODO -- add spatial index if performance is an issue
   const QgsRectangle profileRange( distanceRange.lower(), elevationRange.lower(), distanceRange.upper(), elevationRange.upper() );
@@ -1621,7 +1621,7 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons()
   return true;
 }
 
-double QgsVectorLayerProfileGenerator::terrainHeight( double x, double y )
+double QgsVectorLayerProfileGenerator::terrainHeight( double x, double y ) const
 {
   if ( !mTerrainProvider )
     return std::numeric_limits<double>::quiet_NaN();
@@ -1640,7 +1640,7 @@ double QgsVectorLayerProfileGenerator::terrainHeight( double x, double y )
   return mTerrainProvider->heightAt( x, y );
 }
 
-double QgsVectorLayerProfileGenerator::featureZToHeight( double x, double y, double z, double offset )
+double QgsVectorLayerProfileGenerator::featureZToHeight( double x, double y, double z, double offset ) const
 {
   switch ( mClamping )
   {
@@ -1677,7 +1677,7 @@ double QgsVectorLayerProfileGenerator::featureZToHeight( double x, double y, dou
   return ( std::isnan( z ) ? 0 : z ) * mScale + offset;
 }
 
-void QgsVectorLayerProfileGenerator::clampAltitudes( QgsLineString *lineString, const QgsPoint &centroid, double offset )
+void QgsVectorLayerProfileGenerator::clampAltitudes( QgsLineString *lineString, const QgsPoint &centroid, double offset ) const
 {
   for ( int i = 0; i < lineString->nCoordinates(); ++i )
   {
@@ -1729,7 +1729,7 @@ void QgsVectorLayerProfileGenerator::clampAltitudes( QgsLineString *lineString,
   }
 }
 
-bool QgsVectorLayerProfileGenerator::clampAltitudes( QgsPolygon *polygon, double offset )
+bool QgsVectorLayerProfileGenerator::clampAltitudes( QgsPolygon *polygon, double offset ) const
 {
   if ( !polygon->is3D() )
     polygon->addZValue( 0 );
diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.h b/src/core/vector/qgsvectorlayerprofilegenerator.h
index 8bc647801107..021f210e154e 100644
--- a/src/core/vector/qgsvectorlayerprofilegenerator.h
+++ b/src/core/vector/qgsvectorlayerprofilegenerator.h
@@ -88,9 +88,9 @@ class CORE_EXPORT QgsVectorLayerProfileResults : public QgsAbstractProfileSurfac
     QgsProfileSnapResult snapPointToIndividualFeatures( const QgsProfilePoint &point, const QgsProfileSnapContext &context );
 
     void visitFeaturesAtPoint( const QgsProfilePoint &point, double maximumPointDistanceDelta, double maximumPointElevationDelta, double maximumSurfaceElevationDelta,
-                               const std::function< void( QgsFeatureId, double delta, double distance, double elevation ) > &visitor, bool visitWithin );
+                               const std::function< void( QgsFeatureId, double delta, double distance, double elevation ) > &visitor, bool visitWithin ) const;
     void visitFeaturesInRange( const QgsDoubleRange &distanceRange, const QgsDoubleRange &elevationRange,
-                               const std::function<void ( QgsFeatureId )> &visitor );
+                               const std::function<void ( QgsFeatureId )> &visitor ) const;
 };
 
 
@@ -136,11 +136,11 @@ class CORE_EXPORT QgsVectorLayerProfileGenerator : public QgsAbstractProfileSurf
     void processTriangleIntersectForLine( const QgsPolygon *triangle, const QgsLineString *intersect, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts );
     void processTriangleIntersectForPolygon( const QgsPolygon *triangle, const QgsPolygon *intersectionPolygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts );
 
-    double terrainHeight( double x, double y );
-    double featureZToHeight( double x, double y, double z, double offset );
+    double terrainHeight( double x, double y ) const;
+    double featureZToHeight( double x, double y, double z, double offset ) const;
 
-    void clampAltitudes( QgsLineString *lineString, const QgsPoint &centroid, double offset );
-    bool clampAltitudes( QgsPolygon *polygon, double offset );
+    void clampAltitudes( QgsLineString *lineString, const QgsPoint &centroid, double offset ) const;
+    bool clampAltitudes( QgsPolygon *polygon, double offset ) const;
 
     QString mId;
     std::unique_ptr<QgsFeedback> mFeedback = nullptr;

From 670e2883696407887b9624afb678aea0bd7eb61d Mon Sep 17 00:00:00 2001
From: Jean Felder <jean.felder@oslandia.com>
Date: Fri, 25 Oct 2024 13:34:28 +0200
Subject: [PATCH 3/4] qgsvectorlayerprofilegenerator: Add support for custom
 tolerance

---
 .../vector/qgsvectorlayerprofilegenerator.cpp | 23 ++++++++++++-------
 .../vector/qgsvectorlayerprofilegenerator.h   |  4 ++++
 2 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.cpp b/src/core/vector/qgsvectorlayerprofilegenerator.cpp
index d7971b0205ce..2c7aac503a5f 100644
--- a/src/core/vector/qgsvectorlayerprofilegenerator.cpp
+++ b/src/core/vector/qgsvectorlayerprofilegenerator.cpp
@@ -687,6 +687,8 @@ QgsVectorLayerProfileGenerator::QgsVectorLayerProfileGenerator( QgsVectorLayer *
   , mBinding( qgis::down_cast< QgsVectorLayerElevationProperties * >( layer->elevationProperties() )->binding() )
   , mExtrusionEnabled( qgis::down_cast< QgsVectorLayerElevationProperties * >( layer->elevationProperties() )->extrusionEnabled() )
   , mExtrusionHeight( qgis::down_cast< QgsVectorLayerElevationProperties * >( layer->elevationProperties() )->extrusionHeight() )
+  , mCustomToleranceEnabled( qgis::down_cast< QgsVectorLayerElevationProperties * >( layer->elevationProperties() )->customToleranceEnabled() )
+  , mCustomTolerance( qgis::down_cast< QgsVectorLayerElevationProperties * >( layer->elevationProperties() )->customTolerance() )
   , mExpressionContext( request.expressionContext() )
   , mFields( layer->fields() )
   , mDataDefinedProperties( layer->elevationProperties()->dataDefinedProperties() )
@@ -818,13 +820,13 @@ bool QgsVectorLayerProfileGenerator::generateProfileInner( const QgsProfileGener
   mProfileCurveEngine.reset( new QgsGeos( mProfileCurve.get() ) );
   mProfileCurveEngine->prepareGeometry();
 
-  if ( mTolerance == 0.0 ) // geos does not handle very well buffer with 0 size
+  if ( tolerance() == 0.0 ) // geos does not handle very well buffer with 0 size
   {
     mProfileBufferedCurve = std::unique_ptr<QgsAbstractGeometry>( mProfileCurve->clone() );
   }
   else
   {
-    mProfileBufferedCurve = std::unique_ptr<QgsAbstractGeometry>( mProfileCurveEngine->buffer( mTolerance, 8, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Round, 2 ) );
+    mProfileBufferedCurve = std::unique_ptr<QgsAbstractGeometry>( mProfileCurveEngine->buffer( tolerance(), 8, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Round, 2 ) );
   }
 
   mProfileBufferedCurveEngine.reset( new QgsGeos( mProfileBufferedCurve.get() ) );
@@ -875,7 +877,7 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPoints()
   // get features from layer
   QgsFeatureRequest request;
   request.setCoordinateTransform( QgsCoordinateTransform( mSourceCrs, mTargetCrs, mTransformContext ) );
-  request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), mTolerance );
+  request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), tolerance() );
   request.setSubsetOfAttributes( mDataDefinedProperties.referencedFields( mExpressionContext ), mFields );
   request.setFeedback( mFeedback.get() );
 
@@ -1038,9 +1040,9 @@ bool QgsVectorLayerProfileGenerator::generateProfileForLines()
   // get features from layer
   QgsFeatureRequest request;
   request.setDestinationCrs( mTargetCrs, mTransformContext );
-  if ( mTolerance > 0 )
+  if ( tolerance() > 0 )
   {
-    request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), mTolerance );
+    request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), tolerance() );
   }
   else
   {
@@ -1330,9 +1332,9 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons()
   // get features from layer
   QgsFeatureRequest request;
   request.setDestinationCrs( mTargetCrs, mTransformContext );
-  if ( mTolerance > 0 )
+  if ( tolerance() > 0 )
   {
-    request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), mTolerance );
+    request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), tolerance() );
   }
   else
   {
@@ -1403,7 +1405,7 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons()
     if ( mFeedback->isCanceled() )
       return;
 
-    if ( mTolerance > 0.0 ) // if the tolerance is not 0.0 we will have a polygon / polygon intersection, we do not need tessellation
+    if ( tolerance() > 0.0 ) // if the tolerance is not 0.0 we will have a polygon / polygon intersection, we do not need tessellation
     {
       QString error;
       if ( mProfileBufferedCurveEngine->intersects( clampedPolygon.get(), &error ) )
@@ -1621,6 +1623,11 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons()
   return true;
 }
 
+double QgsVectorLayerProfileGenerator::tolerance() const
+{
+  return mCustomToleranceEnabled ? mCustomTolerance : mTolerance;
+}
+
 double QgsVectorLayerProfileGenerator::terrainHeight( double x, double y ) const
 {
   if ( !mTerrainProvider )
diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.h b/src/core/vector/qgsvectorlayerprofilegenerator.h
index 021f210e154e..e971038937f5 100644
--- a/src/core/vector/qgsvectorlayerprofilegenerator.h
+++ b/src/core/vector/qgsvectorlayerprofilegenerator.h
@@ -136,6 +136,8 @@ class CORE_EXPORT QgsVectorLayerProfileGenerator : public QgsAbstractProfileSurf
     void processTriangleIntersectForLine( const QgsPolygon *triangle, const QgsLineString *intersect, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts );
     void processTriangleIntersectForPolygon( const QgsPolygon *triangle, const QgsPolygon *intersectionPolygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts );
 
+    double tolerance() const;
+
     double terrainHeight( double x, double y ) const;
     double featureZToHeight( double x, double y, double z, double offset ) const;
 
@@ -171,6 +173,8 @@ class CORE_EXPORT QgsVectorLayerProfileGenerator : public QgsAbstractProfileSurf
     Qgis::AltitudeBinding mBinding = Qgis::AltitudeBinding::Centroid;
     bool mExtrusionEnabled = false;
     double mExtrusionHeight = 0;
+    bool mCustomToleranceEnabled = false;
+    double mCustomTolerance = 0;
 
     QgsExpressionContext mExpressionContext;
     QgsFields mFields;

From d9b9b442fce3dedfaf94cda2d7e9c1402acb6ec3 Mon Sep 17 00:00:00 2001
From: Jean Felder <jean.felder@oslandia.com>
Date: Fri, 25 Oct 2024 14:23:20 +0200
Subject: [PATCH 4/4] qgsvectorelevationpropertieswidget: Add support for
 custom tolerance

---
 .../qgsvectorelevationpropertieswidget.cpp    |  6 +++
 .../qgsvectorelevationpropertieswidgetbase.ui | 51 +++++++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/src/app/vector/qgsvectorelevationpropertieswidget.cpp b/src/app/vector/qgsvectorelevationpropertieswidget.cpp
index e784ed745c95..8688bacba42c 100644
--- a/src/app/vector/qgsvectorelevationpropertieswidget.cpp
+++ b/src/app/vector/qgsvectorelevationpropertieswidget.cpp
@@ -45,6 +45,7 @@ QgsVectorElevationPropertiesWidget::QgsVectorElevationPropertiesWidget( QgsVecto
   mOffsetZSpinBox->setClearValue( 0 );
   mScaleZSpinBox->setClearValue( 1 );
   mExtrusionSpinBox->setClearValue( 0 );
+  mToleranceSpinBox->setClearValue( 0 );
 
   mLineStyleButton->setSymbolType( Qgis::SymbolType::Line );
   mFillStyleButton->setSymbolType( Qgis::SymbolType::Fill );
@@ -78,6 +79,7 @@ QgsVectorElevationPropertiesWidget::QgsVectorElevationPropertiesWidget( QgsVecto
   connect( mElevationLimitSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsVectorElevationPropertiesWidget::onChanged );
   connect( mExtrusionSpinBox, qOverload<double>( &QDoubleSpinBox::valueChanged ), this, &QgsVectorElevationPropertiesWidget::onChanged );
   connect( mExtrusionGroupBox, &QGroupBox::toggled, this, &QgsVectorElevationPropertiesWidget::onChanged );
+  connect( mToleranceSpinBox, qOverload<double >( &QDoubleSpinBox::valueChanged ), this, &QgsVectorElevationPropertiesWidget::onChanged );
   connect( mComboClamping, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsVectorElevationPropertiesWidget::onChanged );
   connect( mComboBinding, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsVectorElevationPropertiesWidget::onChanged );
   connect( mComboClamping, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsVectorElevationPropertiesWidget::clampingChanged );
@@ -155,6 +157,8 @@ void QgsVectorElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer )
     mElevationLimitSpinBox->setValue( props->elevationLimit() );
   mExtrusionGroupBox->setChecked( props->extrusionEnabled() );
   mExtrusionSpinBox->setValue( props->extrusionHeight() );
+  mToleranceGroupBox->setChecked( props->customToleranceEnabled() );
+  mToleranceSpinBox->setValue( props->customTolerance() );
   mTypeComboBox->setCurrentIndex( mTypeComboBox->findData( static_cast<int>( props->type() ) ) );
   switch ( props->type() )
   {
@@ -234,6 +238,8 @@ void QgsVectorElevationPropertiesWidget::apply()
   props->setBinding( static_cast<Qgis::AltitudeBinding>( mComboBinding->currentData().toInt() ) );
   props->setExtrusionEnabled( mExtrusionGroupBox->isChecked() );
   props->setExtrusionHeight( mExtrusionSpinBox->value() );
+  props->setCustomToleranceEnabled( mToleranceGroupBox->isChecked() );
+  props->setCustomTolerance( mToleranceSpinBox->value() );
   if ( mElevationLimitSpinBox->value() != mElevationLimitSpinBox->clearValue() )
     props->setElevationLimit( mElevationLimitSpinBox->value() );
   else
diff --git a/src/ui/qgsvectorelevationpropertieswidgetbase.ui b/src/ui/qgsvectorelevationpropertieswidgetbase.ui
index 0e13169f6905..279a3fa3ad18 100644
--- a/src/ui/qgsvectorelevationpropertieswidgetbase.ui
+++ b/src/ui/qgsvectorelevationpropertieswidgetbase.ui
@@ -279,6 +279,57 @@
      </layout>
     </widget>
    </item>
+   <item>
+    <widget class="QGroupBox" name="mToleranceGroupBox">
+     <property name="toolTip">
+      <string>If checked, the layer will use this tolerance instead of the one defined in the Elevation Profile widget.</string>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="title">
+      <string>Custom Tolerance</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
+     </property>
+     <property name="checked">
+      <bool>false</bool>
+     </property>
+     <property name="syncGroup" stdset="0">
+      <string notr="true">vectorgeneral</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout_Tolerance">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_Tolerance">
+        <property name="text">
+         <string>Tolerance</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QgsDoubleSpinBox" name="mToleranceSpinBox">
+        <property name="decimals">
+         <number>6</number>
+        </property>
+        <property name="minimum">
+         <double>0.000000000000000</double>
+        </property>
+        <property name="maximum">
+         <double>99999999999.000000000000000</double>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="Line" name="line_Tolerance">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
    <item>
     <widget class="QGroupBox" name="groupBox">
      <property name="title">