Skip to content

Commit

Permalink
Add utility functions to convert OGR style string to a QgsSymbol
Browse files Browse the repository at this point in the history
Supports line symbols only for now
  • Loading branch information
nyalldawson committed Mar 6, 2021
1 parent 8845a76 commit 9b20e5d
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 0 deletions.
212 changes: 212 additions & 0 deletions src/core/qgsogrutils.cpp
Expand Up @@ -22,6 +22,7 @@
#include "qgsmultipoint.h"
#include "qgsmultilinestring.h"
#include "qgsogrprovider.h"
#include "qgslinesymbollayer.h"
#include <QTextCodec>
#include <QUuid>
#include <cpl_error.h>
Expand Down Expand Up @@ -928,3 +929,214 @@ QString QgsOgrUtils::readShapefileEncodingFromLdid( const QString &path )
return QString();
#endif
}

QVariantMap QgsOgrUtils::parseStyleString( const QString &string )
{
QVariantMap styles;

char **papszStyleString = CSLTokenizeString2( string.toUtf8().constData(), ";",
CSLT_HONOURSTRINGS
| CSLT_PRESERVEQUOTES
| CSLT_PRESERVEESCAPES );
for ( int i = 0; papszStyleString[i] != nullptr; ++i )
{
// style string format is:
// <tool_name>([<tool_param>[,<tool_param>[,...]]])

// first extract tool name
const thread_local QRegularExpression sToolPartRx( QStringLiteral( "^(.*?)\\((.*)\\)$" ) );
const QString stylePart( papszStyleString[i] );
const QRegularExpressionMatch match = sToolPartRx.match( stylePart );
if ( !match.hasMatch() )
continue;

const QString tool = match.captured( 1 );
const QString params = match.captured( 2 );

char **papszTokens = CSLTokenizeString2( params.toUtf8().constData(), ",", CSLT_HONOURSTRINGS
| CSLT_PRESERVEESCAPES );

QVariantMap toolParts;
const thread_local QRegularExpression sToolParamRx( QStringLiteral( "^(.*?):(.*)$" ) );
for ( int j = 0; papszTokens[j] != nullptr; ++j )
{
const QString toolPart( papszTokens[j] );
const QRegularExpressionMatch toolMatch = sToolParamRx.match( toolPart );
if ( !match.hasMatch() )
continue;

// note we always convert the keys to lowercase, just to be safe...
toolParts.insert( toolMatch.captured( 1 ).toLower(), toolMatch.captured( 2 ) );
}
CSLDestroy( papszTokens );

// note we always convert the keys to lowercase, just to be safe...
styles.insert( tool.toLower(), toolParts );
}
CSLDestroy( papszStyleString );
return styles;
}

std::unique_ptr<QgsSymbol> QgsOgrUtils::symbolFromStyleString( const QString &string, QgsSymbol::SymbolType type )
{
const QVariantMap styles = parseStyleString( string );

auto convertSize = []( const QString & size, double & value, QgsUnitTypes::RenderUnit & unit )->bool
{
const thread_local QRegularExpression sUnitRx = QRegularExpression( QStringLiteral( "^([\\d\\.]+)(g|px|pt|mm|cm|in)$" ) );
const QRegularExpressionMatch match = sUnitRx.match( size );
if ( match.hasMatch() )
{
value = match.captured( 1 ).toDouble();
const QString unitString = match.captured( 2 );
if ( unitString.compare( QLatin1String( "px" ), Qt::CaseInsensitive ) == 0 )
{
// pixels are a poor unit choice for QGIS -- they render badly in hidpi layouts. Convert to points instead, using
// a 96 dpi conversion
static constexpr double PT_TO_INCHES_FACTOR = 1 / 72.0;
static constexpr double PX_TO_PT_FACTOR = 1 / ( 96.0 * PT_TO_INCHES_FACTOR );
unit = QgsUnitTypes::RenderPoints;
value *= PX_TO_PT_FACTOR;
return true;
}
else if ( unitString.compare( QLatin1String( "pt" ), Qt::CaseInsensitive ) == 0 )
{
unit = QgsUnitTypes::RenderPoints;
return true;
}
else if ( unitString.compare( QLatin1String( "mm" ), Qt::CaseInsensitive ) == 0 )
{
unit = QgsUnitTypes::RenderMillimeters;
return true;
}
else if ( unitString.compare( QLatin1String( "cm" ), Qt::CaseInsensitive ) == 0 )
{
value *= 10;
unit = QgsUnitTypes::RenderMillimeters;
return true;
}
else if ( unitString.compare( QLatin1String( "in" ), Qt::CaseInsensitive ) == 0 )
{
unit = QgsUnitTypes::RenderInches;
return true;
}
else if ( unitString.compare( QLatin1String( "g" ), Qt::CaseInsensitive ) == 0 )
{
unit = QgsUnitTypes::RenderMapUnits;
return true;
}
QgsDebugMsg( QStringLiteral( "Unknown unit %1" ).arg( unitString ) );
}
else
{
QgsDebugMsg( QStringLiteral( "Could not parse style size %1" ).arg( size ) );
}
return false;
};

auto convertColor = []( const QString & string ) -> 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() )
{
// need to convert #RRGGBBAA to #AARRGGBB for QColor
return QColor( QStringLiteral( "#%1%2" ).arg( match.captured( 2 ), match.captured( 1 ) ) );
}
else
{
return QColor( string );
}
};

