1 /*************************************************************************** 2 qgsmapboxglstyleconverter.h 3 -------------------------------------- 4 Date : September 2020 5 Copyright : (C) 2020 by Nyall Dawson 6 Email : nyall dot dawson at gmail dot com 7 *************************************************************************** 8 * * 9 * This program is free software; you can redistribute it and/or modify * 10 * it under the terms of the GNU General Public License as published by * 11 * the Free Software Foundation; either version 2 of the License, or * 12 * (at your option) any later version. * 13 * * 14 ***************************************************************************/ 15 16 #ifndef QGSMAPBOXGLSTYLECONVERTER_H 17 #define QGSMAPBOXGLSTYLECONVERTER_H 18 19 #include "qgis_core.h" 20 #include "qgis_sip.h" 21 #include "qgsproperty.h" 22 #include <QVariantMap> 23 #include <memory> 24 25 class QgsVectorTileRenderer; 26 class QgsVectorTileLabeling; 27 class QgsVectorTileBasicRendererStyle; 28 class QgsVectorTileBasicLabelingStyle; 29 30 /** 31 * Context for a MapBox GL style conversion operation. 32 * \warning This is private API only, and may change in future QGIS versions 33 * \ingroup core 34 * \since QGIS 3.16 35 */ 36 class CORE_EXPORT QgsMapBoxGlStyleConversionContext 37 { 38 public: 39 40 /** 41 * Pushes a \a warning message generated during the conversion. 42 */ 43 void pushWarning( const QString &warning ); 44 45 /** 46 * Returns a list of warning messages generated during the conversion. 47 */ warnings()48 QStringList warnings() const { return mWarnings; } 49 50 /** 51 * Clears the list of warning messages. 52 */ clearWarnings()53 void clearWarnings() { mWarnings.clear(); } 54 55 /** 56 * Returns the target unit type. 57 * 58 * By default this is QgsUnitTypes::RenderPixels in order to exactly match the original 59 * style rendering. But rendering in pixels can cause issues on hidpi displays or with print 60 * layouts, so setting a target unit of QgsUnitTypes::Millimeters or another real-world unit 61 * type is often more appropriate. 62 * 63 * \see setTargetUnit() 64 */ 65 QgsUnitTypes::RenderUnit targetUnit() const; 66 67 /** 68 * Sets the target unit type. 69 * 70 * By default this is QgsUnitTypes::RenderPixels in order to exactly match the original 71 * style rendering. But rendering in pixels can cause issues on hidpi displays or with print 72 * layouts, so setting a target unit of QgsUnitTypes::Millimeters or another real-world unit 73 * type is often more appropriate. 74 * 75 * If setting to a non-pixel unit, be sure to call setPixelSizeConversionFactor() in order 76 * to setup an appropriate pixel-to-unit conversion factor to scale converted sizes 77 * using. E.g. if the target unit is millimeters, the size conversion factor should be 78 * set to a pixel-to-millimeter value. 79 * 80 * \see targetUnit() 81 */ 82 void setTargetUnit( QgsUnitTypes::RenderUnit targetUnit ); 83 84 /** 85 * Returns the pixel size conversion factor, used to scale the original pixel sizes 86 * when converting styles. 87 * 88 * \see setPixelSizeConversionFactor() 89 */ 90 double pixelSizeConversionFactor() const; 91 92 /** 93 * Sets the pixel size conversion factor, used to scale the original pixel sizes 94 * when converting styles. 95 * 96 * \see pixelSizeConversionFactor() 97 */ 98 void setPixelSizeConversionFactor( double sizeConversionFactor ); 99 100 /** 101 * Returns the sprite image to use during conversion, or an invalid image if this is not set. 102 * 103 * \see spriteDefinitions() 104 * \see setSprites() 105 */ 106 QImage spriteImage() const; 107 108 /** 109 * Returns the sprite definitions to use during conversion. 110 * 111 * \see spriteImage() 112 * \see setSprites() 113 */ 114 QVariantMap spriteDefinitions() const; 115 116 /** 117 * Sets the sprite \a image and \a definitions JSON to use during conversion. 118 * 119 * \see spriteImage() 120 * \see spriteDefinitions() 121 */ 122 void setSprites( const QImage &image, const QVariantMap &definitions ); 123 124 /** 125 * Sets the sprite \a image and \a definitions JSON string to use during conversion. 126 * 127 * \see spriteImage() 128 * \see spriteDefinitions() 129 */ 130 void setSprites( const QImage &image, const QString &definitions ); 131 132 /** 133 * Returns the layer ID of the layer currently being converted. 134 * 135 * \see setLayerId() 136 */ 137 QString layerId() const; 138 139 /** 140 * Sets the layer ID of the layer currently being converted. 141 * 142 * \see layerId() 143 */ 144 void setLayerId( const QString &value ); 145 146 private: 147 148 QStringList mWarnings; 149 150 QString mLayerId; 151 152 QgsUnitTypes::RenderUnit mTargetUnit = QgsUnitTypes::RenderPixels; 153 154 double mSizeConversionFactor = 1.0; 155 156 QImage mSpriteImage; 157 QVariantMap mSpriteDefinitions; 158 }; 159 160 /** 161 * \ingroup core 162 * \brief Handles conversion of MapBox GL styles to QGIS vector tile renderers and labeling 163 * settings. 164 * 165 * Conversions are performed by calling convert() with either a JSON map or JSON 166 * string value, and then retrieving the results by calling renderer() or labeling() 167 * respectively. 168 * 169 * \since QGIS 3.16 170 */ 171 class CORE_EXPORT QgsMapBoxGlStyleConverter 172 { 173 public: 174 175 /** 176 * Constructor for QgsMapBoxGlStyleConverter. 177 */ 178 QgsMapBoxGlStyleConverter(); 179 180 //! QgsMapBoxGlStyleConverter cannot be copied 181 QgsMapBoxGlStyleConverter( const QgsMapBoxGlStyleConverter &other ) = delete; 182 //! QgsMapBoxGlStyleConverter cannot be copied 183 QgsMapBoxGlStyleConverter &operator=( const QgsMapBoxGlStyleConverter &other ) = delete; 184 185 ~QgsMapBoxGlStyleConverter(); 186 187 //! Result of conversion 188 enum Result 189 { 190 Success = 0, //!< Conversion was successful 191 NoLayerList = 1, //!< No layer list was found in JSON input 192 }; 193 194 /** 195 * Converts a JSON \a style map, and returns the resultant status of the conversion. 196 * 197 * If an error occurs during conversion then a descriptive error message can be retrieved 198 * by calling errorMessage(). 199 * 200 * After conversion, the resultant labeling and style rules can be retrieved by calling 201 * renderer() or labeling() respectively. 202 * 203 * The optional \a context argument can be set to use a specific context during the conversion. 204 */ 205 Result convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context = nullptr ); 206 207 /** 208 * Converts a JSON \a style string, and returns the resultant status of the conversion. 209 * 210 * If an error occurs during conversion then a descriptive error message can be retrieved 211 * by calling errorMessage(). 212 * 213 * After conversion, the resultant labeling and style rules can be retrieved by calling 214 * renderer() or labeling() respectively. 215 * 216 * The optional \a context argument can be set to use a specific context during the conversion. 217 */ 218 Result convert( const QString &style, QgsMapBoxGlStyleConversionContext *context = nullptr ); 219 220 /** 221 * Returns a descriptive error message if an error was encountered during the style conversion, 222 * or an empty string if no error was encountered. 223 * 224 * \see warnings() 225 */ errorMessage()226 QString errorMessage() const { return mError; } 227 228 /** 229 * Returns a list of user-friendly warnings generated during the conversion, e.g. as a result 230 * of MapBox GL style settings which cannot be translated to QGIS styles. 231 * 232 * \see errorMessage() 233 */ warnings()234 QStringList warnings() const { return mWarnings; } 235 236 /** 237 * Returns a new instance of a vector tile renderer representing the converted style, 238 * or NULLPTR if the style could not be converted successfully. 239 */ 240 QgsVectorTileRenderer *renderer() const SIP_FACTORY; 241 242 /** 243 * Returns a new instance of a vector tile labeling representing the converted style, 244 * or NULLPTR if the style could not be converted successfully. 245 */ 246 QgsVectorTileLabeling *labeling() const SIP_FACTORY; 247 248 protected: 249 250 /** 251 * Property types, for interpolated value conversion 252 * \warning This is private API only, and may change in future QGIS versions 253 */ 254 enum PropertyType 255 { 256 Color, //!< Color property 257 Numeric, //!< Numeric property (e.g. line width, text size) 258 Opacity, //!< Opacity property 259 Point, //!< Point/offset property 260 }; 261 262 /** 263 * Parse list of \a layers from JSON. 264 * \warning This is private API only, and may change in future QGIS versions 265 */ 266 void parseLayers( const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context = nullptr ); 267 268 /** 269 * Parses a fill layer. 270 * 271 * \warning This is private API only, and may change in future QGIS versions 272 * 273 * \param jsonLayer fill layer to parse 274 * \param style generated QGIS vector tile style 275 * \param context conversion context 276 * \param isBackgroundStyle set to TRUE if the layer should be parsed as background layer 277 * \returns TRUE if the layer was successfully parsed. 278 */ 279 static bool parseFillLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style SIP_OUT, QgsMapBoxGlStyleConversionContext &context, bool isBackgroundStyle = false ); 280 281 /** 282 * Parses a line layer. 283 * 284 * \warning This is private API only, and may change in future QGIS versions 285 * 286 * \param jsonLayer line layer to parse 287 * \param style generated QGIS vector tile style 288 * \param context conversion context 289 * \returns TRUE if the layer was successfully parsed. 290 */ 291 static bool parseLineLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); 292 293 /** 294 * Parses a circle layer. 295 * 296 * \warning This is private API only, and may change in future QGIS versions 297 * 298 * \param jsonLayer circle layer to parse 299 * \param style generated QGIS vector tile style 300 * \param context conversion context 301 * \returns TRUE if the layer was successfully parsed. 302 */ 303 static bool parseCircleLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); 304 305 /** 306 * Parses a symbol layer as renderer or labeling. 307 * 308 * \warning This is private API only, and may change in future QGIS versions 309 * 310 * \param jsonLayer symbol layer to parse 311 * \param rendererStyle generated QGIS vector tile style 312 * \param hasRenderer will be set to TRUE if symbol layer generated a renderer style 313 * \param labelingStyle generated QGIS vector tile labeling 314 * \param hasLabeling will be set to TRUE if symbol layer generated a labeling style 315 * \param context conversion context 316 */ 317 static void parseSymbolLayer( const QVariantMap &jsonLayer, 318 QgsVectorTileBasicRendererStyle &rendererStyle SIP_OUT, 319 bool &hasRenderer SIP_OUT, 320 QgsVectorTileBasicLabelingStyle &labelingStyle SIP_OUT, 321 bool &hasLabeling SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); 322 323 /** 324 * Parses a symbol layer as a renderer 325 * 326 * \warning This is private API only, and may change in future QGIS versions 327 * 328 * \param jsonLayer fill layer to parse 329 * \param rendererStyle generated QGIS vector tile style 330 * \param context conversion context 331 * 332 * \returns TRUE if symbol layer was converted to renderer 333 */ 334 static bool parseSymbolLayerAsRenderer( const QVariantMap &jsonLayer, 335 QgsVectorTileBasicRendererStyle &rendererStyle SIP_OUT, QgsMapBoxGlStyleConversionContext &context ); 336 337 /** 338 * Parses a color value which is interpolated by zoom range. 339 * 340 * \param json definition of color interpolation 341 * \param context conversion context 342 * \param defaultColor optional storage for a reasonable "default" color representing the overall property. 343 * 344 * \returns QgsProperty representing interpolation settings 345 */ 346 static QgsProperty parseInterpolateColorByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, QColor *defaultColor SIP_OUT = nullptr ); 347 348 /** 349 * Parses a numeric value which is interpolated by zoom range. 350 * 351 * \param json definition of interpolation 352 * \param context conversion context 353 * \param multiplier optional multiplication factor 354 * \param defaultNumber optional storage for a reasonable "default" number representing the overall property. 355 * 356 * \returns QgsProperty representing interpolation settings 357 */ 358 static QgsProperty parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1, double *defaultNumber SIP_OUT = nullptr ); 359 360 /** 361 * Interpolates opacity with either scale_linear() or scale_exp() (depending on base value). 362 * For \a json with intermediate stops it uses parseOpacityStops() function. 363 * It uses QGIS set_color_part() function to set alpha component of color. 364 * 365 * \warning This is private API only, and may change in future QGIS versions 366 */ 367 static QgsProperty parseInterpolateOpacityByZoom( const QVariantMap &json, int maxOpacity, QgsMapBoxGlStyleConversionContext *contextPtr = 0 ); 368 369 /** 370 * Takes values from stops and uses either scale_linear() or scale_exp() functions 371 * to interpolate alpha component of color. 372 * 373 * \warning This is private API only, and may change in future QGIS versions 374 */ 375 static QString parseOpacityStops( double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context ); 376 377 /** 378 * Interpolates a point/offset with either scale_linear() or scale_exp() (depending on base value). 379 * For \a json with intermediate stops it uses parsePointStops() function. 380 * 381 * \warning This is private API only, and may change in future QGIS versions 382 */ 383 static QgsProperty parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1, QPointF *defaultPoint SIP_OUT = nullptr ); 384 385 /** 386 * Interpolates a string by zoom. 387 * For \a json with intermediate stops it uses parseStringStops() function. 388 * 389 * \warning This is private API only, and may change in future QGIS versions 390 */ 391 static QgsProperty parseInterpolateStringByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, 392 const QVariantMap &conversionMap, 393 QString *defaultString SIP_OUT = nullptr ); 394 395 396 /** 397 * Takes values from stops and uses either scale_linear() or scale_exp() functions 398 * to interpolate point/offset values. 399 * 400 * \warning This is private API only, and may change in future QGIS versions 401 */ 402 static QString parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1 ); 403 404 /** 405 * Takes numerical arrays from stops. 406 * 407 * \warning This is private API only, and may change in future QGIS versions 408 */ 409 static QString parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1 ); 410 411 /** 412 * Parses a list of interpolation stops 413 * 414 * \param base interpolation exponent base 415 * \param stops definition of interpolation stops 416 * \param multiplier optional multiplication factor 417 * \param context conversion context 418 */ 419 static QString parseStops( double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context ); 420 421 /** 422 * Parses a list of interpolation stops containing string values. 423 * 424 * \param stops definition of interpolation stops 425 * \param context conversion context 426 * \param conversionMap map of input string to output expression value 427 * \param defaultString reasonable default value taken from stops 428 * 429 * \returns converted expression 430 */ 431 static QString parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, 432 const QVariantMap &conversionMap, 433 QString *defaultString SIP_OUT = nullptr ); 434 435 436 /** 437 * Parses and converts a value list (e.g. an interpolate list). 438 * 439 * \warning This is private API only, and may change in future QGIS versions 440 */ 441 static QgsProperty parseValueList( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1, 442 int maxOpacity = 255, QColor *defaultColor SIP_OUT = nullptr, double *defaultNumber SIP_OUT = nullptr ); 443 444 445 /** 446 * Parses and converts a match function value list. 447 * 448 * \warning This is private API only, and may change in future QGIS versions 449 */ 450 static QgsProperty parseMatchList( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1, 451 int maxOpacity = 255, QColor *defaultColor SIP_OUT = nullptr, double *defaultNumber SIP_OUT = nullptr ); 452 453 /** 454 * Interpolates a list which starts with the interpolate function. 455 * 456 * \warning This is private API only, and may change in future QGIS versions 457 */ 458 static QgsProperty parseInterpolateListByZoom( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier = 1, 459 int maxOpacity = 255, QColor *defaultColor SIP_OUT = nullptr, double *defaultNumber SIP_OUT = nullptr ); 460 461 /** 462 * Converts an expression representing a color to a string (can be color string or an expression where a color is expected) 463 * \param colorExpression the color expression 464 * \param context the style conversion context 465 * \returns the QGIS expression string 466 * since QGIS 3.22 467 */ 468 static QString parseColorExpression( const QVariant &colorExpression, QgsMapBoxGlStyleConversionContext &context ); 469 470 /** 471 * Parses a \a color in one of these supported formats: 472 * 473 * - \c \#fff or \c \#ffffff 474 * - ``hsl(30, 19%, 90%)`` or ``hsla(30, 19%, 90%, 0.4)`` 475 * - ``rgb(10, 20, 30)`` or ``rgba(10, 20, 30, 0.5)`` 476 * 477 * Returns an invalid color if the color could not be parsed. 478 * 479 * \warning This is private API only, and may change in future QGIS versions 480 */ 481 static QColor parseColor( const QVariant &color, QgsMapBoxGlStyleConversionContext &context ); 482 483 /** 484 * Takes a QColor object and returns HSLA components in required format for QGIS color_hsla() expression function. 485 * \param color input color 486 * \param hue an integer value from 0 to 360 487 * \param saturation an integer value from 0 to 100 488 * \param lightness an integer value from 0 to 100 489 * \param alpha an integer value from 0 (completely transparent) to 255 (opaque). 490 * 491 * \warning This is private API only, and may change in future QGIS versions 492 */ 493 static void colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha ); 494 495 /** 496 * Generates an interpolation for values between \a valueMin and \a valueMax, scaled between the 497 * ranges \a zoomMin to \a zoomMax. 498 * 499 * \warning This is private API only, and may change in future QGIS versions 500 */ 501 static QString interpolateExpression( double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier = 1, QgsMapBoxGlStyleConversionContext *contextPtr = 0 ); 502 503 /** 504 * Converts a value to Qt::PenCapStyle enum from JSON value. 505 * 506 * \warning This is private API only, and may change in future QGIS versions 507 */ 508 static Qt::PenCapStyle parseCapStyle( const QString &style ); 509 510 /** 511 * Converts a value to Qt::PenJoinStyle enum from JSON value. 512 * 513 * \warning This is private API only, and may change in future QGIS versions 514 */ 515 static Qt::PenJoinStyle parseJoinStyle( const QString &style ); 516 517 /** 518 * Converts a MapBox GL expression to a QGIS expression. 519 * 520 * \warning This is private API only, and may change in future QGIS versions 521 */ 522 static QString parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected = false ); 523 524 /** 525 * Retrieves the sprite image with the specified \a name, taken from the specified \a context. 526 * 527 * The \a context must have valid sprite definitions and images set via QgsMapBoxGlStyleConversionContext::setSprites() 528 * prior to conversion. 529 */ 530 static QImage retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize ); 531 532 /** 533 * Retrieves the sprite image with the specified \a name, taken from the specified \a context as a base64 encoded value 534 * 535 * The \a context must have valid sprite definitions and images set via QgsMapBoxGlStyleConversionContext::setSprites() 536 * prior to conversion. 537 */ 538 static QString retrieveSpriteAsBase64( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty ); 539 540 private: 541 542 #ifdef SIP_RUN 543 QgsMapBoxGlStyleConverter( const QgsMapBoxGlStyleConverter &other ); 544 #endif 545 546 static QString parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected = false ); 547 548 static QString parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context ); 549 550 /** 551 * Checks if interpolation bottom/top values are numeric values 552 * \param bottomVariant bottom value 553 * \param topVariant top value 554 * \param bottom out: bottom value converted to double 555 * \param top out: top value converted to double 556 * \return true if both bottom and top value are numeric. False else (e.g. if one of the values is an expression) 557 */ 558 static bool numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top ); 559 560 QString mError; 561 QStringList mWarnings; 562 563 std::unique_ptr< QgsVectorTileRenderer > mRenderer; 564 std::unique_ptr< QgsVectorTileLabeling > mLabeling; 565 566 }; 567 568 #endif // QGSMAPBOXGLSTYLECONVERTER_H 569