Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
MUCH improved field name parsing for MapBox GL label fields/expressions
  • Loading branch information
nyalldawson committed Sep 6, 2020
1 parent df49f2d commit df774f5
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 14 deletions.
85 changes: 75 additions & 10 deletions src/core/vectortile/qgsmapboxglstyleconverter.cpp
Expand Up @@ -735,32 +735,97 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer,

QgsPalLayerSettings labelSettings;

// TODO - likely improvements required here
labelSettings.fieldName = QStringLiteral( "name:latin" );
// convert field name

auto processLabelField = []( const QString & string, bool & isExpression )->QString
{
// {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
// but if single field is covered in {}, return it directly
const QRegularExpression singleFieldRx( QStringLiteral( "^{([^}]+)}$" ) );
QRegularExpressionMatch match = singleFieldRx.match( string );
if ( match.hasMatch() )
{
isExpression = false;
return match.captured( 1 );
}

const QRegularExpression multiFieldRx( QStringLiteral( "(?={[^}]+})" ) );
const QStringList parts = string.split( multiFieldRx );
if ( parts.size() > 1 )
{
isExpression = true;

QStringList res;
for ( const QString &part : parts )
{
if ( part.isEmpty() )
continue;

// part will start at a {field} reference
const QStringList split = part.split( '}' );
res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
if ( !split.at( 1 ).isEmpty() )
res << QgsExpression::quotedValue( split.at( 1 ) );
}
return QStringLiteral( "concat(%1)" ).arg( res.join( ',' ) );
}
else
{
isExpression = false;
return string;
}
};

if ( jsonLayout.contains( QStringLiteral( "text-field" ) ) )
{
const QVariant jsonTextField = jsonLayout.value( QStringLiteral( "text-field" ) );
switch ( jsonTextField.type() )
{
case QVariant::String:
labelSettings.fieldName = jsonTextField.toString();
{
labelSettings.fieldName = processLabelField( jsonTextField.toString(), labelSettings.isExpression );
break;
}

case QVariant::List:
case QVariant::StringList:
labelSettings.fieldName = jsonTextField.toList().value( 1 ).toList().value( 1 ).toString();
{
const QVariantList textFieldList = jsonTextField.toList();
/*
* e.g.
* "text-field": ["format",
* "foo", { "font-scale": 1.2 },
* "bar", { "font-scale": 0.8 }
* ]
*/
if ( textFieldList.size() > 2 )
{
QStringList parts;
for ( int i = 1; i < textFieldList.size(); ++i )
{
bool isExpression = false;
const QString part = processLabelField( textFieldList.at( i ).toString(), isExpression );
if ( !isExpression )
parts << QgsExpression::quotedColumnRef( part );
else
parts << part;
// TODO -- we could also translate font color, underline, overline, strikethrough to HTML tags!
i += 1;
}
labelSettings.fieldName = QStringLiteral( "concat(%1)" ).arg( parts.join( ',' ) );
labelSettings.isExpression = true;
}
else
{
labelSettings.fieldName = processLabelField( textFieldList.value( 1 ).toList().value( 0 ).toString(), labelSettings.isExpression );
}
break;
}

default:
context.pushWarning( QObject::tr( "Skipping non-implemented text-field expression" ) );
break;
}

// handle ESRI specific field name constants
if ( labelSettings.fieldName == QLatin1String( "{_name_global}" ) )
labelSettings.fieldName = QStringLiteral( "_name_global" );
else if ( labelSettings.fieldName == QLatin1String( "{_name}" ) )
labelSettings.fieldName = QStringLiteral( "_name" );
}

if ( jsonLayout.contains( QStringLiteral( "text-transform" ) ) )
Expand Down
160 changes: 156 additions & 4 deletions tests/src/python/test_qgsmapboxglconverter.py
Expand Up @@ -45,18 +45,21 @@ def testColorAsHslaComponents(self):

def testParseInterpolateColorByZoom(self):
conversion_context = QgsMapBoxGlStyleConversionContext()
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({}, conversion_context).isActive(), False)
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({}, conversion_context).isActive(),
False)
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({'base': 1,
'stops': [[0, '#f1f075'],
[150, '#b52e3e'],
[250, '#e55e5e']]
}, conversion_context).expressionString(),
},
conversion_context).expressionString(),
'CASE WHEN @zoom_level >= 0 AND @zoom_level < 150 THEN color_hsla(scale_linear(@zoom_level, 0, 150, 59, 352), scale_linear(@zoom_level, 0, 150, 81, 59), scale_linear(@zoom_level, 0, 150, 70, 44), scale_linear(@zoom_level, 0, 150, 255, 255)) WHEN @zoom_level >= 150 AND @zoom_level < 250 THEN color_hsla(scale_linear(@zoom_level, 150, 250, 352, 0), scale_linear(@zoom_level, 150, 250, 59, 72), scale_linear(@zoom_level, 150, 250, 44, 63), scale_linear(@zoom_level, 150, 250, 255, 255)) WHEN @zoom_level >= 250 THEN color_hsla(0, 72, 63, 255) ELSE color_hsla(0, 72, 63, 255) END')
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({'base': 2,
'stops': [[0, '#f1f075'],
[150, '#b52e3e'],
[250, '#e55e5e']]
}, conversion_context).expressionString(),
},
conversion_context).expressionString(),
'CASE WHEN @zoom_level >= 0 AND @zoom_level < 150 THEN color_hsla(59 + 293 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 81 + -22 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 70 + -26 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 255 + 0 * (2^(@zoom_level-0)-1)/(2^(150-0)-1)) WHEN @zoom_level >= 150 AND @zoom_level < 250 THEN color_hsla(352 + -352 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 59 + 13 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 44 + 19 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 255 + 0 * (2^(@zoom_level-150)-1)/(2^(250-150)-1)) WHEN @zoom_level >= 250 THEN color_hsla(0, 72, 63, 255) ELSE color_hsla(0, 72, 63, 255) END')