if ( type == QgsSymbol::Line && styles.contains( QStringLiteral( "pen" ) ) )
{
// 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;
QgsUnitTypes::RenderUnit lineWidthUnit = QgsUnitTypes::RenderMillimeters;
convertSize( lineStyle.value( QStringLiteral( "w" ) ).toString(), lineWidth, lineWidthUnit );

std::unique_ptr< QgsSimpleLineSymbolLayer > simpleLine = std::make_unique< QgsSimpleLineSymbolLayer >( color, lineWidth );
simpleLine->setWidthUnit( lineWidthUnit );

// pattern
const QString pattern = lineStyle.value( QStringLiteral( "p" ) ).toString();
if ( !pattern.isEmpty() )
{
const thread_local QRegularExpression sPatternUnitRx = QRegularExpression( QStringLiteral( "^([\\d\\.\\s]+)(g|px|pt|mm|cm|in)$" ) );
const QRegularExpressionMatch match = sPatternUnitRx.match( pattern );
if ( match.hasMatch() )
{
const QStringList patternValues = match.captured( 1 ).split( ' ' );
QVector< qreal > dashPattern;
QgsUnitTypes::RenderUnit patternUnits = QgsUnitTypes::RenderMillimeters;
for ( const QString &val : patternValues )
{
double length;
convertSize( val + match.captured( 2 ), length, patternUnits );
dashPattern.push_back( length * lineWidth * 2 );
}

simpleLine->setCustomDashVector( dashPattern );
simpleLine->setCustomDashPatternUnit( patternUnits );
simpleLine->setUseCustomDashPattern( true );
}
}

Qt::PenCapStyle capStyle = Qt::FlatCap;
Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
// workaround https://github.com/OSGeo/gdal/pull/3509 in older GDAL versions
const QString id = lineStyle.value( QStringLiteral( "id" ) ).toString();
if ( id.contains( QLatin1String( "mapinfo-pen" ), Qt::CaseInsensitive ) )
{
// MapInfo renders all lines using a round pen cap and round pen join
// which are not the default values for OGR pen cap/join styles. So we need to explicitly
// override the OGR default values here on older GDAL versions
capStyle = Qt::RoundCap;
joinStyle = Qt::RoundJoin;
}

// pen cap
const QString penCap = lineStyle.value( QStringLiteral( "cap" ) ).toString();
if ( penCap.compare( QLatin1String( "b" ), Qt::CaseInsensitive ) == 0 )
{
capStyle = Qt::FlatCap;
}
else if ( penCap.compare( QLatin1String( "r" ), Qt::CaseInsensitive ) == 0 )
{
capStyle = Qt::RoundCap;
}
else if ( penCap.compare( QLatin1String( "p" ), Qt::CaseInsensitive ) == 0 )
{
capStyle = Qt::SquareCap;
}
simpleLine->setPenCapStyle( capStyle );

// pen join
const QString penJoin = lineStyle.value( QStringLiteral( "j" ) ).toString();
if ( penJoin.compare( QLatin1String( "m" ), Qt::CaseInsensitive ) == 0 )
{
joinStyle = Qt::MiterJoin;
}
else if ( penJoin.compare( QLatin1String( "r" ), Qt::CaseInsensitive ) == 0 )
{
joinStyle = Qt::RoundJoin;
}
else if ( penJoin.compare( QLatin1String( "b" ), Qt::CaseInsensitive ) == 0 )
{
joinStyle = Qt::BevelJoin;
}
simpleLine->setPenJoinStyle( joinStyle );

const QString priority = lineStyle.value( QStringLiteral( "l" ) ).toString();
if ( !priority.isEmpty() )
{
simpleLine->setRenderingPass( priority.toInt() );
}
return std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << simpleLine.release() );
}
return nullptr;
}
15 changes: 15 additions & 0 deletions src/core/qgsogrutils.h
Expand Up @@ -26,6 +26,7 @@
#include <gdalwarper.h>
#include "cpl_conv.h"
#include "cpl_string.h"
#include "qgssymbol.h"

