mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-22 00:06:12 -05:00
Fix artifacts when rendering filled line symbol
Use geos to calculate the buffered line instead of Qt's path stroker, as the later results in artifacts when line segments pass close to other segments Fixes #59689
This commit is contained in:
parent
a48ec1012f
commit
22d828f2e0
@ -32,7 +32,8 @@
|
||||
#include "qgscolorrampimpl.h"
|
||||
#include "qgsfillsymbol.h"
|
||||
#include "qgscolorutils.h"
|
||||
|
||||
#include "qgsgeos.h"
|
||||
#include "qgspolygon.h"
|
||||
#include <algorithm>
|
||||
#include <QPainter>
|
||||
#include <QDomDocument>
|
||||
@ -3952,44 +3953,67 @@ void QgsFilledLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo
|
||||
|
||||
const bool useSelectedColor = shouldRenderUsingSelectionColor( context );
|
||||
|
||||
// stroke out the path using the correct line cap/join style. We'll then use this as the fill polygon
|
||||
QPainterPathStroker stroker;
|
||||
stroker.setWidth( scaledWidth );
|
||||
stroker.setCapStyle( cap );
|
||||
stroker.setJoinStyle( join );
|
||||
|
||||
QPolygonF polygon;
|
||||
if ( qgsDoubleNear( offset, 0 ) )
|
||||
if ( points.count() >= 2 )
|
||||
{
|
||||
QPainterPath path;
|
||||
path.addPolygon( points );
|
||||
const QPainterPath stroke = stroker.createStroke( path ).simplified();
|
||||
const QPolygonF polygon = stroke.toFillPolygon();
|
||||
if ( !polygon.isEmpty() )
|
||||
{
|
||||
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale );
|
||||
if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview )
|
||||
{
|
||||
// rendering for symbol previews -- a size in meters in map units can't be calculated, so treat the size as millimeters
|
||||
// and clamp it to a reasonable range. It's the best we can do in this situation!
|
||||
scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, Qgis::RenderUnit::Millimeters ), 3.0 ), 100.0 );
|
||||
}
|
||||
std::unique_ptr< QgsAbstractGeometry > ls = QgsLineString::fromQPolygonF( points );
|
||||
geos::unique_ptr lineGeom;
|
||||
|
||||
const QList<QPolygonF> mline = ::offsetLine( points, scaledOffset, context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line );
|
||||
for ( const QPolygonF &part : mline )
|
||||
if ( !qgsDoubleNear( offset, 0 ) )
|
||||
{
|
||||
QPainterPath path;
|
||||
path.addPolygon( part );
|
||||
const QPainterPath stroke = stroker.createStroke( path ).simplified();
|
||||
const QPolygonF polygon = stroke.toFillPolygon();
|
||||
if ( !polygon.isEmpty() )
|
||||
double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale );
|
||||
if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview )
|
||||
{
|
||||
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
|
||||
// rendering for symbol previews -- a size in meters in map units can't be calculated, so treat the size as millimeters
|
||||
// and clamp it to a reasonable range. It's the best we can do in this situation!
|
||||
scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, Qgis::RenderUnit::Millimeters ), 3.0 ), 100.0 );
|
||||
}
|
||||
|
||||
const Qgis::GeometryType geometryType = context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line;
|
||||
if ( geometryType == Qgis::GeometryType::Polygon )
|
||||
{
|
||||
auto inputPoly = std::make_unique< QgsPolygon >( static_cast< QgsLineString * >( ls.release() ) );
|
||||
geos::unique_ptr g( QgsGeos::asGeos( inputPoly.get() ) );
|
||||
lineGeom = QgsGeos::buffer( g.get(), -scaledOffset, 0, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Miter, 2 );
|
||||
// the result is a polygon => extract line work
|
||||
QgsGeometry polygon( QgsGeos::fromGeos( lineGeom.get() ) );
|
||||
QVector<QgsGeometry> parts = polygon.coerceToType( Qgis::WkbType::MultiLineString );
|
||||
if ( !parts.empty() )
|
||||
{
|
||||
lineGeom = QgsGeos::asGeos( parts.at( 0 ).constGet() );
|
||||
}
|
||||
else
|
||||
{
|
||||
lineGeom.reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
geos::unique_ptr g( QgsGeos::asGeos( ls.get() ) );
|
||||
lineGeom = QgsGeos::offsetCurve( g.get(), scaledOffset, 0, Qgis::JoinStyle::Miter, 8.0 );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lineGeom = QgsGeos::asGeos( ls.get() );
|
||||
}
|
||||
|
||||
if ( lineGeom )
|
||||
{
|
||||
geos::unique_ptr buffered = QgsGeos::buffer( lineGeom.get(), scaledWidth / 2, 8,
|
||||
QgsSymbolLayerUtils::penCapStyleToEndCapStyle( cap ),
|
||||
QgsSymbolLayerUtils::penJoinStyleToJoinStyle( join ), 8 );
|
||||
if ( buffered )
|
||||
{
|
||||
// convert to rings
|
||||
std::unique_ptr< QgsAbstractGeometry > bufferedGeom = QgsGeos::fromGeos( buffered.get() );
|
||||
const QList< QList< QPolygonF > > parts = QgsSymbolLayerUtils::toQPolygonF( bufferedGeom.get(), Qgis::SymbolType::Fill );
|
||||
for ( const QList< QPolygonF > &polygon : parts )
|
||||
{
|
||||
QVector< QPolygonF > rings;
|
||||
for ( int i = 1; i < polygon.size(); ++i )
|
||||
rings << polygon.at( i );
|
||||
mFill->renderPolygon( polygon.value( 0 ), &rings, context.feature(), context.renderContext(), -1, useSelectedColor );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ __author__ = "Nyall Dawson"
|
||||
__date__ = "November 2023"
|
||||
__copyright__ = "(C) 2023, Nyall Dawson"
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from qgis.PyQt.QtCore import Qt
|
||||
from qgis.PyQt.QtGui import QColor, QImage, QPainter
|
||||
from qgis.core import (
|
||||
@ -30,6 +28,7 @@ from qgis.core import (
|
||||
QgsMapSettings,
|
||||
QgsRenderContext,
|
||||
QgsFilledLineSymbolLayer,
|
||||
QgsFillSymbol,
|
||||
)
|
||||
import unittest
|
||||
from qgis.testing import start_app, QgisTestCase
|
||||
@ -129,6 +128,42 @@ class TestQgsFilledLineSymbolLayer(QgisTestCase):
|
||||
self.image_check("render_offset", "render_offset", rendered_image)
|
||||
)
|
||||
|
||||
def testLineOffsetPolygon(self):
|
||||
s = QgsFillSymbol()
|
||||
s.deleteSymbolLayer(0)
|
||||
|
||||
line = QgsFilledLineSymbolLayer()
|
||||
line.setColor(QColor(255, 0, 0))
|
||||
line.setWidth(5)
|
||||
line.setOffset(5)
|
||||
|
||||
s.appendSymbolLayer(line.clone())
|
||||
|
||||
g = QgsGeometry.fromWkt("Polygon((2 2, 10 10, 10 0, 2 2))")
|
||||
rendered_image = self.renderGeometry(s, g)
|
||||
self.assertTrue(
|
||||
self.image_check(
|
||||
"render_offset_polygon", "render_offset_polygon", rendered_image
|
||||
)
|
||||
)
|
||||
|
||||
def testRenderLooped(self):
|
||||
s = QgsLineSymbol()
|
||||
s.deleteSymbolLayer(0)
|
||||
|
||||
line = QgsFilledLineSymbolLayer()
|
||||
line.setColor(QColor(255, 0, 0))
|
||||
line.setWidth(8)
|
||||
line.setPenCapStyle(Qt.PenCapStyle.FlatCap)
|
||||
|
||||
s.appendSymbolLayer(line.clone())
|
||||
|
||||
g = QgsGeometry.fromWkt(
|
||||
"LineString (13.07373737373737299 8.51161616161616053, 0.69292929292929273 5.93585858585858439, 11.32222222222222285 2.15808080808080582, 0 0, 10 10, 10 0)"
|
||||
)
|
||||
rendered_image = self.renderGeometry(s, g)
|
||||
self.assertTrue(self.image_check("loop", "loop", rendered_image))
|
||||
|
||||
def renderGeometry(self, symbol, geom, buffer=20):
|
||||
f = QgsFeature()
|
||||
f.setGeometry(geom)
|
||||
|
BIN
tests/testdata/control_images/symbol_filledline/expected_loop/expected_loop.png
vendored
Normal file
BIN
tests/testdata/control_images/symbol_filledline/expected_loop/expected_loop.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 966 B |
Loading…
x
Reference in New Issue
Block a user