diff --git a/python/PyQt6/core/auto_additions/qgsmapboxglstyleconverter.py b/python/PyQt6/core/auto_additions/qgsmapboxglstyleconverter.py index ff7a34da5a83..c532153e0c53 100644 --- a/python/PyQt6/core/auto_additions/qgsmapboxglstyleconverter.py +++ b/python/PyQt6/core/auto_additions/qgsmapboxglstyleconverter.py @@ -6,6 +6,7 @@ QgsMapBoxGlStyleConverter.PropertyType.Numeric.__doc__ = "Numeric property (e.g. line width, text size)" QgsMapBoxGlStyleConverter.PropertyType.Opacity.__doc__ = "Opacity property" QgsMapBoxGlStyleConverter.PropertyType.Point.__doc__ = "Point/offset property" +QgsMapBoxGlStyleConverter.PropertyType.NumericArray.__doc__ = "Numeric array for dash arrays ot such" QgsMapBoxGlStyleConverter.PropertyType.__doc__ = """Property types, for interpolated value conversion .. warning:: @@ -16,6 +17,7 @@ * ``Numeric``: Numeric property (e.g. line width, text size) * ``Opacity``: Opacity property * ``Point``: Point/offset property +* ``NumericArray``: Numeric array for dash arrays ot such """ # -- diff --git a/python/PyQt6/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in b/python/PyQt6/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in index bfcbc09051c6..82399c3795e4 100644 --- a/python/PyQt6/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in +++ b/python/PyQt6/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in @@ -346,6 +346,7 @@ Constructor for QgsMapBoxGlStyleConverter. Numeric, Opacity, Point, + NumericArray, }; Result convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context = 0 ); diff --git a/python/core/auto_additions/qgsmapboxglstyleconverter.py b/python/core/auto_additions/qgsmapboxglstyleconverter.py index af4b1344bc22..5df18942eb8a 100644 --- a/python/core/auto_additions/qgsmapboxglstyleconverter.py +++ b/python/core/auto_additions/qgsmapboxglstyleconverter.py @@ -4,6 +4,7 @@ QgsMapBoxGlStyleConverter.PropertyType.Numeric.__doc__ = "Numeric property (e.g. line width, text size)" QgsMapBoxGlStyleConverter.PropertyType.Opacity.__doc__ = "Opacity property" QgsMapBoxGlStyleConverter.PropertyType.Point.__doc__ = "Point/offset property" +QgsMapBoxGlStyleConverter.PropertyType.NumericArray.__doc__ = "Numeric array for dash arrays ot such" QgsMapBoxGlStyleConverter.PropertyType.__doc__ = """Property types, for interpolated value conversion .. warning:: @@ -14,6 +15,7 @@ * ``Numeric``: Numeric property (e.g. line width, text size) * ``Opacity``: Opacity property * ``Point``: Point/offset property +* ``NumericArray``: Numeric array for dash arrays ot such """ # -- diff --git a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in index 8fb66394254d..0085cb04a88d 100644 --- a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in +++ b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in @@ -346,6 +346,7 @@ Constructor for QgsMapBoxGlStyleConverter. Numeric, Opacity, Point, + NumericArray, }; Result convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context = 0 ); diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.cpp b/src/core/vectortile/qgsmapboxglstyleconverter.cpp index cb33a5e7b449..a65bf1a0db9f 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.cpp +++ b/src/core/vectortile/qgsmapboxglstyleconverter.cpp @@ -699,48 +699,64 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg { const QVariantList dashSource = jsonLineDashArray.toList(); - QVector< double > rawDashVectorSizes; - rawDashVectorSizes.reserve( dashSource.size() ); - for ( const QVariant &v : dashSource ) - { - rawDashVectorSizes << v.toDouble(); - } - - // handle non-compliant dash vector patterns - if ( rawDashVectorSizes.size() == 1 ) + if ( dashSource.at( 0 ).userType() == QMetaType::Type::QString ) { - // match behavior of MapBox style rendering -- if a user makes a line dash array with one element, it's ignored - rawDashVectorSizes.clear(); + QgsProperty property = parseValueList( dashSource, PropertyType::NumericArray, context, 1, 255, nullptr, nullptr ); + if ( !lineWidthProperty.asExpression().isEmpty() ) + { + property = QgsProperty::fromExpression( QStringLiteral( "array_to_string(array_foreach(%1,@element * (%2)), ';')" ) // skip-keyword-check + .arg( property.asExpression(), lineWidthProperty.asExpression() ) ); + } + else + { + property = QgsProperty::fromExpression( QStringLiteral( "array_to_string(%1, ';')" ).arg( property.asExpression() ) ); + } + ddProperties.setProperty( QgsSymbolLayer::Property::CustomDash, property ); } - else if ( rawDashVectorSizes.size() % 2 == 1 ) + else { - // odd number of dash pattern sizes -- this isn't permitted by Qt/QGIS, but isn't explicitly blocked by the MapBox specs - // MapBox seems to add the extra dash element to the first dash size - rawDashVectorSizes[0] = rawDashVectorSizes[0] + rawDashVectorSizes[rawDashVectorSizes.size() - 1]; - rawDashVectorSizes.resize( rawDashVectorSizes.size() - 1 ); - } + QVector< double > rawDashVectorSizes; + rawDashVectorSizes.reserve( dashSource.size() ); + for ( const QVariant &v : dashSource ) + { + rawDashVectorSizes << v.toDouble(); + } - if ( !rawDashVectorSizes.isEmpty() && ( !lineWidthProperty.asExpression().isEmpty() ) ) - { - QStringList dashArrayStringParts; - dashArrayStringParts.reserve( rawDashVectorSizes.size() ); - for ( double v : std::as_const( rawDashVectorSizes ) ) + // handle non-compliant dash vector patterns + if ( rawDashVectorSizes.size() == 1 ) { - dashArrayStringParts << qgsDoubleToString( v ); + // match behavior of MapBox style rendering -- if a user makes a line dash array with one element, it's ignored + rawDashVectorSizes.clear(); + } + else if ( rawDashVectorSizes.size() % 2 == 1 ) + { + // odd number of dash pattern sizes -- this isn't permitted by Qt/QGIS, but isn't explicitly blocked by the MapBox specs + // MapBox seems to add the extra dash element to the first dash size + rawDashVectorSizes[0] = rawDashVectorSizes[0] + rawDashVectorSizes[rawDashVectorSizes.size() - 1]; + rawDashVectorSizes.resize( rawDashVectorSizes.size() - 1 ); } - QString arrayExpression = QStringLiteral( "array_to_string(array_foreach(array(%1),@element * (%2)), ';')" ) // skip-keyword-check - .arg( dashArrayStringParts.join( ',' ), - lineWidthProperty.asExpression() ); - ddProperties.setProperty( QgsSymbolLayer::Property::CustomDash, QgsProperty::fromExpression( arrayExpression ) ); - } + if ( !rawDashVectorSizes.isEmpty() && ( !lineWidthProperty.asExpression().isEmpty() ) ) + { + QStringList dashArrayStringParts; + dashArrayStringParts.reserve( rawDashVectorSizes.size() ); + for ( double v : std::as_const( rawDashVectorSizes ) ) + { + dashArrayStringParts << qgsDoubleToString( v ); + } - // dash vector sizes for QGIS symbols must be multiplied by the target line width - for ( double v : std::as_const( rawDashVectorSizes ) ) - { - dashVector << v *lineWidth; - } + QString arrayExpression = QStringLiteral( "array_to_string(array_foreach(array(%1),@element * (%2)), ';')" ) // skip-keyword-check + .arg( dashArrayStringParts.join( ',' ), + lineWidthProperty.asExpression() ); + ddProperties.setProperty( QgsSymbolLayer::Property::CustomDash, QgsProperty::fromExpression( arrayExpression ) ); + } + // dash vector sizes for QGIS symbols must be multiplied by the target line width + for ( double v : std::as_const( rawDashVectorSizes ) ) + { + dashVector << v *lineWidth; + } + } break; } @@ -2853,6 +2869,19 @@ QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, value.toList().value( 0 ).toDouble() * multiplier ); break; } + + case PropertyType::NumericArray: + { + if ( value.toList().count() == 2 && value.toList().first().toString() == QLatin1String( "literal" ) ) + { + valueString = QStringLiteral( "array(%1)" ).arg( value.toList().at( 1 ).toStringList().join( ',' ) ); + } + else + { + valueString = QStringLiteral( "array(%1)" ).arg( value.toStringList().join( ',' ) ); + } + break; + } } if ( matchString.count() == 1 ) @@ -2915,6 +2944,19 @@ QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, break; } + case PropertyType::NumericArray: + { + if ( json.constLast().toList().count() == 2 && json.constLast().toList().first().toString() == QLatin1String( "literal" ) ) + { + elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toList().at( 1 ).toStringList().join( ',' ) ); + } + else + { + elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toStringList().join( ',' ) ); + } + break; + } + } break; } @@ -2941,7 +2983,9 @@ QgsProperty QgsMapBoxGlStyleConverter::parseStepList( const QVariantList &json, const QVariant stepValue = json.value( i + 1 ); QString valueString; - if ( stepValue.canConvert() && ( stepValue.toList().count() != 2 || type != PropertyType::Point ) ) + if ( stepValue.canConvert() + && ( stepValue.toList().count() != 2 || type != PropertyType::Point ) + && type != PropertyType::NumericArray ) { valueString = parseValueList( stepValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).expressionString(); } @@ -2978,6 +3022,19 @@ QgsProperty QgsMapBoxGlStyleConverter::parseStepList( const QVariantList &json, ); break; } + + case PropertyType::NumericArray: + { + if ( stepValue.toList().count() == 2 && stepValue.toList().first().toString() == QLatin1String( "literal" ) ) + { + valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toList().at( 1 ).toStringList().join( ',' ) ); + } + else + { + valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toStringList().join( ',' ) ); + } + break; + } } } @@ -3048,6 +3105,11 @@ QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateListByZoom( const QVarian case PropertyType::Point: return parseInterpolatePointByZoom( props, context, multiplier ); + + case PropertyType::NumericArray: + context.pushWarning( QObject::tr( "%1: Skipping unsupported numeric array in interpolate" ).arg( context.layerId() ) ); + return QgsProperty(); + } return QgsProperty(); } diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.h b/src/core/vectortile/qgsmapboxglstyleconverter.h index c438f19d168a..81345e890148 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.h +++ b/src/core/vectortile/qgsmapboxglstyleconverter.h @@ -371,6 +371,7 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter Numeric, //!< Numeric property (e.g. line width, text size) Opacity, //!< Opacity property Point, //!< Point/offset property + NumericArray, //!< Numeric array for dash arrays or such }; Q_ENUM( PropertyType ) diff --git a/tests/src/python/test_qgsmapboxglconverter.py b/tests/src/python/test_qgsmapboxglconverter.py index 6fdbcd4b6ea2..a65c93335656 100644 --- a/tests/src/python/test_qgsmapboxglconverter.py +++ b/tests/src/python/test_qgsmapboxglconverter.py @@ -1175,6 +1175,51 @@ def testParseLineDashArraySingleNumber(self): 'CASE WHEN @vector_tile_zoom >= 10 AND @vector_tile_zoom <= 11 THEN (1.5) + ((1.2^(@vector_tile_zoom - 10) - 1) / (1.2^(11 - 10) - 1)) * ((2) - (1.5)) WHEN @vector_tile_zoom > 11 AND @vector_tile_zoom <= 12 THEN (2) + ((1.2^(@vector_tile_zoom - 11) - 1) / (1.2^(12 - 11) - 1)) * ((3) - (2)) WHEN @vector_tile_zoom > 12 AND @vector_tile_zoom <= 13 THEN (3) + ((1.2^(@vector_tile_zoom - 12) - 1) / (1.2^(13 - 12) - 1)) * ((5) - (3)) WHEN @vector_tile_zoom > 13 AND @vector_tile_zoom <= 14 THEN (5) + ((1.2^(@vector_tile_zoom - 13) - 1) / (1.2^(14 - 13) - 1)) * ((6) - (5)) WHEN @vector_tile_zoom > 14 AND @vector_tile_zoom <= 16 THEN (6) + ((1.2^(@vector_tile_zoom - 14) - 1) / (1.2^(16 - 14) - 1)) * ((10) - (6)) WHEN @vector_tile_zoom > 16 AND @vector_tile_zoom <= 17 THEN (10) + ((1.2^(@vector_tile_zoom - 16) - 1) / (1.2^(17 - 16) - 1)) * ((12) - (10)) WHEN @vector_tile_zoom > 17 THEN 12 END') self.assertFalse(dd_properties.property(QgsSymbolLayer.Property.PropertyCustomDash).isActive()) + def testParseLineDashArrayLiteral(self): + conversion_context = QgsMapBoxGlStyleConversionContext() + style = { + "id": "tunnel_public_transport", + "type": "line", + "source": "base_v1.0.0", + "source-layer": "transportation", + "minzoom": 8.0, + "layout": {"line-cap": "butt", "line-join": "miter", "visibility": "visible"}, + "paint": { + "line-blur": 0.4, + "line-color": "hsl(0,80%,60%)", + "line-width": [ + "interpolate", ["linear"], ["zoom"], 8, 0.5, 10, 1.2, 12, ["match", ["get", "class"], ["rail"], ["match", ["get", "subclass"], ["rail", "narrow_gauge", "rack_rail"], ["match", ["get", "service"], ["yard", "siding"], 0.25, 1], 1], 1], 14, ["match", ["get", "class"], ["rail", "rail_construction"], ["match", ["get", "subclass"], ["rail", "narrow_gauge", "rack_rail"], ["match", ["get", "service"], ["yard", "siding"], 0.25, 1.5], 1.5], 1.5], 18, + ["match", ["get", "class"], ["rail", "rail_construction"], ["match", ["get", "subclass"], ["rail", "narrow_gauge", "rack_rail"], ["match", ["get", "service"], ["yard", "siding"], 1, 2], 1.5], 1.5] + ], + "line-opacity": [ + "interpolate", ["linear"], ["zoom"], 8, 0, 8.5, ["match", ["get", "class"], ["rail"], 1, 0], 13, ["match", ["get", "subclass"], ["rail", "subway", "funicular", "narrow_gauge", "rack_rail"], ["match", ["get", "is_route"], 99, 1, 0], 0], 14, + ["match", ["get", "class"], ["rail_construction", "transit_construction"], 0.8, ["match", ["get", "subclass"], ["rail", "narrow_gauge", "funicular", "subway", "rack_rail"], ["match", ["get", "service"], ["yard", "siding"], 0, 1], 0]], 14.5, ["match", ["get", "class"], ["rail_construction", "transit_construction"], 0.8, 1] + ], + "line-dasharray": ["step", ["zoom"], ["literal", [3, 1.875]], 14, ["literal", [4, 2.5]], 15, ["literal", [5, 3.125]], 16, ["literal", [6, 3.75]]] + }, + "filter": ["all", ["==", ["get", "brunnel"], "tunnel"], ["in", ["get", "class"], ["literal", ["cable_car", "gondola", "rail", "rail_construction", "transit", "transit_construction"]]], ["==", ["geometry-type"], "LineString"]] + } + has_renderer, rendererStyle = QgsMapBoxGlStyleConverter.parseLineLayer(style, conversion_context) + self.assertTrue(has_renderer) + self.assertEqual(rendererStyle.geometryType(), QgsWkbTypes.GeometryType.LineGeometry) + self.assertFalse(rendererStyle.symbol()[0].useCustomDashPattern()) + dd_properties = rendererStyle.symbol().symbolLayers()[0].dataDefinedProperties() + self.assertEqual(dd_properties.property(QgsSymbolLayer.Property.CustomDash).asExpression(), + '''array_to_string(array_foreach(CASE WHEN @vector_tile_zoom >= 16 THEN (array(6,3.75)) WHEN @vector_tile_zoom >= 15 THEN (array(5,3.125)) WHEN @vector_tile_zoom >= 14 THEN (array(4,2.5)) ELSE (array(3,1.875)) END,@element * (CASE WHEN @vector_tile_zoom >= 8 AND @vector_tile_zoom <= 10 THEN scale_linear(@vector_tile_zoom,8,10,0.5,1.2) WHEN @vector_tile_zoom > 10 AND @vector_tile_zoom <= 12 THEN scale_linear(@vector_tile_zoom,10,12,1.2,CASE WHEN "class" = 'rail' THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 0.25 ELSE 1 END ELSE 1 END ELSE 1 END) WHEN @vector_tile_zoom > 12 AND @vector_tile_zoom <= 14 THEN scale_linear(@vector_tile_zoom,12,14,CASE WHEN "class" = 'rail' THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 0.25 ELSE 1 END ELSE 1 END ELSE 1 END,CASE WHEN "class" IN ('rail', 'rail_construction') THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 0.25 ELSE 1.5 END ELSE 1.5 END ELSE 1.5 END) WHEN @vector_tile_zoom > 14 AND @vector_tile_zoom <= 18 THEN scale_linear(@vector_tile_zoom,14,18,CASE WHEN "class" IN ('rail', 'rail_construction') THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 0.25 ELSE 1.5 END ELSE 1.5 END ELSE 1.5 END,CASE WHEN "class" IN ('rail', 'rail_construction') THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 1 ELSE 2 END ELSE 1.5 END ELSE 1.5 END) WHEN @vector_tile_zoom > 18 THEN ( ( CASE WHEN "class" IN ('rail', 'rail_construction') THEN CASE WHEN "subclass" IN ('rail', 'narrow_gauge', 'rack_rail') THEN CASE WHEN "service" IN ('yard', 'siding') THEN 1 ELSE 2 END ELSE 1.5 END ELSE 1.5 END ) * 1 ) END)), ';')''') + self.assertTrue(dd_properties.property(QgsSymbolLayer.Property.PropertyCustomDash).isActive()) + + conversion_context = QgsMapBoxGlStyleConversionContext() + style["paint"].pop("line-width") + has_renderer, rendererStyle = QgsMapBoxGlStyleConverter.parseLineLayer(style, conversion_context) + self.assertTrue(has_renderer) + self.assertEqual(rendererStyle.geometryType(), QgsWkbTypes.GeometryType.LineGeometry) + self.assertFalse(rendererStyle.symbol()[0].useCustomDashPattern()) + dd_properties = rendererStyle.symbol().symbolLayers()[0].dataDefinedProperties() + self.assertEqual(dd_properties.property(QgsSymbolLayer.Property.PropertyStrokeWidth).asExpression(), '') + self.assertEqual(dd_properties.property(QgsSymbolLayer.Property.CustomDash).asExpression(), + '''array_to_string(CASE WHEN @vector_tile_zoom >= 16 THEN (array(6,3.75)) WHEN @vector_tile_zoom >= 15 THEN (array(5,3.125)) WHEN @vector_tile_zoom >= 14 THEN (array(4,2.5)) ELSE (array(3,1.875)) END, ';')''') + self.assertTrue(dd_properties.property(QgsSymbolLayer.Property.PropertyCustomDash).isActive()) + def testParseLineNoWidth(self): conversion_context = QgsMapBoxGlStyleConversionContext() style = {