Add fill/brush symbol support to embedded symbol renderer

This commit is contained in:
Nyall Dawson 2021-05-06 15:15:19 +10:00
parent 8651055336
commit 98f0bcbd36
10 changed files with 530 additions and 7 deletions

View File

@ -26,6 +26,8 @@
#include "qgspolygon.h"
#include "qgsmultipolygon.h"
#include "qgsmapinfosymbolconverter.h"
#include "qgsfillsymbollayer.h"
#include "qgssymbollayerutils.h"
#include <QTextCodec>
#include <QUuid>
@ -1234,6 +1236,9 @@ std::unique_ptr<QgsSymbol> QgsOgrUtils::symbolFromStyleString( const QString &st
auto convertColor = []( const QString & string ) -> QColor
{
if ( string.isEmpty() )
return QColor();
const thread_local QRegularExpression sColorWithAlphaRx = QRegularExpression( QStringLiteral( "^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})$" ) );
const QRegularExpressionMatch match = sColorWithAlphaRx.match( string );
if ( match.hasMatch() )
@ -1247,10 +1252,8 @@ std::unique_ptr<QgsSymbol> QgsOgrUtils::symbolFromStyleString( const QString &st
}
};
if ( type == QgsSymbol::Line && styles.contains( QStringLiteral( "pen" ) ) )
auto convertPen = [&convertColor, &convertSize, string]( const QVariantMap & lineStyle ) -> std::unique_ptr< QgsSymbol >
{
// line symbol type
const QVariantMap lineStyle = styles.value( QStringLiteral( "pen" ) ).toMap();
QColor color = convertColor( lineStyle.value( QStringLiteral( "c" ), QStringLiteral( "#000000" ) ).toString() );
double lineWidth = DEFAULT_SIMPLELINE_WIDTH;
@ -1347,6 +1350,157 @@ std::unique_ptr<QgsSymbol> QgsOgrUtils::symbolFromStyleString( const QString &st
simpleLine->setRenderingPass( priority.toInt() );
}
return std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << simpleLine.release() );
};
auto convertBrush = [&convertColor]( const QVariantMap & brushStyle ) -> std::unique_ptr< QgsSymbol >
{
const QColor foreColor = convertColor( brushStyle.value( QStringLiteral( "fc" ), QStringLiteral( "#000000" ) ).toString() );
const QColor backColor = convertColor( brushStyle.value( QStringLiteral( "bc" ), QString() ).toString() );
const QString id = brushStyle.value( QStringLiteral( "id" ) ).toString();
// if the pen is a mapinfo brush, use dedicated converter for more accurate results
const thread_local QRegularExpression sMapInfoId = QRegularExpression( QStringLiteral( "mapinfo-brush-(\\d+)" ) );
const QRegularExpressionMatch match = sMapInfoId.match( id );
if ( match.hasMatch() )
{
const int brushId = match.captured( 1 ).toInt();
QgsMapInfoSymbolConversionContext context;
std::unique_ptr<QgsSymbol> res( QgsMapInfoSymbolConverter::convertFillSymbol( brushId, context, foreColor, backColor ) );
if ( res )
return res;
}
const thread_local QRegularExpression sOgrId = QRegularExpression( QStringLiteral( "ogr-brush-(\\d+)" ) );
const QRegularExpressionMatch ogrMatch = sOgrId.match( id );
Qt::BrushStyle style = Qt::SolidPattern;
if ( ogrMatch.hasMatch() )
{
const int brushId = ogrMatch.captured( 1 ).toInt();
switch ( brushId )
{
case 0:
style = Qt::SolidPattern;
break;
case 1:
style = Qt::NoBrush;
break;
case 2:
style = Qt::HorPattern;
break;
case 3:
style = Qt::VerPattern;
break;
case 4:
style = Qt::FDiagPattern;
break;
case 5:
style = Qt::BDiagPattern;
break;
case 6:
style = Qt::CrossPattern;
break;
case 7:
style = Qt::DiagCrossPattern;
break;
}
}
QgsSymbolLayerList layers;
if ( backColor.isValid() && style != Qt::SolidPattern && style != Qt::NoBrush )
{
std::unique_ptr< QgsSimpleFillSymbolLayer > backgroundFill = std::make_unique< QgsSimpleFillSymbolLayer >( backColor );
backgroundFill->setLocked( true );
backgroundFill->setStrokeStyle( Qt::NoPen );
layers << backgroundFill.release();
}
std::unique_ptr< QgsSimpleFillSymbolLayer > foregroundFill = std::make_unique< QgsSimpleFillSymbolLayer >( foreColor );
foregroundFill->setBrushStyle( style );
foregroundFill->setStrokeStyle( Qt::NoPen );
const QString priority = brushStyle.value( QStringLiteral( "l" ) ).toString();
if ( !priority.isEmpty() )
{
foregroundFill->setRenderingPass( priority.toInt() );
}
layers << foregroundFill.release();
return std::make_unique< QgsFillSymbol >( layers );
};
switch ( type )
{
case QgsSymbol::Marker:
break;
case QgsSymbol::Line:
if ( styles.contains( QStringLiteral( "pen" ) ) )
{
// line symbol type
const QVariantMap lineStyle = styles.value( QStringLiteral( "pen" ) ).toMap();
return convertPen( lineStyle );
}
else
{
return nullptr;
}
case QgsSymbol::Fill:
{
std::unique_ptr< QgsSymbol > fillSymbol = std::make_unique< QgsFillSymbol >();
if ( styles.contains( QStringLiteral( "brush" ) ) )
{
const QVariantMap brushStyle = styles.value( QStringLiteral( "brush" ) ).toMap();
fillSymbol = convertBrush( brushStyle );
}
else
{
std::unique_ptr< QgsSimpleFillSymbolLayer > emptyFill = std::make_unique< QgsSimpleFillSymbolLayer >();
emptyFill->setBrushStyle( Qt::NoBrush );
fillSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList() << emptyFill.release() );
}
std::unique_ptr< QgsSymbol > penSymbol;
if ( styles.contains( QStringLiteral( "pen" ) ) )
{
const QVariantMap lineStyle = styles.value( QStringLiteral( "pen" ) ).toMap();
penSymbol = convertPen( lineStyle );
}
if ( penSymbol )
{
const int count = penSymbol->symbolLayerCount();
if ( count == 1 )
{
// if only one pen symbol layer, let's try and combine it with the topmost brush layer, so that the resultant QGIS symbol is simpler
if ( QgsSymbolLayerUtils::condenseFillAndOutline( dynamic_cast< QgsFillSymbolLayer * >( fillSymbol->symbolLayer( fillSymbol->symbolLayerCount() - 1 ) ),
dynamic_cast< QgsLineSymbolLayer * >( penSymbol->symbolLayer( 0 ) ) ) )
return fillSymbol;
}
for ( int i = 0; i < count; ++i )
{
std::unique_ptr< QgsSymbolLayer > layer( penSymbol->takeSymbolLayer( 0 ) );
layer->setLocked( true );
fillSymbol->appendSymbolLayer( layer.release() );
}
}
return fillSymbol;
}
case QgsSymbol::Hybrid:
break;
}
return nullptr;
}

