From 98ba6a47ffed30a19a6d4fe21a82bd9b4be04590 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 6 Sep 2020 17:18:27 +1000 Subject: [PATCH] Add option to convert units during conversion Because pixel based units are unfriendly for hi-dpi or print layouts --- .../qgsmapboxglstyleconverter.sip.in | 55 ++++++++++- .../vectortile/qgsmapboxglstyleconverter.cpp | 93 ++++++++++++------- .../vectortile/qgsmapboxglstyleconverter.h | 59 +++++++++++- 3 files changed, 166 insertions(+), 41 deletions(-) diff --git a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in index 3864fe0fe71..16afef7a94e 100644 --- a/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in +++ b/python/core/auto_generated/vectortile/qgsmapboxglstyleconverter.sip.in @@ -39,6 +39,51 @@ Returns a list of warning messages generated during the conversion. void clearWarnings(); %Docstring Clears the list of warning messages. +%End + + QgsUnitTypes::RenderUnit targetUnit() const; +%Docstring +Returns the target unit type. + +By default this is QgsUnitTypes.RenderPixels in order to exactly match the original +style rendering. But rendering in pixels can cause issues on hidpi displays or with print +layouts, so setting a target unit of QgsUnitTypes.Millimeters or another real-world unit +type is often more appropriate. + +.. seealso:: :py:func:`setTargetUnit` +%End + + void setTargetUnit( QgsUnitTypes::RenderUnit targetUnit ); +%Docstring +Sets the target unit type. + +By default this is QgsUnitTypes.RenderPixels in order to exactly match the original +style rendering. But rendering in pixels can cause issues on hidpi displays or with print +layouts, so setting a target unit of QgsUnitTypes.Millimeters or another real-world unit +type is often more appropriate. + +If setting to a non-pixel unit, be sure to call :py:func:`~QgsMapBoxGlStyleConversionContext.setPixelSizeConversionFactor` in order +to setup an appropriate pixel-to-unit conversion factor to scale converted sizes +using. E.g. if the target unit is millimeters, the size conversion factor should be +set to a pixel-to-millimeter value. + +.. seealso:: :py:func:`targetUnit` +%End + + double pixelSizeConversionFactor() const; +%Docstring +Returns the pixel size conversion factor, used to scale the original pixel sizes +when converting styles. + +.. seealso:: :py:func:`setSizeConversionFactor` +%End + + void setPixelSizeConversionFactor( double sizeConversionFactor ); +%Docstring +Sets the pixel size conversion factor, used to scale the original pixel sizes +when converting styles. + +.. seealso:: :py:func:`pixelSizeConversionFactor` %End }; @@ -75,7 +120,7 @@ Constructor for QgsMapBoxGlStyleConverter. NoLayerList, }; - Result convert( const QVariantMap &style ); + Result convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context = 0 ); %Docstring Converts a JSON ``style`` map, and returns the resultant status of the conversion. @@ -84,9 +129,11 @@ by calling :py:func:`~QgsMapBoxGlStyleConverter.errorMessage`. After conversion, the resultant labeling and style rules can be retrieved by calling :py:func:`~QgsMapBoxGlStyleConverter.renderer` or :py:func:`~QgsMapBoxGlStyleConverter.labeling` respectively. + +The optional ``context`` argument can be set to use a specific context during the conversion. %End - Result convert( const QString &style ); + Result convert( const QString &style, QgsMapBoxGlStyleConversionContext *context = 0 ); %Docstring Converts a JSON ``style`` string, and returns the resultant status of the conversion. @@ -95,6 +142,8 @@ by calling :py:func:`~QgsMapBoxGlStyleConverter.errorMessage`. After conversion, the resultant labeling and style rules can be retrieved by calling :py:func:`~QgsMapBoxGlStyleConverter.renderer` or :py:func:`~QgsMapBoxGlStyleConverter.labeling` respectively. + +The optional ``context`` argument can be set to use a specific context during the conversion. %End QString errorMessage() const; @@ -134,7 +183,7 @@ or ``None`` if the style could not be converted successfully. Opacity, }; - void parseLayers( const QVariantList &layers ); + void parseLayers( const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context = 0 ); %Docstring Parse list of ``layers`` from JSON. diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.cpp b/src/core/vectortile/qgsmapboxglstyleconverter.cpp index deca56017e7..8187d52158a 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.cpp +++ b/src/core/vectortile/qgsmapboxglstyleconverter.cpp @@ -33,19 +33,17 @@ #include "qgsblureffect.h" -constexpr double PIXEL_RATIO = 1; - QgsMapBoxGlStyleConverter::QgsMapBoxGlStyleConverter() { } -QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QVariantMap &style ) +QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context ) { mError.clear(); mWarnings.clear(); if ( style.contains( QStringLiteral( "layers" ) ) ) { - parseLayers( style.value( QStringLiteral( "layers" ) ).toList() ); + parseLayers( style.value( QStringLiteral( "layers" ) ).toList(), context ); } else { @@ -55,16 +53,21 @@ QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QVar return Success; } -QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QString &style ) +QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QString &style, QgsMapBoxGlStyleConversionContext *context ) { - return convert( QgsJsonUtils::parseJson( style ).toMap() ); + return convert( QgsJsonUtils::parseJson( style ).toMap(), context ); } QgsMapBoxGlStyleConverter::~QgsMapBoxGlStyleConverter() = default; -void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers ) +void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context ) { - QgsMapBoxGlStyleConversionContext context; + std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext; + if ( !context ) + { + tmpContext = qgis::make_unique< QgsMapBoxGlStyleConversionContext >(); + context = tmpContext.get(); + } QList rendererStyles; QList labelingStyles; @@ -88,7 +91,7 @@ void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers ) QString filterExpression; if ( jsonLayer.contains( QStringLiteral( "filter" ) ) ) { - filterExpression = parseExpression( jsonLayer.value( QStringLiteral( "filter" ) ).toList(), context ); + filterExpression = parseExpression( jsonLayer.value( QStringLiteral( "filter" ) ).toList(), *context ); } QgsVectorTileBasicRendererStyle rendererStyle; @@ -98,15 +101,15 @@ void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers ) bool hasLabelingStyle = false; if ( layerType == QLatin1String( "fill" ) ) { - hasRendererStyle = parseFillLayer( jsonLayer, rendererStyle, context ); + hasRendererStyle = parseFillLayer( jsonLayer, rendererStyle, *context ); } else if ( layerType == QLatin1String( "line" ) ) { - hasRendererStyle = parseLineLayer( jsonLayer, rendererStyle, context ); + hasRendererStyle = parseLineLayer( jsonLayer, rendererStyle, *context ); } else if ( layerType == QLatin1String( "symbol" ) ) { - parseSymbolLayer( jsonLayer, rendererStyle, hasRendererStyle, labelingStyle, hasLabelingStyle, context ); + parseSymbolLayer( jsonLayer, rendererStyle, hasRendererStyle, labelingStyle, hasLabelingStyle, *context ); } else { @@ -137,8 +140,8 @@ void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers ) labelingStyles.append( labelingStyle ); } - mWarnings.append( context.warnings() ); - context.clearWarnings(); + mWarnings.append( context->warnings() ); + context->clearWarnings(); } mRenderer = qgis::make_unique< QgsVectorTileBasicRenderer >(); @@ -275,8 +278,8 @@ bool QgsMapBoxGlStyleConverter::parseFillLayer( const QVariantMap &jsonLayer, Qg QgsSimpleFillSymbolLayer *fillSymbol = dynamic_cast< QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) ); // set render units - symbol->setOutputUnit( QgsUnitTypes::RenderPixels ); - fillSymbol->setOutputUnit( QgsUnitTypes::RenderPixels ); + symbol->setOutputUnit( context.targetUnit() ); + fillSymbol->setOutputUnit( context.targetUnit() ); if ( jsonPaint.contains( QStringLiteral( "fill-pattern" ) ) ) { @@ -387,17 +390,17 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg { case QVariant::Int: case QVariant::Double: - lineWidth = jsonLineWidth.toDouble(); + lineWidth = jsonLineWidth.toDouble() * context.pixelSizeConversionFactor(); break; case QVariant::Map: lineWidth = -1; - ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateByZoom( jsonLineWidth.toMap(), context, PIXEL_RATIO ) ); + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateByZoom( jsonLineWidth.toMap(), context, context.pixelSizeConversionFactor() ) ); break; case QVariant::List: case QVariant::StringList: - ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateListByZoom( jsonLineWidth.toList(), PropertyType::Numeric, context, PIXEL_RATIO ) ); + ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateListByZoom( jsonLineWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() ) ); break; default: @@ -460,7 +463,7 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg const QVariantList dashSource = jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList().last().toList().value( 1 ).toList(); for ( const QVariant &v : dashSource ) { - dashVector << v.toDouble() * PIXEL_RATIO; + dashVector << v.toDouble() * context.pixelSizeConversionFactor(); } break; } @@ -471,7 +474,7 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg const QVariantList dashSource = jsonLineDashArray.toList(); for ( const QVariant &v : dashSource ) { - dashVector << v.toDouble() * PIXEL_RATIO; + dashVector << v.toDouble() * context.pixelSizeConversionFactor(); } break; } @@ -501,8 +504,8 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg QgsSimpleLineSymbolLayer *lineSymbol = dynamic_cast< QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) ); // set render units - symbol->setOutputUnit( QgsUnitTypes::RenderPixels ); - lineSymbol->setOutputUnit( QgsUnitTypes::RenderPixels ); + symbol->setOutputUnit( context.targetUnit() ); + lineSymbol->setOutputUnit( context.targetUnit() ); lineSymbol->setPenCapStyle( penCapStyle ); lineSymbol->setPenJoinStyle( penJoinStyle ); lineSymbol->setDataDefinedProperties( ddProperties ); @@ -517,7 +520,7 @@ bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, Qg } if ( lineWidth != -1 ) { - lineSymbol->setWidth( lineWidth * PIXEL_RATIO ); + lineSymbol->setWidth( lineWidth ); } if ( !dashVector.empty() ) { @@ -560,18 +563,18 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, { case QVariant::Int: case QVariant::Double: - textSize = jsonTextSize.toDouble(); + textSize = jsonTextSize.toDouble() * context.pixelSizeConversionFactor(); break; case QVariant::Map: textSize = -1; - ddLabelProperties.setProperty( QgsPalLayerSettings::Size, parseInterpolateByZoom( jsonTextSize.toMap(), context ) ); + ddLabelProperties.setProperty( QgsPalLayerSettings::Size, parseInterpolateByZoom( jsonTextSize.toMap(), context, context.pixelSizeConversionFactor() ) ); break; case QVariant::List: case QVariant::StringList: textSize = -1; - ddLabelProperties.setProperty( QgsPalLayerSettings::Size, parseInterpolateListByZoom( jsonTextSize.toList(), PropertyType::Numeric, context ) ); + ddLabelProperties.setProperty( QgsPalLayerSettings::Size, parseInterpolateListByZoom( jsonTextSize.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() ) ); break; default: @@ -690,18 +693,18 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, { case QVariant::Int: case QVariant::Double: - bufferSize = jsonHaloWidth.toDouble(); + bufferSize = jsonHaloWidth.toDouble() * context.pixelSizeConversionFactor(); break; case QVariant::Map: bufferSize = 1; - ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateByZoom( jsonHaloWidth.toMap(), context ) ); + ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateByZoom( jsonHaloWidth.toMap(), context, context.pixelSizeConversionFactor() ) ); break; case QVariant::List: case QVariant::StringList: bufferSize = 1; - ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateListByZoom( jsonHaloWidth.toList(), PropertyType::Numeric, context ) ); + ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateListByZoom( jsonHaloWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() ) ); break; default: @@ -719,7 +722,7 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, case QVariant::Int: case QVariant::Double: { - haloBlurSize = jsonTextHaloBlur.toDouble(); + haloBlurSize = jsonTextHaloBlur.toDouble() * context.pixelSizeConversionFactor(); break; } @@ -730,7 +733,7 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, } QgsTextFormat format; - format.setSizeUnit( QgsUnitTypes::RenderPixels ); + format.setSizeUnit( context.targetUnit() ); if ( textColor.isValid() ) format.setColor( textColor ); if ( textSize >= 0 ) @@ -741,8 +744,8 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, if ( bufferSize > 0 ) { format.buffer().setEnabled( true ); - format.buffer().setSize( bufferSize * PIXEL_RATIO ); - format.buffer().setSizeUnit( QgsUnitTypes::RenderPixels ); + format.buffer().setSize( bufferSize ); + format.buffer().setSizeUnit( context.targetUnit() ); format.buffer().setColor( bufferColor ); if ( haloBlurSize > 0 ) @@ -750,7 +753,7 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, QgsEffectStack *stack = new QgsEffectStack(); QgsBlurEffect *blur = new QgsBlurEffect() ; blur->setEnabled( true ); - blur->setBlurUnit( QgsUnitTypes::RenderPixels ); + blur->setBlurUnit( context.targetUnit() ); blur->setBlurLevel( haloBlurSize ); blur->setBlurMethod( QgsBlurEffect::StackBlur ); stack->appendEffect( blur ); @@ -1470,3 +1473,23 @@ void QgsMapBoxGlStyleConversionContext::pushWarning( const QString &warning ) QgsDebugMsg( warning ); mWarnings << warning; } + +QgsUnitTypes::RenderUnit QgsMapBoxGlStyleConversionContext::targetUnit() const +{ + return mTargetUnit; +} + +void QgsMapBoxGlStyleConversionContext::setTargetUnit( QgsUnitTypes::RenderUnit targetUnit ) +{ + mTargetUnit = targetUnit; +} + +double QgsMapBoxGlStyleConversionContext::pixelSizeConversionFactor() const +{ + return mSizeConversionFactor; +} + +void QgsMapBoxGlStyleConversionContext::setPixelSizeConversionFactor( double sizeConversionFactor ) +{ + mSizeConversionFactor = sizeConversionFactor; +} diff --git a/src/core/vectortile/qgsmapboxglstyleconverter.h b/src/core/vectortile/qgsmapboxglstyleconverter.h index 0910053316c..f883b0f8714 100644 --- a/src/core/vectortile/qgsmapboxglstyleconverter.h +++ b/src/core/vectortile/qgsmapboxglstyleconverter.h @@ -52,9 +52,58 @@ class CORE_EXPORT QgsMapBoxGlStyleConversionContext */ void clearWarnings() { mWarnings.clear(); } + /** + * Returns the target unit type. + * + * By default this is QgsUnitTypes::RenderPixels in order to exactly match the original + * style rendering. But rendering in pixels can cause issues on hidpi displays or with print + * layouts, so setting a target unit of QgsUnitTypes::Millimeters or another real-world unit + * type is often more appropriate. + * + * \see setTargetUnit() + */ + QgsUnitTypes::RenderUnit targetUnit() const; + + /** + * Sets the target unit type. + * + * By default this is QgsUnitTypes::RenderPixels in order to exactly match the original + * style rendering. But rendering in pixels can cause issues on hidpi displays or with print + * layouts, so setting a target unit of QgsUnitTypes::Millimeters or another real-world unit + * type is often more appropriate. + * + * If setting to a non-pixel unit, be sure to call setPixelSizeConversionFactor() in order + * to setup an appropriate pixel-to-unit conversion factor to scale converted sizes + * using. E.g. if the target unit is millimeters, the size conversion factor should be + * set to a pixel-to-millimeter value. + * + * \see targetUnit() + */ + void setTargetUnit( QgsUnitTypes::RenderUnit targetUnit ); + + /** + * Returns the pixel size conversion factor, used to scale the original pixel sizes + * when converting styles. + * + * \see setSizeConversionFactor() + */ + double pixelSizeConversionFactor() const; + + /** + * Sets the pixel size conversion factor, used to scale the original pixel sizes + * when converting styles. + * + * \see pixelSizeConversionFactor() + */ + void setPixelSizeConversionFactor( double sizeConversionFactor ); + private: QStringList mWarnings; + + QgsUnitTypes::RenderUnit mTargetUnit = QgsUnitTypes::RenderPixels; + + double mSizeConversionFactor = 1.0; }; /** @@ -99,8 +148,10 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter * * After conversion, the resultant labeling and style rules can be retrieved by calling * renderer() or labeling() respectively. + * + * The optional \a context argument can be set to use a specific context during the conversion. */ - Result convert( const QVariantMap &style ); + Result convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context = nullptr ); /** * Converts a JSON \a style string, and returns the resultant status of the conversion. @@ -110,8 +161,10 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter * * After conversion, the resultant labeling and style rules can be retrieved by calling * renderer() or labeling() respectively. + * + * The optional \a context argument can be set to use a specific context during the conversion. */ - Result convert( const QString &style ); + Result convert( const QString &style, QgsMapBoxGlStyleConversionContext *context = nullptr ); /** * Returns a descriptive error message if an error was encountered during the style conversion, @@ -158,7 +211,7 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter * Parse list of \a layers from JSON. * \warning This is private API only, and may change in future QGIS versions */ - void parseLayers( const QVariantList &layers ); + void parseLayers( const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context = nullptr ); /** * Parses a fill layer.