def testParseStops(self):
Expand Down Expand Up @@ -180,9 +183,158 @@ def testParseExpression(self):
self.assertEqual(QgsMapBoxGlStyleConverter.parseExpression(["==", "_symbol", 0], conversion_context),
'''"_symbol" IS 0''')

self.assertEqual(QgsMapBoxGlStyleConverter.parseExpression(["all", ["==", "_symbol", 8], ["!in", "Viz", 3]], conversion_context),
self.assertEqual(QgsMapBoxGlStyleConverter.parseExpression(["all", ["==", "_symbol", 8], ["!in", "Viz", 3]],
conversion_context),
'''("_symbol" IS 8) AND (("Viz" IS NULL OR "Viz" NOT IN (3)))''')

def testConvertLabels(self):
context = QgsMapBoxGlStyleConversionContext()
style = {
"layout": {
"text-field": "{name_en}",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-max-width": 8,
"text-anchor": "top",
"text-size": 11,
"icon-size": 1
},
"type": "symbol",
"id": "poi_label",
"paint": {
"text-color": "#666",
"text-halo-width": 1.5,
"text-halo-color": "rgba(255,255,255,0.95)",
"text-halo-blur": 1
},
"source-layer": "poi_label"
}
renderer, has_renderer, labeling, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertFalse(has_renderer)
self.assertTrue(has_labeling)
self.assertEqual(labeling.labelSettings().fieldName, 'name_en')
self.assertFalse(labeling.labelSettings().isExpression)

style = {
"layout": {
"text-field": "name_en",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-max-width": 8,
"text-anchor": "top",
"text-size": 11,
"icon-size": 1
},
"type": "symbol",
"id": "poi_label",
"paint": {
"text-color": "#666",
"text-halo-width": 1.5,
"text-halo-color": "rgba(255,255,255,0.95)",
"text-halo-blur": 1
},
"source-layer": "poi_label"
}
renderer, has_renderer, labeling, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertFalse(has_renderer)
self.assertTrue(has_labeling)
self.assertEqual(labeling.labelSettings().fieldName, 'name_en')
self.assertFalse(labeling.labelSettings().isExpression)

style = {
"layout": {
"text-field": ["format",
"foo", {"font-scale": 1.2},
"bar", {"font-scale": 0.8}
],
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-max-width": 8,
"text-anchor": "top",
"text-size": 11,
"icon-size": 1
},
"type": "symbol",
"id": "poi_label",
"paint": {
"text-color": "#666",
"text-halo-width": 1.5,
"text-halo-color": "rgba(255,255,255,0.95)",
"text-halo-blur": 1
},
"source-layer": "poi_label"
}
renderer, has_renderer, labeling, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertFalse(has_renderer)
self.assertTrue(has_labeling)
self.assertEqual(labeling.labelSettings().fieldName, 'concat("foo","bar")')
self.assertTrue(labeling.labelSettings().isExpression)

style = {
"layout": {
"text-field": "{name_en} - {name_fr}",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-max-width": 8,
"text-anchor": "top",
"text-size": 11,
"icon-size": 1
},
"type": "symbol",
"id": "poi_label",
"paint": {
"text-color": "#666",
"text-halo-width": 1.5,
"text-halo-color": "rgba(255,255,255,0.95)",
"text-halo-blur": 1
},
"source-layer": "poi_label"
}
renderer, has_renderer, labeling, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertFalse(has_renderer)
self.assertTrue(has_labeling)
self.assertEqual(labeling.labelSettings().fieldName, '''concat("name_en",' - ',"name_fr")''')
self.assertTrue(labeling.labelSettings().isExpression)

style = {
"layout": {
"text-field": ["format",
"{name_en} - {name_fr}", {"font-scale": 1.2},
"bar", {"font-scale": 0.8}
],
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
],
"text-max-width": 8,
"text-anchor": "top",
"text-size": 11,
"icon-size": 1
},
"type": "symbol",
"id": "poi_label",
"paint": {
"text-color": "#666",
"text-halo-width": 1.5,
"text-halo-color": "rgba(255,255,255,0.95)",
"text-halo-blur": 1
},
"source-layer": "poi_label"
}
renderer, has_renderer, labeling, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertFalse(has_renderer)
self.assertTrue(has_labeling)
self.assertEqual(labeling.labelSettings().fieldName, '''concat(concat("name_en",' - ',"name_fr"),"bar")''')
self.assertTrue(labeling.labelSettings().isExpression)


if __name__ == '__main__':
unittest.main()

0 comments on commit df774f5

Please sign in to comment.