[vector tiles] Fix sprites not loaded from the OSM shortbread styles

This commit is contained in:
Mathieu Pellerin 2025-06-06 11:21:47 +07:00 committed by Nyall Dawson
parent b7de3815a4
commit 09c5478ad2
6 changed files with 239 additions and 87 deletions

View File

@ -90,39 +90,54 @@ sizes when converting styles.
.. seealso:: :py:func:`pixelSizeConversionFactor`
%End
QImage spriteImage() const;
QStringList spriteCategories() const;
%Docstring
Returns the sprite image to use during conversion, or an invalid image
if this is not set.
Returns the list of sprite categories to use during conversion, or an
empty list of none is set.
.. seealso:: :py:func:`spriteDefinitions`
.. seealso:: :py:func:`spriteImage`
.. versionadded:: 3.44
%End
QImage spriteImage( const QString &category = QString() ) const;
%Docstring
Returns the sprite image for a given ``category`` to use during
conversion, or an invalid image if this is not set.
.. seealso:: :py:func:`spriteCategories`
.. seealso:: :py:func:`spriteDefinitions`
.. seealso:: :py:func:`setSprites`
%End
QVariantMap spriteDefinitions() const;
QVariantMap spriteDefinitions( const QString &category = QString() ) const;
%Docstring
Returns the sprite definitions to use during conversion.
Returns the sprite definitions for a given ``category`` to use during
conversion.
.. seealso:: :py:func:`spriteImage`
.. seealso:: :py:func:`setSprites`
%End
void setSprites( const QImage &image, const QVariantMap &definitions );
void setSprites( const QImage &image, const QVariantMap &definitions, const QString &category = QString() );
%Docstring
Sets the sprite ``image`` and ``definitions`` JSON to use during
conversion.
Sets the sprite ``image`` and ``definitions`` JSON for a given
``category`` to use during conversion.
.. seealso:: :py:func:`spriteImage`
.. seealso:: :py:func:`spriteDefinitions`
%End
void setSprites( const QImage &image, const QString &definitions );
void setSprites( const QImage &image, const QString &definitions, const QString &category = QString() );
%Docstring
Sets the sprite ``image`` and ``definitions`` JSON string to use during
conversion.
Sets the sprite ``image`` and ``definitions`` JSON string for a given
``category`` to use during conversion.
.. seealso:: :py:func:`spriteImage`

View File

@ -90,39 +90,54 @@ sizes when converting styles.
.. seealso:: :py:func:`pixelSizeConversionFactor`
%End
QImage spriteImage() const;
QStringList spriteCategories() const;
%Docstring
Returns the sprite image to use during conversion, or an invalid image
if this is not set.
Returns the list of sprite categories to use during conversion, or an
empty list of none is set.
.. seealso:: :py:func:`spriteDefinitions`
.. seealso:: :py:func:`spriteImage`
.. versionadded:: 3.44
%End
QImage spriteImage( const QString &category = QString() ) const;
%Docstring
Returns the sprite image for a given ``category`` to use during
conversion, or an invalid image if this is not set.
.. seealso:: :py:func:`spriteCategories`
.. seealso:: :py:func:`spriteDefinitions`
.. seealso:: :py:func:`setSprites`
%End
QVariantMap spriteDefinitions() const;
QVariantMap spriteDefinitions( const QString &category = QString() ) const;
%Docstring
Returns the sprite definitions to use during conversion.
Returns the sprite definitions for a given ``category`` to use during
conversion.
.. seealso:: :py:func:`spriteImage`
.. seealso:: :py:func:`setSprites`
%End
void setSprites( const QImage &image, const QVariantMap &definitions );
void setSprites( const QImage &image, const QVariantMap &definitions, const QString &category = QString() );
%Docstring
Sets the sprite ``image`` and ``definitions`` JSON to use during
conversion.
Sets the sprite ``image`` and ``definitions`` JSON for a given
``category`` to use during conversion.
.. seealso:: :py:func:`spriteImage`
.. seealso:: :py:func:`spriteDefinitions`
%End
void setSprites( const QImage &image, const QString &definitions );
void setSprites( const QImage &image, const QString &definitions, const QString &category = QString() );
%Docstring
Sets the sprite ``image`` and ``definitions`` JSON string to use during
conversion.
Sets the sprite ``image`` and ``definitions`` JSON string for a given
``category`` to use during conversion.
.. seealso:: :py:func:`spriteImage`