View File

@ -17,6 +17,7 @@
#include "qgslogger.h"
#include "qgslinesymbollayer.h"
#include "qgsmarkersymbollayer.h"
#include "qgsfillsymbollayer.h"
//
// QgsMapInfoSymbolConversionContext
@ -1094,3 +1095,294 @@ QgsLineSymbol *QgsMapInfoSymbolConverter::convertLineSymbol( int identifier, Qgs
return symbol.release();
}
QgsFillSymbol *QgsMapInfoSymbolConverter::convertFillSymbol( int identifier, QgsMapInfoSymbolConversionContext &context, const QColor &foreColor, const QColor &backColor )
{
Qt::BrushStyle style = Qt::SolidPattern;
bool useLineFill = false;
bool crossFill = false;
double lineAngle = 0;
double lineWidth = 0;
double lineSpacing = 1;
switch ( identifier )
{
case 0:
case 1:
style = Qt::NoBrush;
break;
case 2:
style = Qt::SolidPattern;
break;
case 3:
case 19:
style = Qt::HorPattern;
break;
case 4:
case 24:
style = Qt::VerPattern;
break;
case 5:
case 34:
style = Qt::FDiagPattern;
break;
case 6:
case 29:
style = Qt::BDiagPattern;
break;
case 7:
case 39:
style = Qt::CrossPattern;
break;
case 8:
case 44:
style = Qt::DiagCrossPattern;
break;
case 12:
style = Qt::Dense1Pattern;
break;
case 13:
style = Qt::Dense2Pattern;
break;
case 14:
style = Qt::Dense3Pattern;
break;
case 15:
style = Qt::Dense4Pattern;
break;
case 16:
style = Qt::Dense5Pattern;
break;
case 17:
style = Qt::Dense6Pattern;
break;
case 18:
style = Qt::Dense7Pattern;
break;
case 20:
useLineFill = true;
lineAngle = 0;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 21:
useLineFill = true;
lineAngle = 0;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 22:
useLineFill = true;
lineAngle = 0;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
case 23:
useLineFill = true;
lineAngle = 0;
lineSpacing = 3.0;
lineWidth = 1.0;
break;
case 25:
useLineFill = true;
lineAngle = 90;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 26:
useLineFill = true;
lineAngle = 90;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 27:
useLineFill = true;
lineAngle = 90;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
case 28:
useLineFill = true;
lineAngle = 90;
lineSpacing = 3.0;
lineWidth = 1.0;
break;
case 30:
useLineFill = true;
lineAngle = 45;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 31:
useLineFill = true;
lineAngle = 45;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 32:
useLineFill = true;
lineAngle = 45;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
case 33:
useLineFill = true;
lineAngle = 45;
lineSpacing = 3.0;
lineWidth = 1.0;
break;
case 35:
useLineFill = true;
lineAngle = 135;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 36:
useLineFill = true;
lineAngle = 135;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 37:
useLineFill = true;
lineAngle = 135;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
case 38:
useLineFill = true;
lineAngle = 135;
lineSpacing = 3.0;
lineWidth = 1.0;
break;
case 40:
useLineFill = true;
crossFill = true;
lineAngle = 0;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 41:
useLineFill = true;
crossFill = true;
lineAngle = 0;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 42:
useLineFill = true;
crossFill = true;
lineAngle = 0;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
case 43:
useLineFill = true;
crossFill = true;
lineAngle = 0;
lineSpacing = 3.0;
lineWidth = 1.0;
break;
case 45:
useLineFill = true;
crossFill = true;
lineAngle = 45;
lineSpacing = 6;
lineWidth = 1.2;
break;
case 46:
useLineFill = true;
crossFill = true;
lineAngle = 45;
lineSpacing = 4;
lineWidth = 0.8;
break;
case 47:
useLineFill = true;
crossFill = true;
lineAngle = 45;
lineSpacing = 3.4;
lineWidth = 1.2;
break;
default:
context.pushWarning( QObject::tr( "The brush style is not supported in QGIS" ) );
return nullptr;
}
QgsSymbolLayerList layers;
if ( backColor.isValid() && style != Qt::SolidPattern && ( useLineFill || style != Qt::NoBrush ) )
{
std::unique_ptr< QgsSimpleFillSymbolLayer > backgroundFill = std::make_unique< QgsSimpleFillSymbolLayer >( backColor );
backgroundFill->setLocked( true );
backgroundFill->setStrokeStyle( Qt::NoPen );
layers << backgroundFill.release();
}
if ( !useLineFill )
{
std::unique_ptr< QgsSimpleFillSymbolLayer > foregroundFill = std::make_unique< QgsSimpleFillSymbolLayer >( foreColor );
foregroundFill->setBrushStyle( style );
foregroundFill->setStrokeStyle( Qt::NoPen );
layers << foregroundFill.release();
}
else
{
std::unique_ptr< QgsLinePatternFillSymbolLayer > lineFill = std::make_unique< QgsLinePatternFillSymbolLayer >();
std::unique_ptr< QgsSimpleLineSymbolLayer > simpleLine = std::make_unique< QgsSimpleLineSymbolLayer >( foreColor, lineWidth );
simpleLine->setWidthUnit( QgsUnitTypes::RenderPoints );
lineFill->setSubSymbol( new QgsLineSymbol( QgsSymbolLayerList() << simpleLine.release() ) );
lineFill->setDistance( lineSpacing );
lineFill->setDistanceUnit( QgsUnitTypes::RenderPoints );
lineFill->setLineAngle( lineAngle );
if ( crossFill )
{
std::unique_ptr< QgsLinePatternFillSymbolLayer > lineFill2( lineFill->clone() );
lineFill2->setLineAngle( lineFill->lineAngle() + 90 );
layers << lineFill2.release();
}
layers << lineFill.release();
}
return new QgsFillSymbol( layers );
}

