Skip to content
Permalink
Browse files

Fix calculation of zoom level for vector tiles

Previously the code was calculating the exact zoom level (a double
value) and then rounding this to an integer in order to determine
which rules should be applied.

This appears to violate the vector tile styling specifications,
which are designed to "round down" the zoom level, so that styling
rules like:

- layer zoom range: 12-22
- interpolated expression:
  case when @zoom_level > 12 and @zoom_level <= 14 then 1
  when @zoom_level > 14 and @zoom_level < 18 then 2
  when @zoom_level >=18 then 4 end

work correctly when the exact zoom level is just less than 12, e.g. 11.8

So now we use floor when converting a zoom level to int so that the
styling rules work correctly.

Additionally, this adds a new @vector_tile_zoom expression variable
which contains the original double value of the calculated tile zoom
(not the integer one used for layer visibility). Many mapbox GL styling
rules rely on non-integer zoom levels for interpolation, e.g.

Case when @vector_tile_zoom >= 11.2 then 4 ...

This change allows for smooth interpolation between zoom levels which
matches the web map appearance, instead of "jumpy" fixed level interpolation
we previously had.
  • Loading branch information
nyalldawson committed Sep 16, 2020
1 parent 079f340 commit f1c0fe259f4934b8b1aa5500afc556cbc6b7e249
@@ -790,6 +790,7 @@ void QgsExpression::initVariableHelp()

// vector tile layer variables
sVariableHelpTexts()->insert( QStringLiteral( "zoom_level" ), QCoreApplication::translate( "variable_help", "Zoom level of the tile that is being rendered (derived from the current map scale). Normally in interval [0, 20]." ) );
sVariableHelpTexts()->insert( QStringLiteral( "vector_tile_zoom" ), QCoreApplication::translate( "variable_help", "Exact zoom level of the tile that is being rendered (derived from the current map scale). Normally in interval [0, 20]. Unlike @zoom_level, this variable is a floating point value which can be used to interpolated values between two integer zoom levels." ) );

sVariableHelpTexts()->insert( QStringLiteral( "row_number" ), QCoreApplication::translate( "variable_help", "Stores the number of the current row." ) );
sVariableHelpTexts()->insert( QStringLiteral( "grid_number" ), QCoreApplication::translate( "variable_help", "Current grid annotation value." ) );
@@ -820,11 +820,11 @@ void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer,

if ( splitFontFamily( bv, fontFamily, fontStyle ) )
{
familyCaseString += QStringLiteral( "WHEN @zoom_level > %1 AND @zoom_level <= %2 "
familyCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
"THEN %3 " ).arg( bz.toString(),
tz.toString(),
QgsExpression::quotedValue( fontFamily ) );
styleCaseString += QStringLiteral( "WHEN @zoom_level > %1 AND @zoom_level <= %2 "
styleCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
"THEN %3 " ).arg( bz.toString(),
tz.toString(),
QgsExpression::quotedValue( fontStyle ) );
@@ -1763,7 +1763,7 @@ QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateColorByZoom( const QVaria
int tcAlpha;
colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );

caseString += QStringLiteral( "WHEN @zoom_level >= %1 AND @zoom_level < %2 THEN color_hsla("
caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
"%3, %4, %5, %6) " ).arg( bz, tz,
interpolateExpression( bz.toDouble(), tz.toDouble(), bcHue, tcHue, base ),
interpolateExpression( bz.toDouble(), tz.toDouble(), bcSat, tcSat, base ),
@@ -1780,7 +1780,7 @@ QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateColorByZoom( const QVaria
int tcAlpha;
colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );

caseString += QStringLiteral( "WHEN @zoom_level >= %1 THEN color_hsla(%2, %3, %4, %5) "
caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
"ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
.arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );

@@ -1842,13 +1842,13 @@ QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateOpacityByZoom( const QVar

QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVariantList &stops, int maxOpacity )
{
QString caseString = QStringLiteral( "CASE WHEN @zoom_level < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
QString caseString = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
.arg( stops.value( 0 ).toList().value( 0 ).toString() )
.arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );

for ( int i = 0; i < stops.size() - 1; ++i )
{
caseString += QStringLiteral( " WHEN @zoom_level >= %1 AND @zoom_level < %2 "
caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
"THEN set_color_part(@symbol_color, 'alpha', %3)" )
.arg( stops.value( i ).toList().value( 0 ).toString(),
stops.value( i + 1 ).toList().value( 0 ).toString(),
@@ -1858,7 +1858,7 @@ QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVarian
stops.value( i + 1 ).toList().value( 1 ).toDouble() * maxOpacity, base ) );
}

caseString += QStringLiteral( " WHEN @zoom_level >= %1 "
caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
"THEN set_color_part(@symbol_color, 'alpha', %2) END" )
.arg( stops.last().toList().value( 0 ).toString() )
.arg( stops.last().toList().value( 1 ).toDouble() * maxOpacity );
@@ -1933,8 +1933,8 @@ QString QgsMapBoxGlStyleConverter::parsePointStops( double base, const QVariantL
return QString();
}

caseString += QStringLiteral( "WHEN @zoom_level > %1 AND @zoom_level <= %2 "
"THEN array(%3,%4) " ).arg( bz.toString(),
caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
"THEN array(%3,%4)" ).arg( bz.toString(),
tz.toString(),
interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ).toDouble(), tv.toList().value( 0 ).toDouble(), base, multiplier ),
interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ).toDouble(), tv.toList().value( 1 ).toDouble(), base, multiplier ) );
@@ -1967,15 +1967,15 @@ QString QgsMapBoxGlStyleConverter::parseStops( double base, const QVariantList &
return QString();
}

