From da5f9db0918ebffbdc4593c295309bbada56d89f Mon Sep 17 00:00:00 2001 From: Peter Petrik Date: Fri, 19 Feb 2021 23:01:02 +0100 Subject: [PATCH] add circle vector tile layer support (#41584) * fix #41529: add circle vector tile layer support --- .../qgsmapboxglstyleconverter.sip.in | 19 +- .../vectortile/qgsmapboxglstyleconverter.cpp | 250 ++++++++++++++++++ .../vectortile/qgsmapboxglstyleconverter.h | 16 +- tests/src/python/test_qgsmapboxglconverter.py | 41 +++ 4 files changed, 322 insertions(+), 4 deletions(-) diff --git a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in index 35400539611d..ffd438c4891a 100644 --- a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in +++ b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in @@ -266,7 +266,22 @@ Parses a line layer. This is private API only, and may change in future QGIS versions -:param jsonLayer: fill layer to parse +:param jsonLayer: line layer to parse +:param context: conversion context + +:return: - ``True`` if the layer was successfully parsed. + - style: generated QGIS vector tile style +%End + + static bool parseCircleLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style /Out/, QgsMapBoxGlStyleConversionContext &context ); +%Docstring +Parses a circle layer. + +.. warning:: + + This is private API only, and may change in future QGIS versions + +:param jsonLayer: circle layer to parse :param context: conversion context :return: - ``True`` if the layer was successfully parsed. @@ -285,7 +300,7 @@ Parses a symbol layer as renderer or labeling. This is private API only, and may change in future QGIS versions -:param jsonLayer: fill layer to parse +:param jsonLayer: symbol layer to parse :param rendererStyle: generated QGIS vector tile style :param hasRenderer: will be set to ``True`` if symbol layer generated a renderer style :param labelingStyle: generated QGIS vector tile labeling diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.cpp b/src/core/vectortile/qgsmapboxglstyleconverter.cpp index b18148a2ee24..15f598e9461e 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.cpp +++ b/src/core/vectortile/qgsmapboxglstyleconverter.cpp @@ -110,6 +110,10 @@ void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers, QgsMapB { hasRendererStyle = parseLineLayer( jsonLayer, rendererStyle, *context ); } + else if ( layerType == QLatin1String( "circle" ) ) + { + hasRendererStyle = parseCircleLayer( jsonLayer, rendererStyle, *context ); + } else if ( layerType == QLatin1String( "symbol" ) ) { parseSymbolLayer( jsonLayer, rendererStyle, hasRendererStyle, labelingStyle, hasLabelingStyle, *context ); @@ -625,6 +629,252 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg return true; } +bool QgsMapBoxGlStyleConverter::parseCircleLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context ) +{ + if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) ) + { + context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) ); + return false; + } + + const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap(); + QgsPropertyCollection ddProperties; + + // circle color + QColor circleFillColor; + if ( jsonPaint.contains( QStringLiteral( "circle-color" ) ) ) + { + const QVariant jsonCircleColor = jsonPaint.value( QStringLiteral( "circle-color" ) ); + switch ( jsonCircleColor.type() ) + { + case QVariant::Map: + ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonCircleColor.toMap(), context, &circleFillColor ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleColor.toList(), PropertyType::Color, context, 1, 255, &circleFillColor ) ); + break; + + case QVariant::String: + circleFillColor = parseColor( jsonCircleColor.toString(), context ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleColor.type() ) ) ); + break; + } + } + else + { + // defaults to #000000 + circleFillColor = QColor( 0, 0, 0 ); + } + + // circle radius + double circleDiameter = 10.0; + if ( jsonPaint.contains( QStringLiteral( "circle-radius" ) ) ) + { + const QVariant jsonCircleRadius = jsonPaint.value( QStringLiteral( "circle-radius" ) ); + switch ( jsonCircleRadius.type() ) + { + case QVariant::Int: + case QVariant::Double: + circleDiameter = jsonCircleRadius.toDouble() * context.pixelSizeConversionFactor() * 2; + break; + + case QVariant::Map: + circleDiameter = -1; + ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseInterpolateByZoom( jsonCircleRadius.toMap(), context, context.pixelSizeConversionFactor() * 2, &circleDiameter ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseValueList( jsonCircleRadius.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * 2, 255, nullptr, &circleDiameter ) ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-radius type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleRadius.type() ) ) ); + break; + } + } + + double circleOpacity = -1.0; + if ( jsonPaint.contains( QStringLiteral( "circle-opacity" ) ) ) + { + const QVariant jsonCircleOpacity = jsonPaint.value( QStringLiteral( "circle-opacity" ) ); + switch ( jsonCircleOpacity.type() ) + { + case QVariant::Int: + case QVariant::Double: + circleOpacity = jsonCircleOpacity.toDouble(); + break; + + case QVariant::Map: + ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateOpacityByZoom( jsonCircleOpacity.toMap(), circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleOpacity.toList(), PropertyType::Opacity, context, 1, circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleOpacity.type() ) ) ); + break; + } + } + if ( ( circleOpacity != -1 ) && circleFillColor.isValid() ) + { + circleFillColor.setAlphaF( circleOpacity ); + } + + // circle stroke color + QColor circleStrokeColor; + if ( jsonPaint.contains( QStringLiteral( "circle-stroke-color" ) ) ) + { + const QVariant jsonCircleStrokeColor = jsonPaint.value( QStringLiteral( "circle-stroke-color" ) ); + switch ( jsonCircleStrokeColor.type() ) + { + case QVariant::Map: + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateColorByZoom( jsonCircleStrokeColor.toMap(), context, &circleStrokeColor ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeColor.toList(), PropertyType::Color, context, 1, 255, &circleStrokeColor ) ); + break; + + case QVariant::String: + circleStrokeColor = parseColor( jsonCircleStrokeColor.toString(), context ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeColor.type() ) ) ); + break; + } + } + + // circle stroke width + double circleStrokeWidth = -1.0; + if ( jsonPaint.contains( QStringLiteral( "circle-stroke-width" ) ) ) + { + const QVariant circleStrokeWidthJson = jsonPaint.value( QStringLiteral( "circle-stroke-width" ) ); + switch ( circleStrokeWidthJson.type() ) + { + case QVariant::Int: + case QVariant::Double: + circleStrokeWidth = circleStrokeWidthJson.toDouble() * context.pixelSizeConversionFactor(); + break; + + case QVariant::Map: + circleStrokeWidth = -1.0; + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateByZoom( circleStrokeWidthJson.toMap(), context, context.pixelSizeConversionFactor(), &circleStrokeWidth ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseValueList( circleStrokeWidthJson.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &circleStrokeWidth ) ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( circleStrokeWidthJson.type() ) ) ); + break; + } + } + + double circleStrokeOpacity = -1.0; + if ( jsonPaint.contains( QStringLiteral( "circle-stroke-opacity" ) ) ) + { + const QVariant jsonCircleStrokeOpacity = jsonPaint.value( QStringLiteral( "circle-stroke-opacity" ) ); + switch ( jsonCircleStrokeOpacity.type() ) + { + case QVariant::Int: + case QVariant::Double: + circleStrokeOpacity = jsonCircleStrokeOpacity.toDouble(); + break; + + case QVariant::Map: + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonCircleStrokeOpacity.toMap(), circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) ); + break; + + case QVariant::List: + case QVariant::StringList: + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeOpacity.toList(), PropertyType::Opacity, context, 1, circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeOpacity.type() ) ) ); + break; + } + } + if ( ( circleStrokeOpacity != -1 ) && circleStrokeColor.isValid() ) + { + circleStrokeColor.setAlphaF( circleStrokeOpacity ); + } + + // translate + QPointF circleTranslate; + if ( jsonPaint.contains( QStringLiteral( "circle-translate" ) ) ) + { + const QVariant jsonCircleTranslate = jsonPaint.value( QStringLiteral( "circle-translate" ) ); + switch ( jsonCircleTranslate.type() ) + { + + case QVariant::Map: + ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolatePointByZoom( jsonCircleTranslate.toMap(), context, context.pixelSizeConversionFactor(), &circleTranslate ) ); + break; + + case QVariant::List: + case QVariant::StringList: + circleTranslate = QPointF( jsonCircleTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(), + jsonCircleTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() ); + break; + + default: + context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleTranslate.type() ) ) ); + break; + } + } + + std::unique_ptr< QgsSymbol > symbol( qgis::make_unique< QgsMarkerSymbol >() ); + QgsSimpleMarkerSymbolLayer *markerSymbolLayer = dynamic_cast< QgsSimpleMarkerSymbolLayer * >( symbol->symbolLayer( 0 ) ); + Q_ASSERT( markerSymbolLayer ); + + // set render units + symbol->setOutputUnit( context.targetUnit() ); + symbol->setDataDefinedProperties( ddProperties ); + + if ( !circleTranslate.isNull() ) + { + markerSymbolLayer->setOffset( circleTranslate ); + markerSymbolLayer->setOffsetUnit( context.targetUnit() ); + } + + if ( circleFillColor.isValid() ) + { + markerSymbolLayer->setFillColor( circleFillColor ); + } + if ( circleDiameter != -1 ) + { + markerSymbolLayer->setSize( circleDiameter ); + markerSymbolLayer->setSizeUnit( context.targetUnit() ); + } + if ( circleStrokeColor.isValid() ) + { + markerSymbolLayer->setStrokeColor( circleStrokeColor ); + } + if ( circleStrokeWidth != -1 ) + { + markerSymbolLayer->setStrokeWidth( circleStrokeWidth ); + markerSymbolLayer->setStrokeWidthUnit( context.targetUnit() ); + } + + style.setGeometryType( QgsWkbTypes::PointGeometry ); + style.setSymbol( symbol.release() ); + return true; +} + void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &renderer, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context ) { hasLabeling = false; diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.h b/src/core/vectortile/qgsmapboxglstyleconverter.h index 8702fd41714d..de4f9059b7a4 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.h +++ b/src/core/vectortile/qgsmapboxglstyleconverter.h @@ -282,19 +282,31 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter * * \warning This is private API only, and may change in future QGIS versions * - * \param jsonLayer fill layer to parse + * \param jsonLayer line layer to parse * \param style generated QGIS vector tile style * \param context conversion context * \returns TRUE if the layer was successfully parsed. */ static bool parseLineLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); + /** + * Parses a circle layer. + * + * \warning This is private API only, and may change in future QGIS versions + * + * \param jsonLayer circle layer to parse + * \param style generated QGIS vector tile style + * \param context conversion context + * \returns TRUE if the layer was successfully parsed. + */ + static bool parseCircleLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); + /** * Parses a symbol layer as renderer or labeling. * * \warning This is private API only, and may change in future QGIS versions * - * \param jsonLayer fill layer to parse + * \param jsonLayer symbol layer to parse * \param rendererStyle generated QGIS vector tile style * \param hasRenderer will be set to TRUE if symbol layer generated a renderer style * \param labelingStyle generated QGIS vector tile labeling diff --git a/tests/src/python/test_qgsmapboxglconverter.py b/tests/src/python/test_qgsmapboxglconverter.py index 2ee20ab2fe94..2b452ac1a595 100644 --- a/tests/src/python/test_qgsmapboxglconverter.py +++ b/tests/src/python/test_qgsmapboxglconverter.py @@ -15,6 +15,7 @@ from qgis.PyQt.QtGui import (QColor) from qgis.core import (QgsMapBoxGlStyleConverter, QgsMapBoxGlStyleConversionContext, + QgsWkbTypes, QgsEffectStack ) @@ -548,6 +549,46 @@ def testConvertLabels(self): self.assertEqual(labeling.labelSettings().fieldName, '''lower(concat(concat("name_en",' - ',"name_fr"),"bar"))''') self.assertTrue(labeling.labelSettings().isExpression) + def testCircleLayer(self): + context = QgsMapBoxGlStyleConversionContext() + style = { + "id": "cicle_layer", + "type": "circle", + "paint": { + "circle-stroke-color": "rgba(46, 46, 46, 1)", + "circle-stroke-opacity": 0.5, + "circle-stroke-width": 3, + "circle-color": "rgba(22, 22, 22, 1)", + "circle-opacity": 0.6, + "circle-radius": 33, + "circle-translate": [11, 22] + } + } + has_renderer, rendererStyle = QgsMapBoxGlStyleConverter.parseCircleLayer(style, context) + self.assertTrue(has_renderer) + self.assertEqual(rendererStyle.geometryType(), QgsWkbTypes.PointGeometry) + properties = rendererStyle.symbol().symbolLayers()[0].properties() + expected_properties = { + 'angle': '0', + 'color': '22,22,22,153', + 'horizontal_anchor_point': '1', + 'joinstyle': 'bevel', + 'name': 'circle', + 'offset': '11,22', + 'offset_map_unit_scale': '3x:0,0,0,0,0,0', + 'offset_unit': 'Pixel', + 'outline_color': '46,46,46,128', + 'outline_style': 'solid', + 'outline_width': '3', + 'outline_width_map_unit_scale': '3x:0,0,0,0,0,0', + 'outline_width_unit': 'Pixel', + 'scale_method': 'diameter', + 'size': '66', + 'size_map_unit_scale': '3x:0,0,0,0,0,0', + 'size_unit': 'Pixel', + 'vertical_anchor_point': '1'} + self.assertEqual(properties, expected_properties) + if __name__ == '__main__': unittest.main()