View File

@ -3524,23 +3524,46 @@ QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expressi
QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
{
if ( context.spriteImage().isNull() )
QImage spriteImage;
QString category;
QString actualName = name;
const int categorySeparator = name.indexOf( ':' );
if ( categorySeparator > 0 )
{
category = name.left( categorySeparator );
if ( context.spriteCategories().contains( category ) )
{
actualName = name.mid( categorySeparator + 1 );
spriteImage = context.spriteImage( category );
}
else
{
category.clear();
}
}
if ( category.isEmpty() )
{
spriteImage = context.spriteImage();
}
if ( spriteImage.isNull() )
{
context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
return QImage();
}
const QVariantMap spriteDefinition = context.spriteDefinitions().value( name ).toMap();
const QVariantMap spriteDefinition = context.spriteDefinitions( category ).value( actualName ).toMap();
if ( spriteDefinition.size() == 0 )
{
context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
return QImage();
}
const QImage sprite = context.spriteImage().copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
const QImage sprite = spriteImage.copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
if ( sprite.isNull() )
{
context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
@ -4091,25 +4114,30 @@ void QgsMapBoxGlStyleConversionContext::setPixelSizeConversionFactor( double siz
mSizeConversionFactor = sizeConversionFactor;
}
QImage QgsMapBoxGlStyleConversionContext::spriteImage() const
QStringList QgsMapBoxGlStyleConversionContext::spriteCategories() const
{
return mSpriteImage;
return mSpriteImage.keys();
}
QVariantMap QgsMapBoxGlStyleConversionContext::spriteDefinitions() const
QImage QgsMapBoxGlStyleConversionContext::spriteImage( const QString &category ) const
{
return mSpriteDefinitions;
return mSpriteImage.contains( category ) ? mSpriteImage[category] : QImage();
}
void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions )
QVariantMap QgsMapBoxGlStyleConversionContext::spriteDefinitions( const QString &category ) const
{
mSpriteImage = image;
mSpriteDefinitions = definitions;
return mSpriteDefinitions.contains( category ) ? mSpriteDefinitions[category] : QVariantMap();
}
void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions )
void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions, const QString &category )
{
setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap() );
mSpriteImage[category] = image;
mSpriteDefinitions[category] = definitions;
}
void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions, const QString &category )
{
setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap(), category );
}
QString QgsMapBoxGlStyleConversionContext::layerId() const

View File

