Handle stops in text-field when converting mapbox styles

This commit is contained in:
Nyall Dawson 2022-06-20 14:33:40 +10:00
parent 825482db2a
commit 54a979c600
4 changed files with 174 additions and 48 deletions

View File

@ -391,7 +391,6 @@ For ``json`` with intermediate stops it uses :py:func:`~QgsMapBoxGlStyleConverte
This is private API only, and may change in future QGIS versions
%End
static QString parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1 );
%Docstring
Takes values from stops and uses either :py:func:`~QgsMapBoxGlStyleConverter.scale_linear` or :py:func:`~QgsMapBoxGlStyleConverter.scale_exp` functions
@ -436,6 +435,16 @@ Parses a list of interpolation stops containing string values.
%End
static QString parseLabelStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context );
%Docstring
Parses a list of interpolation stops containing label values.
:param stops: definition of interpolation stops
:param context: conversion context
:return: converted expression
%End
static QgsProperty parseValueList( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1,
int maxOpacity = 255, QColor *defaultColor /Out/ = 0, double *defaultNumber /Out/ = 0 );
%Docstring

View File

@ -1427,52 +1427,6 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer,
}
// 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( "^{([^}]+)}$" ) );
const 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;
if ( !part.contains( '{' ) )
{
res << QgsExpression::quotedValue( part );
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" ) );
@ -1524,6 +1478,21 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer,
break;
}
case QVariant::Map:
{
const QVariantList stops = jsonTextField.toMap().value( QStringLiteral( "stops" ) ).toList();
if ( !stops.empty() )
{
labelSettings.fieldName = parseLabelStops( stops, context );
labelSettings.isExpression = true;
}
else
{
context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field dictionary" ).arg( context.layerId() ) );
}
break;
}
default:
context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextField.type() ) ) );
break;
@ -2508,6 +2477,70 @@ QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops,
return caseString;
}
QString QgsMapBoxGlStyleConverter::parseLabelStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context )
{
QString caseString = QStringLiteral( "CASE " );
bool isExpression = false;
for ( int i = 0; i < stops.length() - 1; ++i )
{
// bottom zoom and value
const QVariant bz = stops.value( i ).toList().value( 0 );
if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
{
context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
return QString();
}
// top zoom
const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
{
context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
return QString();
}
QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
if ( fieldPart.isEmpty() )
fieldPart = QStringLiteral( "''" );
else if ( !isExpression )
fieldPart = QgsExpression::quotedColumnRef( fieldPart );
caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom < %2 "
"THEN %3 " ).arg( bz.toString(),
tz.toString(),
fieldPart ) ;
}
{
const QVariant bz = stops.constLast().toList().value( 0 );
if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
{
context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
return QString();
}
QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
if ( fieldPart.isEmpty() )
fieldPart = QStringLiteral( "''" );
else if ( !isExpression )
fieldPart = QgsExpression::quotedColumnRef( fieldPart );
caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 "
"THEN %3 " ).arg( bz.toString(),
fieldPart ) ;
}
QString defaultPart = processLabelField( stops.constFirst().toList().value( 1 ).toString(), isExpression );
if ( defaultPart.isEmpty() )
defaultPart = QStringLiteral( "''" );
else if ( !isExpression )
defaultPart = QgsExpression::quotedColumnRef( defaultPart );
caseString += QStringLiteral( "ELSE %1 END" ).arg( defaultPart );
return caseString;
}
QgsProperty QgsMapBoxGlStyleConverter::parseValueList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
{
const QString method = json.value( 0 ).toString();
@ -3249,6 +3282,51 @@ QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlS
return QgsExpression::quotedColumnRef( value.toString() );
}
QString QgsMapBoxGlStyleConverter::processLabelField( const QString &string, bool &isExpression )
{
// {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( "^{([^}]+)}$" ) );
const 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;
if ( !part.contains( '{' ) )
{
res << QgsExpression::quotedValue( part );
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;
}
}
QgsVectorTileRenderer *QgsMapBoxGlStyleConverter::renderer() const
{
return mRenderer ? mRenderer->clone() : nullptr;

View File

@ -393,7 +393,6 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
const QVariantMap &conversionMap,
QString *defaultString SIP_OUT = nullptr );
/**
* Takes values from stops and uses either scale_linear() or scale_exp() functions
* to interpolate point/offset values.
@ -434,6 +433,16 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
QString *defaultString SIP_OUT = nullptr );
/**
* Parses a list of interpolation stops containing label values.
*
* \param stops definition of interpolation stops
* \param context conversion context
*
* \returns converted expression
*/
static QString parseLabelStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context );
/**
* Parses and converts a value list (e.g. an interpolate list).
*
@ -548,6 +557,8 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
static QString parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context );
static QString processLabelField( const QString &string, bool &isExpression );
/**
* Checks if interpolation bottom/top values are numeric values
* \param bottomVariant bottom value

View File

@ -874,6 +874,34 @@ class TestQgsMapBoxGlStyleConverter(unittest.TestCase):
self.assertFalse(labeling_style.labelSettings().isExpression)
self.assertEqual(labeling_style.labelSettings().fieldName, 'substance')
def testLabelWithStops(self):
context = QgsMapBoxGlStyleConversionContext()
style = {
"layout": {
"visibility": "visible",
"text-field": {
"stops": [
[
6,
""
],
[
15,
"my {class} and {stuff}"
]
]
}
},
"paint": {
"text-color": "rgba(47, 47, 47, 1)",
},
"type": "symbol"
}
rendererStyle, has_renderer, labeling_style, has_labeling = QgsMapBoxGlStyleConverter.parseSymbolLayer(style, context)
self.assertTrue(has_labeling)
self.assertTrue(labeling_style.labelSettings().isExpression)
self.assertEqual(labeling_style.labelSettings().fieldName, 'CASE WHEN @vector_tile_zoom > 6 AND @vector_tile_zoom < 15 THEN concat(\'my \',"class",\' and \',"stuff") WHEN @vector_tile_zoom >= 15 THEN concat(\'my \',"class",\' and \',"stuff") ELSE \'\' END')
if __name__ == '__main__':
unittest.main()