namespace gdal
{
Expand Down Expand Up @@ -320,6 +321,20 @@ class CORE_EXPORT QgsOgrUtils
* \since QGIS 3.12
*/
static QString readShapefileEncodingFromLdid( const QString &path );

/**
* Parses an OGR style \a string to a variant map containing the style string components.
*
* \since QGIS 3.20
*/
static QVariantMap parseStyleString( const QString &string );

/**
* Creates a new QgsSymbol matching an OGR style \a string.
*
* \since QGIS 3.20
*/
static std::unique_ptr< QgsSymbol > symbolFromStyleString( const QString &string, QgsSymbol::SymbolType type ) SIP_FACTORY;
};

#endif // QGSOGRUTILS_H
84 changes: 84 additions & 0 deletions tests/src/core/testqgsogrutils.cpp
Expand Up @@ -28,6 +28,7 @@
#include "qgsapplication.h"
#include "qgspoint.h"
#include "qgsogrproxytextcodec.h"
#include "qgslinesymbollayer.h"

class TestQgsOgrUtils: public QObject
{
Expand All @@ -49,6 +50,9 @@ class TestQgsOgrUtils: public QObject
void stringToFeatureList();
void stringToFields();
void textCodec();
void parseStyleString_data();
void parseStyleString();
void convertStyleString();

private:

Expand Down Expand Up @@ -512,5 +516,85 @@ void TestQgsOgrUtils::textCodec()
// cppcheck-suppress memleak
}

void TestQgsOgrUtils::parseStyleString_data()
{
QTest::addColumn<QString>( "string" );
QTest::addColumn<QVariantMap>( "expected" );

QTest::newRow( "symbol" ) << QStringLiteral( R"""(SYMBOL(a:0,c:#000000,s:12pt,id:"mapinfo-sym-35,ogr-sym-10"))""" ) << QVariantMap{ { "symbol", QVariantMap{ { "a", "0"},
{"c", "#000000"},
{"s", "12pt"},
{"id", "mapinfo-sym-35,ogr-sym-10"},
}
} };

QTest::newRow( "pen" ) << QStringLiteral( R"""(PEN(w:2px,c:#ffb060,id:"mapinfo-pen-14,ogr-pen-6",p:"8 2 1 2px"))""" ) << QVariantMap{ { "pen", QVariantMap{ { "w", "2px"},
{"c", "#ffb060"},
{"id", "mapinfo-pen-14,ogr-pen-6"},
{"p", "8 2 1 2px"},
}
} };

QTest::newRow( "brush and pen" ) << QStringLiteral( R"""(BRUSH(FC:#ff8000,bc:#f0f000,id:"mapinfo-brush-6,ogr-brush-4");pen(W:3px,c:#e00000,id:"mapinfo-pen-2,ogr-pen-0"))""" )
<< QVariantMap{ { "brush", QVariantMap{ { "fc", "#ff8000"},
{"bc", "#f0f000"},
{"id", "mapinfo-brush-6,ogr-brush-4"}
}
},
{
"pen", QVariantMap{ { "w", "3px"},
{"c", "#e00000"},
{"id", "mapinfo-pen-2,ogr-pen-0"}
}
}
};
}

void TestQgsOgrUtils::parseStyleString()
{
QFETCH( QString, string );
QFETCH( QVariantMap, expected );

const QVariantMap res = QgsOgrUtils::parseStyleString( string );
QCOMPARE( expected, res );
}

void TestQgsOgrUtils::convertStyleString()
{
std::unique_ptr<QgsSymbol> symbol( QgsOgrUtils::symbolFromStyleString( QStringLiteral( "xxx" ), QgsSymbol::Line ) );
QVERIFY( !symbol );
symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(PEN(w:7px,c:#0040c0,id:"mapinfo-pen-5,ogr-pen-3",p:"3 1px"))""" ), QgsSymbol::Line );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#0040c0" ) );
// px sizes should be converted to pts
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->width(), 5.25 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->widthUnit(), QgsUnitTypes::RenderPoints );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->penCapStyle(), Qt::RoundCap );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->penJoinStyle(), Qt::RoundJoin );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->customDashVector(), QVector< qreal >() << 23.625 << 7.875 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->customDashPatternUnit(), QgsUnitTypes::RenderPoints );
QVERIFY( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->useCustomDashPattern() );

symbol = QgsOgrUtils::symbolFromStyleString( QStringLiteral( R"""(PEN(c:#00000087,w:10.500000cm,cap:p,j:b))""" ), QgsSymbol::Line );
QVERIFY( symbol );
QCOMPARE( symbol->symbolLayerCount(), 1 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().name(), QStringLiteral( "#000000" ) );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->color().alpha(), 135 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->width(), 105.0 );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->widthUnit(), QgsUnitTypes::RenderMillimeters );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->penCapStyle(), Qt::SquareCap );
QCOMPARE( dynamic_cast<QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) )->penJoinStyle(), Qt::BevelJoin );

// both brush and pen, but requesting a line symbol only
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 );
}

QGSTEST_MAIN( TestQgsOgrUtils )
#include "testqgsogrutils.moc"

0 comments on commit 9b20e5d

Please sign in to comment.