@ -103,36 +103,46 @@ class CORE_EXPORT QgsMapBoxGlStyleConversionContext
void setPixelSizeConversionFactor( double sizeConversionFactor );
/**
* Returns the sprite image to use during conversion, or an invalid image if this is not set.
* Returns the list of sprite categories to use during conversion, or an empty list of none is set.
*
* \see spriteDefinitions()
* \see spriteImage()
* \since QGIS 3.44
*/
QStringList spriteCategories() const;
/**
* Returns the sprite image for a given \a category to use during conversion, or an invalid image if this is not set.
*
* \see spriteCategories()
* \see spriteDefinitions()
* \see setSprites()
*/
QImage spriteImage() const;
QImage spriteImage( const QString &category = QString() ) const;
/**
* Returns the sprite definitions to use during conversion.
* Returns the sprite definitions for a given \a category to use during conversion.
*
* \see spriteImage()
* \see setSprites()
*/
QVariantMap spriteDefinitions() const;
QVariantMap spriteDefinitions( const QString &category = QString() ) const;
/**
* Sets the sprite \a image and \a definitions JSON to use during conversion.
* Sets the sprite \a image and \a definitions JSON for a given \a category to use during conversion.
*
* \see spriteImage()
* \see spriteDefinitions()
*/
void setSprites( const QImage &image, const QVariantMap &definitions );
void setSprites( const QImage &image, const QVariantMap &definitions, const QString &category = QString() );
/**
* Sets the sprite \a image and \a definitions JSON string to use during conversion.
* Sets the sprite \a image and \a definitions JSON string for a given \a category to use during conversion.
*
* \see spriteImage()
* \see spriteDefinitions()
*/
void setSprites( const QImage &image, const QString &definitions );
void setSprites( const QImage &image, const QString &definitions, const QString &category = QString() );
/**
* Returns the layer ID of the layer currently being converted.
@ -158,8 +168,8 @@ class CORE_EXPORT QgsMapBoxGlStyleConversionContext
double mSizeConversionFactor = 1.0;
QImage mSpriteImage;
QVariantMap mSpriteDefinitions;
QMap<QString, QImage> mSpriteImage;
QMap<QString, QVariantMap> mSpriteDefinitions;
};

View File

@ -320,66 +320,101 @@ void QgsVectorTileUtils::sortTilesByDistanceFromCenter( QVector<QgsTileXYZ> &til
void QgsVectorTileUtils::loadSprites( const QVariantMap &styleDefinition, QgsMapBoxGlStyleConversionContext &context, const QString &styleUrl )
{
if ( styleDefinition.contains( QStringLiteral( "sprite" ) ) && ( context.spriteDefinitions().empty() || context.spriteImage().isNull() ) )
if ( styleDefinition.contains( QStringLiteral( "sprite" ) ) && ( context.spriteCategories().isEmpty() ) )
{
// retrieve sprite definition
QString spriteUriBase;
if ( styleDefinition.value( QStringLiteral( "sprite" ) ).toString().startsWith( QLatin1String( "http" ) ) )
auto prepareSpriteUrl = []( const QString & sprite, const QString & styleUrl )
{
spriteUriBase = styleDefinition.value( QStringLiteral( "sprite" ) ).toString();
if ( sprite.startsWith( QLatin1String( "http" ) ) )
{
return sprite;
}
else if ( sprite.startsWith( QLatin1String( "/" ) ) )
{
const QUrl url( styleUrl );
return QStringLiteral( "%1://%2%3" ).arg( url.scheme(), url.host(), sprite );
}
return QStringLiteral( "%1/%2" ).arg( styleUrl, sprite );
};
// retrieve sprite definition
QMap<QString, QString> sprites;
const QVariant spriteVariant = styleDefinition.value( QStringLiteral( "sprite" ) );
if ( spriteVariant.userType() == QMetaType::Type::QVariantList )
{
const QVariantList spriteList = spriteVariant.toList();
for ( const QVariant &spriteItem : spriteList )
{
QVariantMap spriteMap = spriteItem.toMap();
if ( spriteMap.contains( QStringLiteral( "id" ) ) && spriteMap.contains( "url" ) )
{
sprites[spriteMap.value( QStringLiteral( "id" ) ).toString()] = prepareSpriteUrl( spriteMap.value( QStringLiteral( "url" ) ).toString(), styleUrl );
}
}
}
else
{
spriteUriBase = styleUrl + '/' + styleDefinition.value( QStringLiteral( "sprite" ) ).toString();
sprites[""] = prepareSpriteUrl( spriteVariant.toString(), styleUrl );
}
for ( int resolution = 2; resolution > 0; resolution-- )
if ( sprites.isEmpty() )
{
QUrl spriteUrl = QUrl( spriteUriBase );
spriteUrl.setPath( spriteUrl.path() + QStringLiteral( "%1.json" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() ) );
QNetworkRequest request = QNetworkRequest( spriteUrl );
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLayer" ) )
QgsBlockingNetworkRequest networkRequest;
switch ( networkRequest.get( request ) )
{
case QgsBlockingNetworkRequest::NoError:
{
const QgsNetworkReplyContent content = networkRequest.reply();
const QVariantMap spriteDefinition = QgsJsonUtils::parseJson( content.content() ).toMap();
return;
}
// retrieve sprite images
QUrl spriteUrl = QUrl( spriteUriBase );
spriteUrl.setPath( spriteUrl.path() + QStringLiteral( "%1.png" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() ) );
QNetworkRequest request = QNetworkRequest( spriteUrl );
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLayer" ) )
QgsBlockingNetworkRequest networkRequest;
switch ( networkRequest.get( request ) )
QMap<QString, QString>::const_iterator spritesIterator = sprites.constBegin();
while ( spritesIterator != sprites.end() )
{
for ( int resolution = 2; resolution > 0; resolution-- )
{
QUrl spriteUrl = QUrl( spritesIterator.value() );
spriteUrl.setPath( spriteUrl.path() + QStringLiteral( "%1.json" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() ) );
QNetworkRequest request = QNetworkRequest( spriteUrl );
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLayer" ) )
QgsBlockingNetworkRequest networkRequest;
switch ( networkRequest.get( request ) )
{
case QgsBlockingNetworkRequest::NoError:
{
case QgsBlockingNetworkRequest::NoError:
const QgsNetworkReplyContent content = networkRequest.reply();
const QVariantMap spriteDefinition = QgsJsonUtils::parseJson( content.content() ).toMap();
// retrieve sprite images
QUrl spriteUrl = QUrl( spritesIterator.value() );
spriteUrl.setPath( spriteUrl.path() + QStringLiteral( "%1.png" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() ) );
QNetworkRequest request = QNetworkRequest( spriteUrl );
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLayer" ) )
QgsBlockingNetworkRequest networkRequest;
switch ( networkRequest.get( request ) )
{
const QgsNetworkReplyContent imageContent = networkRequest.reply();
const QImage spriteImage( QImage::fromData( imageContent.content() ) );
context.setSprites( spriteImage, spriteDefinition );
break;
case QgsBlockingNetworkRequest::NoError:
{
const QgsNetworkReplyContent imageContent = networkRequest.reply();
const QImage spriteImage( QImage::fromData( imageContent.content() ) );
context.setSprites( spriteImage, spriteDefinition, spritesIterator.key() );
break;
}
case QgsBlockingNetworkRequest::NetworkError:
case QgsBlockingNetworkRequest::TimeoutError:
case QgsBlockingNetworkRequest::ServerExceptionError:
break;
}
case QgsBlockingNetworkRequest::NetworkError:
case QgsBlockingNetworkRequest::TimeoutError:
case QgsBlockingNetworkRequest::ServerExceptionError:
break;
break;
}
break;
case QgsBlockingNetworkRequest::NetworkError:
case QgsBlockingNetworkRequest::TimeoutError:
case QgsBlockingNetworkRequest::ServerExceptionError:
break;
}
case QgsBlockingNetworkRequest::NetworkError:
case QgsBlockingNetworkRequest::TimeoutError:
case QgsBlockingNetworkRequest::ServerExceptionError:
if ( !context.spriteDefinitions().isEmpty() )
break;
}
if ( !context.spriteDefinitions().isEmpty() )
break;
++spritesIterator;
}
}
}

View File

@ -2735,6 +2735,55 @@ class TestQgsMapBoxGlStyleConverter(QgisTestCase):
'CASE WHEN "lake_depth" IS NOT NULL THEN 24 WHEN length(to_string("ele")) IS 3 THEN 18 ELSE 24 END',
)
def testRetrieveSpriteWithCategory(self):
context = QgsMapBoxGlStyleConversionContext()
sprite_image_file = (
f"{TEST_DATA_DIR}/vector_tile/sprites/swisstopo-sprite@2x.png"
)
with open(sprite_image_file, "rb") as f:
sprite_image = QImage()
sprite_image.loadFromData(f.read())
sprite_definition_file = (
f"{TEST_DATA_DIR}/vector_tile/sprites/swisstopo-sprite@2x.json"
)
with open(sprite_definition_file) as f:
sprite_definition = json.load(f)
context.setSprites(sprite_image, sprite_definition, "basics")
self.assertEqual(context.spriteCategories(), ["basics"])
# swisstopo - lightbasemap - sinkhole
icon_image = [
"match",
["get", "class"],
"sinkhole",
"basics:arrow_brown",
["sinkhole_rock", "sinkhole_scree"],
"basics:arrow_grey",
["sinkhole_ice", "sinkhole_water"],
"basics:arrow_blue",
"",
]
sprite, size, sprite_property, sprite_size_property = (
QgsMapBoxGlStyleConverter.retrieveSpriteAsBase64WithProperties(
icon_image, context
)
)
def strip_base64(s):
pos = 0
while True:
pos = s.find("'base64:", pos)
if pos < 0:
break
end_pos = s.find("'", pos + 1)
assert end_pos > pos
s = s[0:pos] + "'base64:[snip]'" + s[end_pos + 1 :]
pos += len("'base64:")
return s
expected = "CASE WHEN \"class\" IN ('sinkhole') THEN 'base64:[snip]' WHEN \"class\" IN ('sinkhole_rock','sinkhole_scree') THEN 'base64:[snip]' WHEN \"class\" IN ('sinkhole_ice','sinkhole_water') THEN 'base64:[snip]' ELSE '' END"
self.assertEqual(strip_base64(sprite_property), expected)
if __name__ == "__main__":
unittest.main()