diff --git a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in index 2282b881c3b..4ca3e26aa02 100644 --- a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in +++ b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in @@ -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 diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.cpp b/src/core/vectortile/qgsmapboxglstyleconverter.cpp index 7b1cf69dcdd..8ef17430543 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.cpp +++ b/src/core/vectortile/qgsmapboxglstyleconverter.cpp @@ -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; diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.h b/src/core/vectortile/qgsmapboxglstyleconverter.h index 916100bc17b..6ef0dcffddc 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.h +++ b/src/core/vectortile/qgsmapboxglstyleconverter.h @@ -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 diff --git a/tests/src/python/test_qgsmapboxglconverter.py b/tests/src/python/test_qgsmapboxglconverter.py index 2c7b3f3776d..17001bcecc2 100644 --- a/tests/src/python/test_qgsmapboxglconverter.py +++ b/tests/src/python/test_qgsmapboxglconverter.py @@ -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()