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