View File

@ -20,8 +20,10 @@
#include "qgis_sip.h"
#include "qgsunittypes.h"
#include <QStringList>
#include <QColor>
class QgsLineSymbol;
class QgsFillSymbol;
/**
* Context for a MapInfo symbol conversion operation.
@ -71,6 +73,13 @@ class CORE_EXPORT QgsMapInfoSymbolConverter
*/
static QgsLineSymbol *convertLineSymbol( int identifier, QgsMapInfoSymbolConversionContext &context, const QColor &foreColor, double size, QgsUnitTypes::RenderUnit sizeUnit, bool interleaved = false ) SIP_FACTORY;
/**
* Converts the MapInfo fill symbol with the specified \a identifier to a QgsFillSymbol.
*
* The caller takes ownership of the returned symbol.
*/
static QgsFillSymbol *convertFillSymbol( int identifier, QgsMapInfoSymbolConversionContext &context, const QColor &foreColor, const QColor &backColor = QColor() ) SIP_FACTORY;
};
#endif // QGSMAPINFOSYMBOLCONVERTER_H

View File

@ -29,6 +29,7 @@
#include "qgspoint.h"
#include "qgsogrproxytextcodec.h"
#include "qgslinesymbollayer.h"
#include "qgsfillsymbollayer.h"
class TestQgsOgrUtils: public QObject
{
@ -591,10 +592,49 @@ void TestQgsOgrUtils::convertStyleString()
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(PEN(c:#FFFF007F,w:4.000000pt);BRUSH(fc:#00FF007F))""" ), QgsSymbol::Line );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#ffff00" ) );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 127 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->width(), 4.0 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->widthUnit(), QgsUnitTypes::RenderPoints );
QCOMPARE( qgis::down_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#ffff00" ) );
QCOMPARE( qgis::down_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 127 );
QCOMPARE( qgis::down_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->width(), 4.0 );
QCOMPARE( qgis::down_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->widthUnit(), QgsUnitTypes::RenderPoints );
// brush
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(BRUSH(fc:#00FF007F))""" ), QgsSymbol::Fill );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#00ff00" ) );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 127 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->brushStyle(), Qt::SolidPattern );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeStyle(), Qt::NoPen );
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(BRUSH(fc:#00FF007F,bc:#00000087,id:ogr-brush-6))""" ), QgsSymbol::Fill );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 2 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#000000" ) );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 135 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->brushStyle(), Qt::SolidPattern );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeStyle(), Qt::NoPen );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 1 ) )->color().name(), QStringLiteral( "#00ff00" ) );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 1 ) )->color().alpha(), 127 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 1 ) )->brushStyle(), Qt::CrossPattern );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 1 ) )->strokeStyle(), Qt::NoPen );
// brush with pen
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(PEN(c:#FFFF007F,w:4.000000pt);BRUSH(fc:#00FF007F))""" ), QgsSymbol::Fill );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#00ff00" ) );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 127 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->brushStyle(), Qt::SolidPattern );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeStyle(), Qt::SolidLine );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeColor().name(), QStringLiteral( "#ffff00" ) );
// no brush, but need fill symbol
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(PEN(c:#FFFF007F,w:4.000000pt))""" ), QgsSymbol::Fill );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->brushStyle(), Qt::NoBrush );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeStyle(), Qt::SolidLine );
QCOMPARE( qgis::down_cast<QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) )->strokeColor().name(), QStringLiteral( "#ffff00" ) );
}
QGSTEST_MAIN( TestQgsOgrUtils )

View File

@ -174,6 +174,26 @@ class TestQgsEmbeddedSymbolRenderer(unittest.TestCase):
renderchecker.setControlName('expected_embedded_mapinfo_lines')
self.assertTrue(renderchecker.runTest('embedded_mapinfo_lines'))
def testMapFillLineSymbolConversion(self):
line_layer = QgsVectorLayer(TEST_DATA_DIR + '/mapinfo/fill_styles.TAB', 'Fills', 'ogr')
renderer = QgsEmbeddedSymbolRenderer(defaultSymbol=QgsLineSymbol.createSimple({}))
line_layer.setRenderer(renderer)
mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(2000, 4000))
mapsettings.setOutputDpi(96)
mapsettings.setMagnificationFactor(2)
mapsettings.setExtent(line_layer.extent().buffered(0.1))
mapsettings.setLayers([line_layer])
renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('embedded')
renderchecker.setControlName('expected_embedded_mapinfo_fills')
self.assertTrue(renderchecker.runTest('embedded_mapinfo_fills'))
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
tests/testdata/mapinfo/fill_styles.DAT vendored Normal file

Binary file not shown.

BIN
tests/testdata/mapinfo/fill_styles.ID vendored Normal file

Binary file not shown.

BIN
tests/testdata/mapinfo/fill_styles.MAP vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
!table
!version 450
!charset WindowsLatin1
Definition Table
Type NATIVE Charset "WindowsLatin1"
Fields 1
Field1 Char (10) ;