caseString += QStringLiteral( "WHEN @zoom_level > %1 AND @zoom_level <= %2 "
caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
"THEN %3 " ).arg( bz.toString(),
tz.toString(),
interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toDouble(), tv.toDouble(), base, multiplier ) );
}

const QVariant z = stops.last().toList().value( 0 );
const QVariant v = stops.last().toList().value( 1 );
caseString += QStringLiteral( "WHEN @zoom_level > %1 "
caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
"THEN %2 END" ).arg( z.toString() ).arg( v.toDouble() * multiplier );
return caseString;
}
@@ -2003,7 +2003,7 @@ QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops,
return QString();
}

caseString += QStringLiteral( "WHEN @zoom_level > %1 AND @zoom_level <= %2 "
caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
"THEN %3 " ).arg( bz.toString(),
tz.toString(),
QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
@@ -2224,14 +2224,14 @@ QString QgsMapBoxGlStyleConverter::interpolateExpression( double zoomMin, double
QString expression;
if ( base == 1 )
{
expression = QStringLiteral( "scale_linear(@zoom_level,%1,%2,%3,%4)" ).arg( zoomMin )
expression = QStringLiteral( "scale_linear(@vector_tile_zoom,%1,%2,%3,%4)" ).arg( zoomMin )
.arg( zoomMax )
.arg( valueMin )
.arg( valueMax );
}
else
{
expression = QStringLiteral( "scale_exp(@zoom_level,%1,%2,%3,%4,%5)" ).arg( zoomMin )
expression = QStringLiteral( "scale_exp(@vector_tile_zoom,%1,%2,%3,%4,%5)" ).arg( zoomMin )
.arg( zoomMax )
.arg( valueMin )
.arg( valueMax )
@@ -2539,10 +2539,10 @@ QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64( const QVariant &value
sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
spritePath = prepareBase64( sprite );

spriteProperty = QStringLiteral( "CASE WHEN @zoom_level < %1 THEN '%2'" )
spriteProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN '%2'" )
.arg( stops.value( 0 ).toList().value( 0 ).toString() )
.arg( spritePath );
spriteSizeProperty = QStringLiteral( "CASE WHEN @zoom_level < %1 THEN %2" )
spriteSizeProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN %2" )
.arg( stops.value( 0 ).toList().value( 0 ).toString() )
.arg( spriteSize.width() );

@@ -2552,12 +2552,12 @@ QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64( const QVariant &value
sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
path = prepareBase64( sprite );

spriteProperty += QStringLiteral( " WHEN @zoom_level >= %1 AND @zoom_level < %2 "
spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
"THEN '%3'" )
.arg( stops.value( i ).toList().value( 0 ).toString(),
stops.value( i + 1 ).toList().value( 0 ).toString(),
path );
spriteSizeProperty += QStringLiteral( " WHEN @zoom_level >= %1 AND @zoom_level < %2 "
spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
"THEN %3" )
.arg( stops.value( i ).toList().value( 0 ).toString(),
stops.value( i + 1 ).toList().value( 0 ).toString() )
@@ -2566,11 +2566,11 @@ QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64( const QVariant &value
sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
path = prepareBase64( sprite );

spriteProperty += QStringLiteral( " WHEN @zoom_level >= %1 "
spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
"THEN '%2' END" )
.arg( stops.last().toList().value( 0 ).toString() )
.arg( path );
spriteSizeProperty += QStringLiteral( " WHEN @zoom_level >= %1 "
spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
"THEN %2 END" )
.arg( stops.last().toList().value( 0 ).toString() )
.arg( size.width() );
@@ -132,6 +132,7 @@ bool QgsVectorTileLayerRenderer::render()
// add @zoom_level variable which can be used in styling
QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Tiles" ) ); // will be deleted by popper
scope->setVariable( "zoom_level", mTileZoom, true );
scope->setVariable( "vector_tile_zoom", QgsVectorTileUtils::scaleToZoom( ctx.rendererScale() ), true );
QgsExpressionContextScopePopper popper( ctx.expressionContext(), scope );

mRenderer->startRender( *renderContext(), mTileZoom, mTileRange );
@@ -60,13 +60,17 @@ QgsFields QgsVectorTileUtils::makeQgisFields( QSet<QString> flds )
return fields;
}


int QgsVectorTileUtils::scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom )
double QgsVectorTileUtils::scaleToZoom( double mapScale )
{
double s0 = 559082264.0287178; // scale denominator at zoom level 0 of GoogleCRS84Quad
double tileZoom2 = log( s0 / mapScale ) / log( 2 );
tileZoom2 -= 1; // TODO: it seems that map scale is double (is that because of high-dpi screen?)
int tileZoom = static_cast<int>( round( tileZoom2 ) );
return tileZoom2;
}

int QgsVectorTileUtils::scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom )
{
int tileZoom = static_cast<int>( floor( scaleToZoom( mapScale ) ) );

if ( tileZoom < sourceMinZoom )
tileZoom = sourceMinZoom;
@@ -55,6 +55,14 @@ class CORE_EXPORT QgsVectorTileUtils
static QPolygon tilePolygon( QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp );
//! Returns QgsFields instance based on the set of field names
static QgsFields makeQgisFields( QSet<QString> flds );

/**
* Finds zoom level (assuming GoogleCRS84Quad tile matrix set) given map scale denominator.
*
* \since QGIS 3.16
*/
static double scaleToZoom( double mapScale );

//! Finds best fitting zoom level (assuming GoogleCRS84Quad tile matrix set) given map scale denominator and allowed zoom level range
static int scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom );
//! Returns a temporary vector layer for given sub-layer of tile in vector tile layer
@@ -105,6 +105,10 @@ QgsExpressionContext QgsSymbolLayerWidget::createExpressionContext() const
{
highlights << QStringLiteral( "zoom_level" );
}
if ( expContext.hasVariable( QStringLiteral( "vector_tile_zoom" ) ) )
{
highlights << QStringLiteral( "vector_tile_zoom" );
}

expContext.setHighlightedVariables( highlights );

@@ -413,6 +413,7 @@ void QgsVectorTileBasicLabelingWidget::editStyleAtIndex( const QModelIndex &prox
QList<QgsExpressionContextScope> scopes = context.additionalExpressionContextScopes();
QgsExpressionContextScope tileScope;
tileScope.setVariable( "zoom_level", zoom, true );
tileScope.setVariable( "vector_tile_zoom", QgsVectorTileUtils::scaleToZoom( mMapCanvas->scale() ), true );
scopes << tileScope;
context.setAdditionalExpressionContextScopes( scopes );
}
@@ -416,6 +416,7 @@ void QgsVectorTileBasicRendererWidget::editStyleAtIndex( const QModelIndex &prox
QList<QgsExpressionContextScope> scopes = context.additionalExpressionContextScopes();
QgsExpressionContextScope tileScope;
tileScope.setVariable( "zoom_level", zoom, true );
tileScope.setVariable( "vector_tile_zoom", QgsVectorTileUtils::scaleToZoom( mMapCanvas->scale() ), true );
scopes << tileScope;
context.setAdditionalExpressionContextScopes( scopes );
}

0 comments on commit f1c0fe2

Please sign in to comment.
You can’t perform that action at this time.