1 /***************************************************************************
2   qgsmapboxglstyleconverter.cpp
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 
17 /*
18  * Ported from original work by Martin Dobias, and extended by the MapTiler team!
19  */
20 
21 #include "qgsmapboxglstyleconverter.h"
22 #include "qgsvectortilebasicrenderer.h"
23 #include "qgsvectortilebasiclabeling.h"
24 #include "qgssymbollayer.h"
25 #include "qgssymbollayerutils.h"
26 #include "qgslogger.h"
27 #include "qgsfillsymbollayer.h"
28 #include "qgslinesymbollayer.h"
29 #include "qgsfontutils.h"
30 #include "qgsjsonutils.h"
31 #include "qgspainteffect.h"
32 #include "qgseffectstack.h"
33 #include "qgsblureffect.h"
34 #include "qgsmarkersymbollayer.h"
35 #include "qgstextbackgroundsettings.h"
36 #include "qgsfillsymbol.h"
37 #include "qgsmarkersymbol.h"
38 #include "qgslinesymbol.h"
39 
40 #include <QBuffer>
41 #include <QRegularExpression>
42 
QgsMapBoxGlStyleConverter()43 QgsMapBoxGlStyleConverter::QgsMapBoxGlStyleConverter()
44 {
45 }
46 
convert(const QVariantMap & style,QgsMapBoxGlStyleConversionContext * context)47 QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context )
48 {
49   mError.clear();
50   mWarnings.clear();
51   if ( style.contains( QStringLiteral( "layers" ) ) )
52   {
53     parseLayers( style.value( QStringLiteral( "layers" ) ).toList(), context );
54   }
55   else
56   {
57     mError = QObject::tr( "Could not find layers list in JSON" );
58     return NoLayerList;
59   }
60   return Success;
61 }
62 
convert(const QString & style,QgsMapBoxGlStyleConversionContext * context)63 QgsMapBoxGlStyleConverter::Result QgsMapBoxGlStyleConverter::convert( const QString &style, QgsMapBoxGlStyleConversionContext *context )
64 {
65   return convert( QgsJsonUtils::parseJson( style ).toMap(), context );
66 }
67 
68 QgsMapBoxGlStyleConverter::~QgsMapBoxGlStyleConverter() = default;
69 
parseLayers(const QVariantList & layers,QgsMapBoxGlStyleConversionContext * context)70 void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context )
71 {
72   std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
73   if ( !context )
74   {
75     tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
76     context = tmpContext.get();
77   }
78 
79   QList<QgsVectorTileBasicRendererStyle> rendererStyles;
80   QList<QgsVectorTileBasicLabelingStyle> labelingStyles;
81 
82   QgsVectorTileBasicRendererStyle rendererBackgroundStyle;
83   bool hasRendererBackgroundStyle = false;
84 
85   for ( const QVariant &layer : layers )
86   {
87     const QVariantMap jsonLayer = layer.toMap();
88 
89     const QString layerType = jsonLayer.value( QStringLiteral( "type" ) ).toString();
90     if ( layerType == QLatin1String( "background" ) )
91     {
92       hasRendererBackgroundStyle = parseFillLayer( jsonLayer, rendererBackgroundStyle, *context, true );
93       if ( hasRendererBackgroundStyle )
94       {
95         rendererBackgroundStyle.setStyleName( layerType );
96         rendererBackgroundStyle.setLayerName( layerType );
97         rendererBackgroundStyle.setFilterExpression( QString() );
98         rendererBackgroundStyle.setEnabled( true );
99       }
100       continue;
101     }
102 
103     const QString styleId = jsonLayer.value( QStringLiteral( "id" ) ).toString();
104     context->setLayerId( styleId );
105     const QString layerName = jsonLayer.value( QStringLiteral( "source-layer" ) ).toString();
106 
107     const int minZoom = jsonLayer.value( QStringLiteral( "minzoom" ), QStringLiteral( "-1" ) ).toInt();
108     const int maxZoom = jsonLayer.value( QStringLiteral( "maxzoom" ), QStringLiteral( "-1" ) ).toInt();
109 
110     const bool enabled = jsonLayer.value( QStringLiteral( "visibility" ) ).toString() != QLatin1String( "none" );
111 
112     QString filterExpression;
113     if ( jsonLayer.contains( QStringLiteral( "filter" ) ) )
114     {
115       filterExpression = parseExpression( jsonLayer.value( QStringLiteral( "filter" ) ).toList(), *context );
116     }
117 
118     QgsVectorTileBasicRendererStyle rendererStyle;
119     QgsVectorTileBasicLabelingStyle labelingStyle;
120 
121     bool hasRendererStyle = false;
122     bool hasLabelingStyle = false;
123     if ( layerType == QLatin1String( "fill" ) )
124     {
125       hasRendererStyle = parseFillLayer( jsonLayer, rendererStyle, *context );
126     }
127     else if ( layerType == QLatin1String( "line" ) )
128     {
129       hasRendererStyle = parseLineLayer( jsonLayer, rendererStyle, *context );
130     }
131     else if ( layerType == QLatin1String( "circle" ) )
132     {
133       hasRendererStyle = parseCircleLayer( jsonLayer, rendererStyle, *context );
134     }
135     else if ( layerType == QLatin1String( "symbol" ) )
136     {
137       parseSymbolLayer( jsonLayer, rendererStyle, hasRendererStyle, labelingStyle, hasLabelingStyle, *context );
138     }
139     else
140     {
141       mWarnings << QObject::tr( "%1: Skipping unknown layer type %2" ).arg( context->layerId(), layerType );
142       QgsDebugMsg( mWarnings.constLast() );
143       continue;
144     }
145 
146     if ( hasRendererStyle )
147     {
148       rendererStyle.setStyleName( styleId );
149       rendererStyle.setLayerName( layerName );
150       rendererStyle.setFilterExpression( filterExpression );
151       rendererStyle.setMinZoomLevel( minZoom );
152       rendererStyle.setMaxZoomLevel( maxZoom );
153       rendererStyle.setEnabled( enabled );
154       rendererStyles.append( rendererStyle );
155     }
156 
157     if ( hasLabelingStyle )
158     {
159       labelingStyle.setStyleName( styleId );
160       labelingStyle.setLayerName( layerName );
161       labelingStyle.setFilterExpression( filterExpression );
162       labelingStyle.setMinZoomLevel( minZoom );
163       labelingStyle.setMaxZoomLevel( maxZoom );
164       labelingStyle.setEnabled( enabled );
165       labelingStyles.append( labelingStyle );
166     }
167 
168     mWarnings.append( context->warnings() );
169     context->clearWarnings();
170   }
171 
172   if ( hasRendererBackgroundStyle )
173     rendererStyles.prepend( rendererBackgroundStyle );
174 
175   mRenderer = std::make_unique< QgsVectorTileBasicRenderer >();
176   QgsVectorTileBasicRenderer *renderer = dynamic_cast< QgsVectorTileBasicRenderer *>( mRenderer.get() );
177   renderer->setStyles( rendererStyles );
178 
179   mLabeling = std::make_unique< QgsVectorTileBasicLabeling >();
180   QgsVectorTileBasicLabeling *labeling = dynamic_cast< QgsVectorTileBasicLabeling * >( mLabeling.get() );
181   labeling->setStyles( labelingStyles );
182 }
183 
parseFillLayer(const QVariantMap & jsonLayer,QgsVectorTileBasicRendererStyle & style,QgsMapBoxGlStyleConversionContext & context,bool isBackgroundStyle)184 bool QgsMapBoxGlStyleConverter::parseFillLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context, bool isBackgroundStyle )
185 {
186   if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
187   {
188     context.pushWarning( QObject::tr( "%1: Layer has no paint property, skipping" ).arg( jsonLayer.value( QStringLiteral( "id" ) ).toString() ) );
189     return false;
190   }
191 
192   const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
193 
194   QgsPropertyCollection ddProperties;
195   QgsPropertyCollection ddRasterProperties;
196 
197   std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsFillSymbol >() );
198 
199   // fill color
200   QColor fillColor;
201   if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-color" ) : QStringLiteral( "fill-color" ) ) )
202   {
203     const QVariant jsonFillColor = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-color" ) : QStringLiteral( "fill-color" ) );
204     switch ( jsonFillColor.type() )
205     {
206       case QVariant::Map:
207         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonFillColor.toMap(), context, &fillColor ) );
208         break;
209 
210       case QVariant::List:
211       case QVariant::StringList:
212         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonFillColor.toList(), PropertyType::Color, context, 1, 255, &fillColor ) );
213         break;
214 
215       case QVariant::String:
216         fillColor = parseColor( jsonFillColor.toString(), context );
217         break;
218 
219       default:
220       {
221         context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonFillColor.type() ) ) );
222         break;
223       }
224     }
225   }
226   else
227   {
228     // defaults to #000000
229     fillColor = QColor( 0, 0, 0 );
230   }
231 
232   QColor fillOutlineColor;
233   if ( !isBackgroundStyle )
234   {
235     if ( !jsonPaint.contains( QStringLiteral( "fill-outline-color" ) ) )
236     {
237       if ( fillColor.isValid() )
238         fillOutlineColor = fillColor;
239 
240       // match fill color data defined property when active
241       if ( ddProperties.isActive( QgsSymbolLayer::PropertyFillColor ) )
242         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor,  ddProperties.property( QgsSymbolLayer::PropertyFillColor ) );
243     }
244     else
245     {
246       const QVariant jsonFillOutlineColor = jsonPaint.value( QStringLiteral( "fill-outline-color" ) );
247       switch ( jsonFillOutlineColor.type() )
248       {
249         case QVariant::Map:
250           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateColorByZoom( jsonFillOutlineColor.toMap(), context, &fillOutlineColor ) );
251           break;
252 
253         case QVariant::List:
254         case QVariant::StringList:
255           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonFillOutlineColor.toList(), PropertyType::Color, context, 1, 255, &fillOutlineColor ) );
256           break;
257 
258         case QVariant::String:
259           fillOutlineColor = parseColor( jsonFillOutlineColor.toString(), context );
260           break;
261 
262         default:
263           context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-outline-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonFillOutlineColor.type() ) ) );
264           break;
265       }
266     }
267   }
268 
269   double fillOpacity = -1.0;
270   double rasterOpacity = -1.0;
271   if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-opacity" ) : QStringLiteral( "fill-opacity" ) ) )
272   {
273     const QVariant jsonFillOpacity = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-opacity" ) : QStringLiteral( "fill-opacity" ) );
274     switch ( jsonFillOpacity.type() )
275     {
276       case QVariant::Int:
277       case QVariant::Double:
278         fillOpacity = jsonFillOpacity.toDouble();
279         rasterOpacity = fillOpacity;
280         break;
281 
282       case QVariant::Map:
283         if ( ddProperties.isActive( QgsSymbolLayer::PropertyFillColor ) )
284         {
285           symbol->setDataDefinedProperty( QgsSymbol::PropertyOpacity, parseInterpolateOpacityByZoom( jsonFillOpacity.toMap(), 255, &context ) );
286         }
287         else
288         {
289           ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateOpacityByZoom( jsonFillOpacity.toMap(), fillColor.isValid() ? fillColor.alpha() : 255, &context ) );
290           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonFillOpacity.toMap(), fillOutlineColor.isValid() ? fillOutlineColor.alpha() : 255, &context ) );
291           ddRasterProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseInterpolateByZoom( jsonFillOpacity.toMap(), context, 100, &rasterOpacity ) );
292         }
293         break;
294 
295       case QVariant::List:
296       case QVariant::StringList:
297         if ( ddProperties.isActive( QgsSymbolLayer::PropertyFillColor ) )
298         {
299           symbol->setDataDefinedProperty( QgsSymbol::PropertyOpacity, parseValueList( jsonFillOpacity.toList(), PropertyType::Numeric, context, 100, 100 ) );
300         }
301         else
302         {
303           ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonFillOpacity.toList(), PropertyType::Opacity, context, 1, fillColor.isValid() ? fillColor.alpha() : 255 ) );
304           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonFillOpacity.toList(), PropertyType::Opacity, context, 1, fillOutlineColor.isValid() ? fillOutlineColor.alpha() : 255 ) );
305           ddRasterProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseValueList( jsonFillOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &rasterOpacity ) );
306         }
307         break;
308 
309       default:
310         context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonFillOpacity.type() ) ) );
311         break;
312     }
313   }
314 
315   // fill-translate
316   QPointF fillTranslate;
317   if ( jsonPaint.contains( QStringLiteral( "fill-translate" ) ) )
318   {
319     const QVariant jsonFillTranslate = jsonPaint.value( QStringLiteral( "fill-translate" ) );
320     switch ( jsonFillTranslate.type() )
321     {
322 
323       case QVariant::Map:
324         ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolatePointByZoom( jsonFillTranslate.toMap(), context, context.pixelSizeConversionFactor(), &fillTranslate ) );
325         break;
326 
327       case QVariant::List:
328       case QVariant::StringList:
329         fillTranslate = QPointF( jsonFillTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(),
330                                  jsonFillTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
331         break;
332 
333       default:
334         context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonFillTranslate.type() ) ) );
335         break;
336     }
337   }
338 
339   QgsSimpleFillSymbolLayer *fillSymbol = dynamic_cast< QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) );
340   Q_ASSERT( fillSymbol ); // should not fail since QgsFillSymbol() constructor instantiates a QgsSimpleFillSymbolLayer
341 
342   // set render units
343   symbol->setOutputUnit( context.targetUnit() );
344   fillSymbol->setOutputUnit( context.targetUnit() );
345 
346   if ( !fillTranslate.isNull() )
347   {
348     fillSymbol->setOffset( fillTranslate );
349   }
350   fillSymbol->setOffsetUnit( context.targetUnit() );
351 
352   if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-pattern" ) : QStringLiteral( "fill-pattern" ) ) )
353   {
354     // get fill-pattern to set sprite
355 
356     const QVariant fillPatternJson = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-pattern" ) : QStringLiteral( "fill-pattern" ) );
357 
358     // fill-pattern disabled dillcolor
359     fillColor = QColor();
360     fillOutlineColor = QColor();
361 
362     // fill-pattern can be String or Object
363     // String: {"fill-pattern": "dash-t"}
364     // Object: {"fill-pattern":{"stops":[[11,"wetland8"],[12,"wetland16"]]}}
365 
366     QSize spriteSize;
367     QString spriteProperty, spriteSizeProperty;
368     const QString sprite = retrieveSpriteAsBase64( fillPatternJson, context, spriteSize, spriteProperty, spriteSizeProperty );
369     if ( !sprite.isEmpty() )
370     {
371       // when fill-pattern exists, set and insert QgsRasterFillSymbolLayer
372       QgsRasterFillSymbolLayer *rasterFill = new QgsRasterFillSymbolLayer();
373       rasterFill->setImageFilePath( sprite );
374       rasterFill->setWidth( spriteSize.width() );
375       rasterFill->setWidthUnit( context.targetUnit() );
376       rasterFill->setCoordinateMode( QgsRasterFillSymbolLayer::Viewport );
377 
378       if ( rasterOpacity >= 0 )
379       {
380         rasterFill->setOpacity( rasterOpacity );
381       }
382 
383       if ( !spriteProperty.isEmpty() )
384       {
385         ddRasterProperties.setProperty( QgsSymbolLayer::PropertyFile, QgsProperty::fromExpression( spriteProperty ) );
386         ddRasterProperties.setProperty( QgsSymbolLayer::PropertyWidth, QgsProperty::fromExpression( spriteSizeProperty ) );
387       }
388 
389       rasterFill->setDataDefinedProperties( ddRasterProperties );
390       symbol->appendSymbolLayer( rasterFill );
391     }
392   }
393 
394   fillSymbol->setDataDefinedProperties( ddProperties );
395 
396   if ( fillOpacity != -1 )
397   {
398     symbol->setOpacity( fillOpacity );
399   }
400 
401   if ( fillOutlineColor.isValid() )
402   {
403     fillSymbol->setStrokeColor( fillOutlineColor );
404   }
405   else
406   {
407     fillSymbol->setStrokeStyle( Qt::NoPen );
408   }
409 
410   if ( fillColor.isValid() )
411   {
412     fillSymbol->setFillColor( fillColor );
413   }
414   else
415   {
416     fillSymbol->setBrushStyle( Qt::NoBrush );
417   }
418 
419   style.setGeometryType( QgsWkbTypes::PolygonGeometry );
420   style.setSymbol( symbol.release() );
421   return true;
422 }
423 
parseLineLayer(const QVariantMap & jsonLayer,QgsVectorTileBasicRendererStyle & style,QgsMapBoxGlStyleConversionContext & context)424 bool QgsMapBoxGlStyleConverter::parseLineLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context )
425 {
426   if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
427   {
428     context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
429     return false;
430   }
431 
432   const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
433   if ( jsonPaint.contains( QStringLiteral( "line-pattern" ) ) )
434   {
435     context.pushWarning( QObject::tr( "%1: Skipping unsupported line-pattern property" ).arg( context.layerId() ) );
436     return false;
437   }
438 
439   QgsPropertyCollection ddProperties;
440 
441   // line color
442   QColor lineColor;
443   if ( jsonPaint.contains( QStringLiteral( "line-color" ) ) )
444   {
445     const QVariant jsonLineColor = jsonPaint.value( QStringLiteral( "line-color" ) );
446     switch ( jsonLineColor.type() )
447     {
448       case QVariant::Map:
449         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonLineColor.toMap(), context, &lineColor ) );
450         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, ddProperties.property( QgsSymbolLayer::PropertyFillColor ) );
451         break;
452 
453       case QVariant::List:
454       case QVariant::StringList:
455         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonLineColor.toList(), PropertyType::Color, context, 1, 255, &lineColor ) );
456         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, ddProperties.property( QgsSymbolLayer::PropertyFillColor ) );
457         break;
458 
459       case QVariant::String:
460         lineColor = parseColor( jsonLineColor.toString(), context );
461         break;
462 
463       default:
464         context.pushWarning( QObject::tr( "%1: Skipping unsupported line-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineColor.type() ) ) );
465         break;
466     }
467   }
468   else
469   {
470     // defaults to #000000
471     lineColor = QColor( 0, 0, 0 );
472   }
473 
474 
475   double lineWidth = 1.0;
476   QgsProperty lineWidthProperty;
477   if ( jsonPaint.contains( QStringLiteral( "line-width" ) ) )
478   {
479     const QVariant jsonLineWidth = jsonPaint.value( QStringLiteral( "line-width" ) );
480     switch ( jsonLineWidth.type() )
481     {
482       case QVariant::Int:
483       case QVariant::Double:
484         lineWidth = jsonLineWidth.toDouble() * context.pixelSizeConversionFactor();
485         break;
486 
487       case QVariant::Map:
488         lineWidth = -1;
489         lineWidthProperty = parseInterpolateByZoom( jsonLineWidth.toMap(), context, context.pixelSizeConversionFactor(), &lineWidth );
490         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, lineWidthProperty );
491         break;
492 
493       case QVariant::List:
494       case QVariant::StringList:
495         lineWidthProperty = parseValueList( jsonLineWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &lineWidth );
496         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, lineWidthProperty );
497         break;
498 
499       default:
500         context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineWidth.type() ) ) );
501         break;
502     }
503   }
504 
505   double lineOffset = 0.0;
506   if ( jsonPaint.contains( QStringLiteral( "line-offset" ) ) )
507   {
508     const QVariant jsonLineOffset = jsonPaint.value( QStringLiteral( "line-offset" ) );
509     switch ( jsonLineOffset.type() )
510     {
511       case QVariant::Int:
512       case QVariant::Double:
513         lineOffset = -jsonLineOffset.toDouble() * context.pixelSizeConversionFactor();
514         break;
515 
516       case QVariant::Map:
517         lineWidth = -1;
518         ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolateByZoom( jsonLineOffset.toMap(), context, context.pixelSizeConversionFactor() * -1, &lineOffset ) );
519         break;
520 
521       case QVariant::List:
522       case QVariant::StringList:
523         ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseValueList( jsonLineOffset.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * -1, 255, nullptr, &lineOffset ) );
524         break;
525 
526       default:
527         context.pushWarning( QObject::tr( "%1: Skipping unsupported line-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineOffset.type() ) ) );
528         break;
529     }
530   }
531 
532   double lineOpacity = -1.0;
533   if ( jsonPaint.contains( QStringLiteral( "line-opacity" ) ) )
534   {
535     const QVariant jsonLineOpacity = jsonPaint.value( QStringLiteral( "line-opacity" ) );
536     switch ( jsonLineOpacity.type() )
537     {
538       case QVariant::Int:
539       case QVariant::Double:
540         lineOpacity = jsonLineOpacity.toDouble();
541         break;
542 
543       case QVariant::Map:
544         if ( ddProperties.isActive( QgsSymbolLayer::PropertyStrokeColor ) )
545         {
546           context.pushWarning( QObject::tr( "%1: Could not set opacity of layer, opacity already defined in stroke color" ).arg( context.layerId() ) );
547         }
548         else
549         {
550           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonLineOpacity.toMap(), lineColor.isValid() ? lineColor.alpha() : 255, &context ) );
551         }
552         break;
553 
554       case QVariant::List:
555       case QVariant::StringList:
556         if ( ddProperties.isActive( QgsSymbolLayer::PropertyStrokeColor ) )
557         {
558           context.pushWarning( QObject::tr( "%1: Could not set opacity of layer, opacity already defined in stroke color" ).arg( context.layerId() ) );
559         }
560         else
561         {
562           ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonLineOpacity.toList(), PropertyType::Opacity, context, 1, lineColor.isValid() ? lineColor.alpha() : 255 ) );
563         }
564         break;
565 
566       default:
567         context.pushWarning( QObject::tr( "%1: Skipping unsupported line-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineOpacity.type() ) ) );
568         break;
569     }
570   }
571 
572   QVector< double > dashVector;
573   if ( jsonPaint.contains( QStringLiteral( "line-dasharray" ) ) )
574   {
575     const QVariant jsonLineDashArray = jsonPaint.value( QStringLiteral( "line-dasharray" ) );
576     switch ( jsonLineDashArray.type() )
577     {
578       case QVariant::Map:
579       {
580         QString arrayExpression;
581         if ( !lineWidthProperty.asExpression().isEmpty() )
582         {
583           arrayExpression = QStringLiteral( "array_to_string(array_foreach(%1,@element * (%2)), ';')" ) // skip-keyword-check
584                             .arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, 1 ),
585                                   lineWidthProperty.asExpression() );
586         }
587         else
588         {
589           arrayExpression = QStringLiteral( "array_to_string(%1, ';')" ).arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, lineWidth ) );
590         }
591         ddProperties.setProperty( QgsSymbolLayer::PropertyCustomDash, QgsProperty::fromExpression( arrayExpression ) );
592 
593         const QVariantList dashSource = jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList().first().toList().value( 1 ).toList();
594         for ( const QVariant &v : dashSource )
595         {
596           dashVector << v.toDouble() * lineWidth;
597         }
598         break;
599       }
600 
601       case QVariant::List:
602       case QVariant::StringList:
603       {
604         if ( ( !lineWidthProperty.asExpression().isEmpty() ) )
605         {
606           QString arrayExpression = QStringLiteral( "array_to_string(array_foreach(array(%1),@element * (%2)), ';')" ) // skip-keyword-check
607                                     .arg( jsonLineDashArray.toStringList().join( ',' ),
608                                           lineWidthProperty.asExpression() );
609           ddProperties.setProperty( QgsSymbolLayer::PropertyCustomDash, QgsProperty::fromExpression( arrayExpression ) );
610         }
611         const QVariantList dashSource = jsonLineDashArray.toList();
612         for ( const QVariant &v : dashSource )
613         {
614           dashVector << v.toDouble() * lineWidth;
615         }
616         break;
617       }
618 
619       default:
620         context.pushWarning( QObject::tr( "%1: Skipping unsupported line-dasharray type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineDashArray.type() ) ) );
621         break;
622     }
623   }
624 
625   Qt::PenCapStyle penCapStyle = Qt::FlatCap;
626   Qt::PenJoinStyle penJoinStyle = Qt::MiterJoin;
627   if ( jsonLayer.contains( QStringLiteral( "layout" ) ) )
628   {
629     const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
630     if ( jsonLayout.contains( QStringLiteral( "line-cap" ) ) )
631     {
632       penCapStyle = parseCapStyle( jsonLayout.value( QStringLiteral( "line-cap" ) ).toString() );
633     }
634     if ( jsonLayout.contains( QStringLiteral( "line-join" ) ) )
635     {
636       penJoinStyle = parseJoinStyle( jsonLayout.value( QStringLiteral( "line-join" ) ).toString() );
637     }
638   }
639 
640   std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsLineSymbol >() );
641   QgsSimpleLineSymbolLayer *lineSymbol = dynamic_cast< QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) );
642   Q_ASSERT( lineSymbol ); // should not fail since QgsLineSymbol() constructor instantiates a QgsSimpleLineSymbolLayer
643 
644   // set render units
645   symbol->setOutputUnit( context.targetUnit() );
646   lineSymbol->setOutputUnit( context.targetUnit() );
647   lineSymbol->setPenCapStyle( penCapStyle );
648   lineSymbol->setPenJoinStyle( penJoinStyle );
649   lineSymbol->setDataDefinedProperties( ddProperties );
650   lineSymbol->setOffset( lineOffset );
651   lineSymbol->setOffsetUnit( context.targetUnit() );
652 
653   if ( lineOpacity != -1 )
654   {
655     symbol->setOpacity( lineOpacity );
656   }
657   if ( lineColor.isValid() )
658   {
659     lineSymbol->setColor( lineColor );
660   }
661   if ( lineWidth != -1 )
662   {
663     lineSymbol->setWidth( lineWidth );
664   }
665   if ( !dashVector.empty() )
666   {
667     lineSymbol->setUseCustomDashPattern( true );
668     lineSymbol->setCustomDashVector( dashVector );
669   }
670 
671   style.setGeometryType( QgsWkbTypes::LineGeometry );
672   style.setSymbol( symbol.release() );
673   return true;
674 }
675 
parseCircleLayer(const QVariantMap & jsonLayer,QgsVectorTileBasicRendererStyle & style,QgsMapBoxGlStyleConversionContext & context)676 bool QgsMapBoxGlStyleConverter::parseCircleLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context )
677 {
678   if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
679   {
680     context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
681     return false;
682   }
683 
684   const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
685   QgsPropertyCollection ddProperties;
686 
687   // circle color
688   QColor circleFillColor;
689   if ( jsonPaint.contains( QStringLiteral( "circle-color" ) ) )
690   {
691     const QVariant jsonCircleColor = jsonPaint.value( QStringLiteral( "circle-color" ) );
692     switch ( jsonCircleColor.type() )
693     {
694       case QVariant::Map:
695         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonCircleColor.toMap(), context, &circleFillColor ) );
696         break;
697 
698       case QVariant::List:
699       case QVariant::StringList:
700         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleColor.toList(), PropertyType::Color, context, 1, 255, &circleFillColor ) );
701         break;
702 
703       case QVariant::String:
704         circleFillColor = parseColor( jsonCircleColor.toString(), context );
705         break;
706 
707       default:
708         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleColor.type() ) ) );
709         break;
710     }
711   }
712   else
713   {
714     // defaults to #000000
715     circleFillColor = QColor( 0, 0, 0 );
716   }
717 
718   // circle radius
719   double circleDiameter = 10.0;
720   if ( jsonPaint.contains( QStringLiteral( "circle-radius" ) ) )
721   {
722     const QVariant jsonCircleRadius = jsonPaint.value( QStringLiteral( "circle-radius" ) );
723     switch ( jsonCircleRadius.type() )
724     {
725       case QVariant::Int:
726       case QVariant::Double:
727         circleDiameter = jsonCircleRadius.toDouble() * context.pixelSizeConversionFactor() * 2;
728         break;
729 
730       case QVariant::Map:
731         circleDiameter = -1;
732         ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseInterpolateByZoom( jsonCircleRadius.toMap(), context, context.pixelSizeConversionFactor() * 2, &circleDiameter ) );
733         break;
734 
735       case QVariant::List:
736       case QVariant::StringList:
737         ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseValueList( jsonCircleRadius.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * 2, 255, nullptr, &circleDiameter ) );
738         break;
739 
740       default:
741         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-radius type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleRadius.type() ) ) );
742         break;
743     }
744   }
745 
746   double circleOpacity = -1.0;
747   if ( jsonPaint.contains( QStringLiteral( "circle-opacity" ) ) )
748   {
749     const QVariant jsonCircleOpacity = jsonPaint.value( QStringLiteral( "circle-opacity" ) );
750     switch ( jsonCircleOpacity.type() )
751     {
752       case QVariant::Int:
753       case QVariant::Double:
754         circleOpacity = jsonCircleOpacity.toDouble();
755         break;
756 
757       case QVariant::Map:
758         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateOpacityByZoom( jsonCircleOpacity.toMap(), circleFillColor.isValid() ? circleFillColor.alpha() : 255, &context ) );
759         break;
760 
761       case QVariant::List:
762       case QVariant::StringList:
763         ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleOpacity.toList(), PropertyType::Opacity, context, 1, circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) );
764         break;
765 
766       default:
767         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleOpacity.type() ) ) );
768         break;
769     }
770   }
771   if ( ( circleOpacity != -1 ) && circleFillColor.isValid() )
772   {
773     circleFillColor.setAlphaF( circleOpacity );
774   }
775 
776   // circle stroke color
777   QColor circleStrokeColor;
778   if ( jsonPaint.contains( QStringLiteral( "circle-stroke-color" ) ) )
779   {
780     const QVariant jsonCircleStrokeColor = jsonPaint.value( QStringLiteral( "circle-stroke-color" ) );
781     switch ( jsonCircleStrokeColor.type() )
782     {
783       case QVariant::Map:
784         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateColorByZoom( jsonCircleStrokeColor.toMap(), context, &circleStrokeColor ) );
785         break;
786 
787       case QVariant::List:
788       case QVariant::StringList:
789         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeColor.toList(), PropertyType::Color, context, 1, 255, &circleStrokeColor ) );
790         break;
791 
792       case QVariant::String:
793         circleStrokeColor = parseColor( jsonCircleStrokeColor.toString(), context );
794         break;
795 
796       default:
797         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeColor.type() ) ) );
798         break;
799     }
800   }
801 
802   // circle stroke width
803   double circleStrokeWidth = -1.0;
804   if ( jsonPaint.contains( QStringLiteral( "circle-stroke-width" ) ) )
805   {
806     const QVariant circleStrokeWidthJson = jsonPaint.value( QStringLiteral( "circle-stroke-width" ) );
807     switch ( circleStrokeWidthJson.type() )
808     {
809       case QVariant::Int:
810       case QVariant::Double:
811         circleStrokeWidth = circleStrokeWidthJson.toDouble() * context.pixelSizeConversionFactor();
812         break;
813 
814       case QVariant::Map:
815         circleStrokeWidth = -1.0;
816         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateByZoom( circleStrokeWidthJson.toMap(), context, context.pixelSizeConversionFactor(), &circleStrokeWidth ) );
817         break;
818 
819       case QVariant::List:
820       case QVariant::StringList:
821         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseValueList( circleStrokeWidthJson.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &circleStrokeWidth ) );
822         break;
823 
824       default:
825         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( circleStrokeWidthJson.type() ) ) );
826         break;
827     }
828   }
829 
830   double circleStrokeOpacity = -1.0;
831   if ( jsonPaint.contains( QStringLiteral( "circle-stroke-opacity" ) ) )
832   {
833     const QVariant jsonCircleStrokeOpacity = jsonPaint.value( QStringLiteral( "circle-stroke-opacity" ) );
834     switch ( jsonCircleStrokeOpacity.type() )
835     {
836       case QVariant::Int:
837       case QVariant::Double:
838         circleStrokeOpacity = jsonCircleStrokeOpacity.toDouble();
839         break;
840 
841       case QVariant::Map:
842         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonCircleStrokeOpacity.toMap(), circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255, &context ) );
843         break;
844 
845       case QVariant::List:
846       case QVariant::StringList:
847         ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeOpacity.toList(), PropertyType::Opacity, context, 1, circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) );
848         break;
849 
850       default:
851         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeOpacity.type() ) ) );
852         break;
853     }
854   }
855   if ( ( circleStrokeOpacity != -1 ) && circleStrokeColor.isValid() )
856   {
857     circleStrokeColor.setAlphaF( circleStrokeOpacity );
858   }
859 
860   // translate
861   QPointF circleTranslate;
862   if ( jsonPaint.contains( QStringLiteral( "circle-translate" ) ) )
863   {
864     const QVariant jsonCircleTranslate = jsonPaint.value( QStringLiteral( "circle-translate" ) );
865     switch ( jsonCircleTranslate.type() )
866     {
867 
868       case QVariant::Map:
869         ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolatePointByZoom( jsonCircleTranslate.toMap(), context, context.pixelSizeConversionFactor(), &circleTranslate ) );
870         break;
871 
872       case QVariant::List:
873       case QVariant::StringList:
874         circleTranslate = QPointF( jsonCircleTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(),
875                                    jsonCircleTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
876         break;
877 
878       default:
879         context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleTranslate.type() ) ) );
880         break;
881     }
882   }
883 
884   std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsMarkerSymbol >() );
885   QgsSimpleMarkerSymbolLayer *markerSymbolLayer = dynamic_cast< QgsSimpleMarkerSymbolLayer * >( symbol->symbolLayer( 0 ) );
886   Q_ASSERT( markerSymbolLayer );
887 
888   // set render units
889   symbol->setOutputUnit( context.targetUnit() );
890   symbol->setDataDefinedProperties( ddProperties );
891 
892   if ( !circleTranslate.isNull() )
893   {
894     markerSymbolLayer->setOffset( circleTranslate );
895     markerSymbolLayer->setOffsetUnit( context.targetUnit() );
896   }
897 
898   if ( circleFillColor.isValid() )
899   {
900     markerSymbolLayer->setFillColor( circleFillColor );
901   }
902   if ( circleDiameter != -1 )
903   {
904     markerSymbolLayer->setSize( circleDiameter );
905     markerSymbolLayer->setSizeUnit( context.targetUnit() );
906   }
907   if ( circleStrokeColor.isValid() )
908   {
909     markerSymbolLayer->setStrokeColor( circleStrokeColor );
910   }
911   if ( circleStrokeWidth != -1 )
912   {
913     markerSymbolLayer->setStrokeWidth( circleStrokeWidth );
914     markerSymbolLayer->setStrokeWidthUnit( context.targetUnit() );
915   }
916 
917   style.setGeometryType( QgsWkbTypes::PointGeometry );
918   style.setSymbol( symbol.release() );
919   return true;
920 }
921 
parseSymbolLayer(const QVariantMap & jsonLayer,QgsVectorTileBasicRendererStyle & renderer,bool & hasRenderer,QgsVectorTileBasicLabelingStyle & labelingStyle,bool & hasLabeling,QgsMapBoxGlStyleConversionContext & context)922 void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &renderer, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context )
923 {
924   hasLabeling = false;
925   hasRenderer = false;
926 
927   if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
928   {
929     context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
930     return;
931   }
932   const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
933   if ( !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
934   {
935     hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
936     return;
937   }
938 
939   if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
940   {
941     context.pushWarning( QObject::tr( "%1: Style layer has no paint property, skipping" ).arg( context.layerId() ) );
942     return;
943   }
944   const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
945 
946   QgsPropertyCollection ddLabelProperties;
947 
948   double textSize = 16.0 * context.pixelSizeConversionFactor();
949   QgsProperty textSizeProperty;
950   if ( jsonLayout.contains( QStringLiteral( "text-size" ) ) )
951   {
952     const QVariant jsonTextSize = jsonLayout.value( QStringLiteral( "text-size" ) );
953     switch ( jsonTextSize.type() )
954     {
955       case QVariant::Int:
956       case QVariant::Double:
957         textSize = jsonTextSize.toDouble() * context.pixelSizeConversionFactor();
958         break;
959 
960       case QVariant::Map:
961         textSize = -1;
962         textSizeProperty = parseInterpolateByZoom( jsonTextSize.toMap(), context, context.pixelSizeConversionFactor(), &textSize );
963 
964         break;
965 
966       case QVariant::List:
967       case QVariant::StringList:
968         textSize = -1;
969         textSizeProperty = parseValueList( jsonTextSize.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &textSize );
970         break;
971 
972       default:
973         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextSize.type() ) ) );
974         break;
975     }
976 
977     if ( textSizeProperty )
978     {
979       ddLabelProperties.setProperty( QgsPalLayerSettings::Size, textSizeProperty );
980     }
981   }
982 
983   // a rough average of ems to character count conversion for a variety of fonts
984   constexpr double EM_TO_CHARS = 2.0;
985 
986   double textMaxWidth = -1;
987   if ( jsonLayout.contains( QStringLiteral( "text-max-width" ) ) )
988   {
989     const QVariant jsonTextMaxWidth = jsonLayout.value( QStringLiteral( "text-max-width" ) );
990     switch ( jsonTextMaxWidth.type() )
991     {
992       case QVariant::Int:
993       case QVariant::Double:
994         textMaxWidth = jsonTextMaxWidth.toDouble() * EM_TO_CHARS;
995         break;
996 
997       case QVariant::Map:
998         ddLabelProperties.setProperty( QgsPalLayerSettings::AutoWrapLength, parseInterpolateByZoom( jsonTextMaxWidth.toMap(), context, EM_TO_CHARS, &textMaxWidth ) );
999         break;
1000 
1001       case QVariant::List:
1002       case QVariant::StringList:
1003         ddLabelProperties.setProperty( QgsPalLayerSettings::AutoWrapLength, parseValueList( jsonTextMaxWidth.toList(), PropertyType::Numeric, context, EM_TO_CHARS, 255, nullptr, &textMaxWidth ) );
1004         break;
1005 
1006       default:
1007         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-max-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextMaxWidth.type() ) ) );
1008         break;
1009     }
1010   }
1011   else
1012   {
1013     // defaults to 10
1014     textMaxWidth = 10 * EM_TO_CHARS;
1015   }
1016 
1017   double textLetterSpacing = -1;
1018   if ( jsonLayout.contains( QStringLiteral( "text-letter-spacing" ) ) )
1019   {
1020     const QVariant jsonTextLetterSpacing = jsonLayout.value( QStringLiteral( "text-letter-spacing" ) );
1021     switch ( jsonTextLetterSpacing.type() )
1022     {
1023       case QVariant::Int:
1024       case QVariant::Double:
1025         textLetterSpacing = jsonTextLetterSpacing.toDouble();
1026         break;
1027 
1028       case QVariant::Map:
1029         ddLabelProperties.setProperty( QgsPalLayerSettings::FontLetterSpacing, parseInterpolateByZoom( jsonTextLetterSpacing.toMap(), context, 1, &textLetterSpacing ) );
1030         break;
1031 
1032       case QVariant::List:
1033       case QVariant::StringList:
1034         ddLabelProperties.setProperty( QgsPalLayerSettings::FontLetterSpacing, parseValueList( jsonTextLetterSpacing.toList(), PropertyType::Numeric, context, 1, 255, nullptr, &textLetterSpacing ) );
1035         break;
1036 
1037       default:
1038         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-letter-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextLetterSpacing.type() ) ) );
1039         break;
1040     }
1041   }
1042 
1043   QFont textFont;
1044   bool foundFont = false;
1045   QString fontName;
1046   QString fontStyleName;
1047 
1048   if ( jsonLayout.contains( QStringLiteral( "text-font" ) ) )
1049   {
1050     auto splitFontFamily = []( const QString & fontName, QString & family, QString & style ) -> bool
1051     {
1052       const QStringList textFontParts = fontName.split( ' ' );
1053       for ( int i = 1; i < textFontParts.size(); ++i )
1054       {
1055         const QString candidateFontName = textFontParts.mid( 0, i ).join( ' ' );
1056         const QString candidateFontStyle = textFontParts.mid( i ).join( ' ' );
1057         if ( QgsFontUtils::fontFamilyHasStyle( candidateFontName, candidateFontStyle ) )
1058         {
1059           family = candidateFontName;
1060           style = candidateFontStyle;
1061           return true;
1062         }
1063       }
1064 
1065       if ( QFontDatabase().hasFamily( fontName ) )
1066       {
1067         // the json isn't following the spec correctly!!
1068         family = fontName;
1069         style.clear();
1070         return true;
1071       }
1072       return false;
1073     };
1074 
1075     const QVariant jsonTextFont = jsonLayout.value( QStringLiteral( "text-font" ) );
1076     if ( jsonTextFont.type() != QVariant::List && jsonTextFont.type() != QVariant::StringList && jsonTextFont.type() != QVariant::String
1077          && jsonTextFont.type() != QVariant::Map )
1078     {
1079       context.pushWarning( QObject::tr( "%1: Skipping unsupported text-font type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextFont.type() ) ) );
1080     }
1081     else
1082     {
1083       switch ( jsonTextFont.type() )
1084       {
1085         case QVariant::List:
1086         case QVariant::StringList:
1087           fontName = jsonTextFont.toList().value( 0 ).toString();
1088           break;
1089 
1090         case QVariant::String:
1091           fontName = jsonTextFont.toString();
1092           break;
1093 
1094         case QVariant::Map:
1095         {
1096           QString familyCaseString = QStringLiteral( "CASE " );
1097           QString styleCaseString = QStringLiteral( "CASE " );
1098           QString fontFamily;
1099           const QVariantList stops = jsonTextFont.toMap().value( QStringLiteral( "stops" ) ).toList();
1100 
1101           bool error = false;
1102           for ( int i = 0; i < stops.length() - 1; ++i )
1103           {
1104             // bottom zoom and value
1105             const QVariant bz = stops.value( i ).toList().value( 0 );
1106             const QString bv = stops.value( i ).toList().value( 1 ).type() == QVariant::String ? stops.value( i ).toList().value( 1 ).toString() : stops.value( i ).toList().value( 1 ).toList().value( 0 ).toString();
1107             if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
1108             {
1109               context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1110               error = true;
1111               break;
1112             }
1113 
1114             // top zoom
1115             const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
1116             if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
1117             {
1118               context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1119               error = true;
1120               break;
1121             }
1122 
1123             if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1124             {
1125               familyCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1126                                                   "THEN %3 " ).arg( bz.toString(),
1127                                                       tz.toString(),
1128                                                       QgsExpression::quotedValue( fontFamily ) );
1129               styleCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1130                                                  "THEN %3 " ).arg( bz.toString(),
1131                                                      tz.toString(),
1132                                                      QgsExpression::quotedValue( fontStyleName ) );
1133             }
1134             else
1135             {
1136               context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1137             }
1138           }
1139           if ( error )
1140             break;
1141 
1142           const QString bv = stops.constLast().toList().value( 1 ).type() == QVariant::String ? stops.constLast().toList().value( 1 ).toString() : stops.constLast().toList().value( 1 ).toList().value( 0 ).toString();
1143           if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1144           {
1145             familyCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontFamily ) );
1146             styleCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontStyleName ) );
1147           }
1148           else
1149           {
1150             context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1151           }
1152 
1153           ddLabelProperties.setProperty( QgsPalLayerSettings::Family, QgsProperty::fromExpression( familyCaseString ) );
1154           ddLabelProperties.setProperty( QgsPalLayerSettings::FontStyle, QgsProperty::fromExpression( styleCaseString ) );
1155 
1156           foundFont = true;
1157           fontName = fontFamily;
1158 
1159           break;
1160         }
1161 
1162         default:
1163           break;
1164       }
1165 
1166       QString fontFamily;
1167       if ( splitFontFamily( fontName, fontFamily, fontStyleName ) )
1168       {
1169         textFont = QFont( fontFamily );
1170         if ( !fontStyleName.isEmpty() )
1171           textFont.setStyleName( fontStyleName );
1172         foundFont = true;
1173       }
1174     }
1175   }
1176   else
1177   {
1178     // Defaults to ["Open Sans Regular","Arial Unicode MS Regular"].
1179     if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Open Sans" ), QStringLiteral( "Regular" ) ) )
1180     {
1181       fontName = QStringLiteral( "Open Sans" );
1182       textFont = QFont( fontName );
1183       textFont.setStyleName( QStringLiteral( "Regular" ) );
1184       fontStyleName = QStringLiteral( "Regular" );
1185       foundFont = true;
1186     }
1187     else if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Arial Unicode MS" ), QStringLiteral( "Regular" ) ) )
1188     {
1189       fontName = QStringLiteral( "Arial Unicode MS" );
1190       textFont = QFont( fontName );
1191       textFont.setStyleName( QStringLiteral( "Regular" ) );
1192       fontStyleName = QStringLiteral( "Regular" );
1193       foundFont = true;
1194     }
1195     else
1196     {
1197       fontName = QStringLiteral( "Open Sans, Arial Unicode MS" );
1198     }
1199   }
1200   if ( !foundFont && !fontName.isEmpty() )
1201   {
1202     context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), fontName ) );
1203   }
1204 
1205   // text color
1206   QColor textColor;
1207   if ( jsonPaint.contains( QStringLiteral( "text-color" ) ) )
1208   {
1209     const QVariant jsonTextColor = jsonPaint.value( QStringLiteral( "text-color" ) );
1210     switch ( jsonTextColor.type() )
1211     {
1212       case QVariant::Map:
1213         ddLabelProperties.setProperty( QgsPalLayerSettings::Color, parseInterpolateColorByZoom( jsonTextColor.toMap(), context, &textColor ) );
1214         break;
1215 
1216       case QVariant::List:
1217       case QVariant::StringList:
1218         ddLabelProperties.setProperty( QgsPalLayerSettings::Color, parseValueList( jsonTextColor.toList(), PropertyType::Color, context, 1, 255, &textColor ) );
1219         break;
1220 
1221       case QVariant::String:
1222         textColor = parseColor( jsonTextColor.toString(), context );
1223         break;
1224 
1225       default:
1226         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextColor.type() ) ) );
1227         break;
1228     }
1229   }
1230   else
1231   {
1232     // defaults to #000000
1233     textColor = QColor( 0, 0, 0 );
1234   }
1235 
1236   // buffer color
1237   QColor bufferColor;
1238   if ( jsonPaint.contains( QStringLiteral( "text-halo-color" ) ) )
1239   {
1240     const QVariant jsonBufferColor = jsonPaint.value( QStringLiteral( "text-halo-color" ) );
1241     switch ( jsonBufferColor.type() )
1242     {
1243       case QVariant::Map:
1244         ddLabelProperties.setProperty( QgsPalLayerSettings::BufferColor, parseInterpolateColorByZoom( jsonBufferColor.toMap(), context, &bufferColor ) );
1245         break;
1246 
1247       case QVariant::List:
1248       case QVariant::StringList:
1249         ddLabelProperties.setProperty( QgsPalLayerSettings::BufferColor, parseValueList( jsonBufferColor.toList(), PropertyType::Color, context, 1, 255, &bufferColor ) );
1250         break;
1251 
1252       case QVariant::String:
1253         bufferColor = parseColor( jsonBufferColor.toString(), context );
1254         break;
1255 
1256       default:
1257         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonBufferColor.type() ) ) );
1258         break;
1259     }
1260   }
1261 
1262   double bufferSize = 0.0;
1263   // the pixel based text buffers appear larger when rendered on the web - so automatically scale
1264   // them up when converting to a QGIS style
1265   // (this number is based on trial-and-error comparisons only!)
1266   constexpr double BUFFER_SIZE_SCALE = 2.0;
1267   if ( jsonPaint.contains( QStringLiteral( "text-halo-width" ) ) )
1268   {
1269     const QVariant jsonHaloWidth = jsonPaint.value( QStringLiteral( "text-halo-width" ) );
1270     switch ( jsonHaloWidth.type() )
1271     {
1272       case QVariant::Int:
1273       case QVariant::Double:
1274         bufferSize = jsonHaloWidth.toDouble() * context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE;
1275         break;
1276 
1277       case QVariant::Map:
1278         bufferSize = 1;
1279         ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateByZoom( jsonHaloWidth.toMap(), context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, &bufferSize ) );
1280         break;
1281 
1282       case QVariant::List:
1283       case QVariant::StringList:
1284         bufferSize = 1;
1285         ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseValueList( jsonHaloWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, 255, nullptr, &bufferSize ) );
1286         break;
1287 
1288       default:
1289         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonHaloWidth.type() ) ) );
1290         break;
1291     }
1292   }
1293 
1294   double haloBlurSize = 0;
1295   if ( jsonPaint.contains( QStringLiteral( "text-halo-blur" ) ) )
1296   {
1297     const QVariant jsonTextHaloBlur = jsonPaint.value( QStringLiteral( "text-halo-blur" ) );
1298     switch ( jsonTextHaloBlur.type() )
1299     {
1300       case QVariant::Int:
1301       case QVariant::Double:
1302       {
1303         haloBlurSize = jsonTextHaloBlur.toDouble() * context.pixelSizeConversionFactor();
1304         break;
1305       }
1306 
1307       default:
1308         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-blur type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextHaloBlur.type() ) ) );
1309         break;
1310     }
1311   }
1312 
1313   QgsTextFormat format;
1314   format.setSizeUnit( context.targetUnit() );
1315   if ( textColor.isValid() )
1316     format.setColor( textColor );
1317   if ( textSize >= 0 )
1318     format.setSize( textSize );
1319   if ( foundFont )
1320   {
1321     format.setFont( textFont );
1322     if ( !fontStyleName.isEmpty() )
1323       format.setNamedStyle( fontStyleName );
1324   }
1325   if ( textLetterSpacing > 0 )
1326   {
1327     QFont f = format.font();
1328     f.setLetterSpacing( QFont::AbsoluteSpacing, textLetterSpacing );
1329     format.setFont( f );
1330   }
1331 
1332   if ( bufferSize > 0 )
1333   {
1334     format.buffer().setEnabled( true );
1335     format.buffer().setSize( bufferSize );
1336     format.buffer().setSizeUnit( context.targetUnit() );
1337     format.buffer().setColor( bufferColor );
1338 
1339     if ( haloBlurSize > 0 )
1340     {
1341       QgsEffectStack *stack = new QgsEffectStack();
1342       QgsBlurEffect *blur = new QgsBlurEffect() ;
1343       blur->setEnabled( true );
1344       blur->setBlurUnit( context.targetUnit() );
1345       blur->setBlurLevel( haloBlurSize );
1346       blur->setBlurMethod( QgsBlurEffect::StackBlur );
1347       stack->appendEffect( blur );
1348       stack->setEnabled( true );
1349       format.buffer().setPaintEffect( stack );
1350     }
1351   }
1352 
1353   QgsPalLayerSettings labelSettings;
1354 
1355   if ( textMaxWidth > 0 )
1356   {
1357     labelSettings.autoWrapLength = textMaxWidth;
1358   }
1359 
1360   // convert field name
1361 
1362   auto processLabelField = []( const QString & string, bool & isExpression )->QString
1363   {
1364     // {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
1365     // but if single field is covered in {}, return it directly
1366     const QRegularExpression singleFieldRx( QStringLiteral( "^{([^}]+)}$" ) );
1367     const QRegularExpressionMatch match = singleFieldRx.match( string );
1368     if ( match.hasMatch() )
1369     {
1370       isExpression = false;
1371       return match.captured( 1 );
1372     }
1373 
1374     const QRegularExpression multiFieldRx( QStringLiteral( "(?={[^}]+})" ) );
1375     const QStringList parts = string.split( multiFieldRx );
1376     if ( parts.size() > 1 )
1377     {
1378       isExpression = true;
1379 
1380       QStringList res;
1381       for ( const QString &part : parts )
1382       {
1383         if ( part.isEmpty() )
1384           continue;
1385 
1386         if ( !part.contains( '{' ) )
1387         {
1388           res << QgsExpression::quotedValue( part );
1389           continue;
1390         }
1391 
1392         // part will start at a {field} reference
1393         const QStringList split = part.split( '}' );
1394         res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
1395         if ( !split.at( 1 ).isEmpty() )
1396           res << QgsExpression::quotedValue( split.at( 1 ) );
1397       }
1398       return QStringLiteral( "concat(%1)" ).arg( res.join( ',' ) );
1399     }
1400     else
1401     {
1402       isExpression = false;
1403       return string;
1404     }
1405   };
1406 
1407   if ( jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1408   {
1409     const QVariant jsonTextField = jsonLayout.value( QStringLiteral( "text-field" ) );
1410     switch ( jsonTextField.type() )
1411     {
1412       case QVariant::String:
1413       {
1414         labelSettings.fieldName = processLabelField( jsonTextField.toString(), labelSettings.isExpression );
1415         break;
1416       }
1417 
1418       case QVariant::List:
1419       case QVariant::StringList:
1420       {
1421         const QVariantList textFieldList = jsonTextField.toList();
1422         /*
1423          * e.g.
1424          *     "text-field": ["format",
1425          *                    "foo", { "font-scale": 1.2 },
1426          *                    "bar", { "font-scale": 0.8 }
1427          * ]
1428          */
1429         if ( textFieldList.size() > 2 && textFieldList.at( 0 ).toString() == QLatin1String( "format" ) )
1430         {
1431           QStringList parts;
1432           for ( int i = 1; i < textFieldList.size(); ++i )
1433           {
1434             bool isExpression = false;
1435             const QString part = processLabelField( textFieldList.at( i ).toString(), isExpression );
1436             if ( !isExpression )
1437               parts << QgsExpression::quotedColumnRef( part );
1438             else
1439               parts << part;
1440             // TODO -- we could also translate font color, underline, overline, strikethrough to HTML tags!
1441             i += 1;
1442           }
1443           labelSettings.fieldName = QStringLiteral( "concat(%1)" ).arg( parts.join( ',' ) );
1444           labelSettings.isExpression = true;
1445         }
1446         else
1447         {
1448           /*
1449            * e.g.
1450            *     "text-field": ["to-string", ["get", "name"]]
1451            */
1452           labelSettings.fieldName = parseExpression( textFieldList, context );
1453           labelSettings.isExpression = true;
1454         }
1455         break;
1456       }
1457 
1458       default:
1459         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextField.type() ) ) );
1460         break;
1461     }
1462   }
1463 
1464   if ( jsonLayout.contains( QStringLiteral( "text-transform" ) ) )
1465   {
1466     const QString textTransform = jsonLayout.value( QStringLiteral( "text-transform" ) ).toString();
1467     if ( textTransform == QLatin1String( "uppercase" ) )
1468     {
1469       labelSettings.fieldName = QStringLiteral( "upper(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1470     }
1471     else if ( textTransform == QLatin1String( "lowercase" ) )
1472     {
1473       labelSettings.fieldName = QStringLiteral( "lower(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1474     }
1475     labelSettings.isExpression = true;
1476   }
1477 
1478   labelSettings.placement = QgsPalLayerSettings::OverPoint;
1479   QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::PointGeometry;
1480   if ( jsonLayout.contains( QStringLiteral( "symbol-placement" ) ) )
1481   {
1482     const QString symbolPlacement = jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString();
1483     if ( symbolPlacement == QLatin1String( "line" ) )
1484     {
1485       labelSettings.placement = QgsPalLayerSettings::Curved;
1486       labelSettings.lineSettings().setPlacementFlags( QgsLabeling::OnLine );
1487       geometryType = QgsWkbTypes::LineGeometry;
1488 
1489       if ( jsonLayout.contains( QStringLiteral( "text-rotation-alignment" ) ) )
1490       {
1491         const QString textRotationAlignment = jsonLayout.value( QStringLiteral( "text-rotation-alignment" ) ).toString();
1492         if ( textRotationAlignment == QLatin1String( "viewport" ) )
1493         {
1494           labelSettings.placement = QgsPalLayerSettings::Horizontal;
1495         }
1496       }
1497 
1498       if ( labelSettings.placement == QgsPalLayerSettings::Curved )
1499       {
1500         QPointF textOffset;
1501         QgsProperty textOffsetProperty;
1502         if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1503         {
1504           const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1505 
1506           // units are ems!
1507           switch ( jsonTextOffset.type() )
1508           {
1509             case QVariant::Map:
1510               textOffsetProperty = parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, !textSizeProperty ? textSize : 1.0, &textOffset );
1511               if ( !textSizeProperty )
1512               {
1513                 ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "abs(array_get(%1,1))-%2" ).arg( textOffsetProperty ).arg( textSize ) );
1514               }
1515               else
1516               {
1517                 ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "with_variable('text_size',%2,abs(array_get(%1,1))*@text_size-@text_size)" ).arg( textOffsetProperty.asExpression(), textSizeProperty.asExpression() ) );
1518               }
1519               ddLabelProperties.setProperty( QgsPalLayerSettings::LinePlacementOptions, QStringLiteral( "if(array_get(%1,1)>0,'BL','AL')" ).arg( textOffsetProperty ) );
1520               break;
1521 
1522             case QVariant::List:
1523             case QVariant::StringList:
1524               textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1525                                     jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1526               break;
1527 
1528             default:
1529               context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextOffset.type() ) ) );
1530               break;
1531           }
1532 
1533           if ( !textOffset.isNull() )
1534           {
1535             labelSettings.distUnits = context.targetUnit();
1536             labelSettings.dist = std::abs( textOffset.y() ) - textSize;
1537             labelSettings.lineSettings().setPlacementFlags( textOffset.y() > 0.0 ? QgsLabeling::BelowLine : QgsLabeling::AboveLine );
1538             if ( textSizeProperty && !textOffsetProperty )
1539             {
1540               ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "with_variable('text_size',%2,%1*@text_size-@text_size)" ).arg( std::abs( textOffset.y() / textSize ) ).arg( textSizeProperty.asExpression() ) );
1541             }
1542           }
1543         }
1544 
1545         if ( textOffset.isNull() )
1546         {
1547           labelSettings.lineSettings().setPlacementFlags( QgsLabeling::OnLine );
1548         }
1549       }
1550     }
1551   }
1552 
1553   if ( jsonLayout.contains( QStringLiteral( "text-justify" ) ) )
1554   {
1555     const QVariant jsonTextJustify = jsonLayout.value( QStringLiteral( "text-justify" ) );
1556 
1557     // default is center
1558     QString textAlign = QStringLiteral( "center" );
1559 
1560     const QVariantMap conversionMap
1561     {
1562       { QStringLiteral( "left" ), QStringLiteral( "left" ) },
1563       { QStringLiteral( "center" ), QStringLiteral( "center" ) },
1564       { QStringLiteral( "right" ), QStringLiteral( "right" ) },
1565       { QStringLiteral( "auto" ), QStringLiteral( "follow" ) }
1566     };
1567 
1568     switch ( jsonTextJustify.type() )
1569     {
1570       case QVariant::String:
1571         textAlign = jsonTextJustify.toString();
1572         break;
1573 
1574       case QVariant::List:
1575         ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextJustify.toList(), context, conversionMap, &textAlign ) ) );
1576         break;
1577 
1578       case QVariant::Map:
1579         ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, parseInterpolateStringByZoom( jsonTextJustify.toMap(), context, conversionMap, &textAlign ) );
1580         break;
1581 
1582       default:
1583         context.pushWarning( QObject::tr( "%1: Skipping unsupported text-justify type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextJustify.type() ) ) );
1584         break;
1585     }
1586 
1587     if ( textAlign == QLatin1String( "left" ) )
1588       labelSettings.multilineAlign = QgsPalLayerSettings::MultiLeft;
1589     else if ( textAlign == QLatin1String( "right" ) )
1590       labelSettings.multilineAlign = QgsPalLayerSettings::MultiRight;
1591     else if ( textAlign == QLatin1String( "center" ) )
1592       labelSettings.multilineAlign = QgsPalLayerSettings::MultiCenter;
1593     else if ( textAlign == QLatin1String( "follow" ) )
1594       labelSettings.multilineAlign = QgsPalLayerSettings::MultiFollowPlacement;
1595   }
1596   else
1597   {
1598     labelSettings.multilineAlign = QgsPalLayerSettings::MultiCenter;
1599   }
1600 
1601   if ( labelSettings.placement == QgsPalLayerSettings::OverPoint )
1602   {
1603     if ( jsonLayout.contains( QStringLiteral( "text-anchor" ) ) )
1604     {
1605       const QVariant jsonTextAnchor = jsonLayout.value( QStringLiteral( "text-anchor" ) );
1606       QString textAnchor;
1607 
1608       const QVariantMap conversionMap
1609       {
1610         { QStringLiteral( "center" ), 4 },
1611         { QStringLiteral( "left" ), 5 },
1612         { QStringLiteral( "right" ), 3 },
1613         { QStringLiteral( "top" ), 7 },
1614         { QStringLiteral( "bottom" ), 1 },
1615         { QStringLiteral( "top-left" ), 8 },
1616         { QStringLiteral( "top-right" ), 6 },
1617         { QStringLiteral( "bottom-left" ), 2 },
1618         { QStringLiteral( "bottom-right" ), 0 },
1619       };
1620 
1621       switch ( jsonTextAnchor.type() )
1622       {
1623         case QVariant::String:
1624           textAnchor = jsonTextAnchor.toString();
1625           break;
1626 
1627         case QVariant::List:
1628           ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextAnchor.toList(), context, conversionMap, &textAnchor ) ) );
1629           break;
1630 
1631         case QVariant::Map:
1632           ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, parseInterpolateStringByZoom( jsonTextAnchor.toMap(), context, conversionMap, &textAnchor ) );
1633           break;
1634 
1635         default:
1636           context.pushWarning( QObject::tr( "%1: Skipping unsupported text-anchor type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextAnchor.type() ) ) );
1637           break;
1638       }
1639 
1640       if ( textAnchor == QLatin1String( "center" ) )
1641         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantOver;
1642       else if ( textAnchor == QLatin1String( "left" ) )
1643         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantRight;
1644       else if ( textAnchor == QLatin1String( "right" ) )
1645         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantLeft;
1646       else if ( textAnchor == QLatin1String( "top" ) )
1647         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantBelow;
1648       else if ( textAnchor == QLatin1String( "bottom" ) )
1649         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantAbove;
1650       else if ( textAnchor == QLatin1String( "top-left" ) )
1651         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantBelowRight;
1652       else if ( textAnchor == QLatin1String( "top-right" ) )
1653         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantBelowLeft;
1654       else if ( textAnchor == QLatin1String( "bottom-left" ) )
1655         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantAboveRight;
1656       else if ( textAnchor == QLatin1String( "bottom-right" ) )
1657         labelSettings.quadOffset = QgsPalLayerSettings::QuadrantAboveLeft;
1658     }
1659 
1660     QPointF textOffset;
1661     if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1662     {
1663       const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1664 
1665       // units are ems!
1666       switch ( jsonTextOffset.type() )
1667       {
1668         case QVariant::Map:
1669           ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetXY, parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, textSize, &textOffset ) );
1670           break;
1671 
1672         case QVariant::List:
1673         case QVariant::StringList:
1674           textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1675                                 jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1676           break;
1677 
1678         default:
1679           context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextOffset.type() ) ) );
1680           break;
1681       }
1682 
1683       if ( !textOffset.isNull() )
1684       {
1685         labelSettings.offsetUnits = context.targetUnit();
1686         labelSettings.xOffset = textOffset.x();
1687         labelSettings.yOffset = textOffset.y();
1688       }
1689     }
1690   }
1691 
1692   if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) &&
1693        ( labelSettings.placement == QgsPalLayerSettings::Horizontal || labelSettings.placement == QgsPalLayerSettings::Curved ) )
1694   {
1695     QSize spriteSize;
1696     QString spriteProperty, spriteSizeProperty;
1697     const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1698     if ( !sprite.isEmpty() )
1699     {
1700       QgsRasterMarkerSymbolLayer *markerLayer = new QgsRasterMarkerSymbolLayer( );
1701       markerLayer->setPath( sprite );
1702       markerLayer->setSize( spriteSize.width() );
1703       markerLayer->setSizeUnit( context.targetUnit() );
1704 
1705       if ( !spriteProperty.isEmpty() )
1706       {
1707         QgsPropertyCollection markerDdProperties;
1708         markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1709         markerLayer->setDataDefinedProperties( markerDdProperties );
1710 
1711         ddLabelProperties.setProperty( QgsPalLayerSettings::ShapeSizeX, QgsProperty::fromExpression( spriteSizeProperty ) );
1712       }
1713 
1714       QgsTextBackgroundSettings backgroundSettings;
1715       backgroundSettings.setEnabled( true );
1716       backgroundSettings.setType( QgsTextBackgroundSettings::ShapeMarkerSymbol );
1717       backgroundSettings.setSize( spriteSize );
1718       backgroundSettings.setSizeUnit( context.targetUnit() );
1719       backgroundSettings.setSizeType( QgsTextBackgroundSettings::SizeFixed );
1720       backgroundSettings.setMarkerSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
1721       format.setBackground( backgroundSettings );
1722     }
1723   }
1724 
1725   if ( textSize >= 0 )
1726   {
1727     // TODO -- this probably needs revisiting -- it was copied from the MapTiler code, but may be wrong...
1728     labelSettings.priority = std::min( textSize / ( context.pixelSizeConversionFactor() * 3 ), 10.0 );
1729   }
1730 
1731   labelSettings.setFormat( format );
1732 
1733   // use a low obstacle weight for layers by default -- we'd rather have more labels for these layers, even if placement isn't ideal
1734   labelSettings.obstacleSettings().setFactor( 0.1 );
1735 
1736   labelSettings.setDataDefinedProperties( ddLabelProperties );
1737 
1738   labelingStyle.setGeometryType( geometryType );
1739   labelingStyle.setLabelSettings( labelSettings );
1740 
1741   hasLabeling = true;
1742 
1743   hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
1744 }
1745 
parseSymbolLayerAsRenderer(const QVariantMap & jsonLayer,QgsVectorTileBasicRendererStyle & rendererStyle,QgsMapBoxGlStyleConversionContext & context)1746 bool QgsMapBoxGlStyleConverter::parseSymbolLayerAsRenderer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &rendererStyle, QgsMapBoxGlStyleConversionContext &context )
1747 {
1748   if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
1749   {
1750     context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
1751     return false;
1752   }
1753   const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
1754 
1755   if ( jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString() == QLatin1String( "line" ) && !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1756   {
1757     QgsPropertyCollection ddProperties;
1758 
1759     double spacing = -1.0;
1760     if ( jsonLayout.contains( QStringLiteral( "symbol-spacing" ) ) )
1761     {
1762       const QVariant jsonSpacing = jsonLayout.value( QStringLiteral( "symbol-spacing" ) );
1763       switch ( jsonSpacing.type() )
1764       {
1765         case QVariant::Int:
1766         case QVariant::Double:
1767           spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
1768           break;
1769 
1770         case QVariant::Map:
1771           ddProperties.setProperty( QgsSymbolLayer::PropertyInterval, parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing ) );
1772           break;
1773 
1774         case QVariant::List:
1775         case QVariant::StringList:
1776           ddProperties.setProperty( QgsSymbolLayer::PropertyInterval, parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing ) );
1777           break;
1778 
1779         default:
1780           context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonSpacing.type() ) ) );
1781           break;
1782       }
1783     }
1784     else
1785     {
1786       // defaults to 250
1787       spacing = 250 * context.pixelSizeConversionFactor();
1788     }
1789 
1790     bool rotateMarkers = true;
1791     if ( jsonLayout.contains( QStringLiteral( "icon-rotation-alignment" ) ) )
1792     {
1793       const QString alignment = jsonLayout.value( QStringLiteral( "icon-rotation-alignment" ) ).toString();
1794       if ( alignment == QLatin1String( "map" ) || alignment == QLatin1String( "auto" ) )
1795       {
1796         rotateMarkers = true;
1797       }
1798       else if ( alignment == QLatin1String( "viewport" ) )
1799       {
1800         rotateMarkers = false;
1801       }
1802     }
1803 
1804     QgsPropertyCollection markerDdProperties;
1805     double rotation = 0.0;
1806     if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
1807     {
1808       const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
1809       switch ( jsonIconRotate.type() )
1810       {
1811         case QVariant::Int:
1812         case QVariant::Double:
1813           rotation = jsonIconRotate.toDouble();
1814           break;
1815 
1816         case QVariant::Map:
1817           markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
1818           break;
1819 
1820         case QVariant::List:
1821         case QVariant::StringList:
1822           markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
1823           break;
1824 
1825         default:
1826           context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconRotate.type() ) ) );
1827           break;
1828       }
1829     }
1830 
1831     QgsMarkerLineSymbolLayer *lineSymbol = new QgsMarkerLineSymbolLayer( rotateMarkers, spacing > 0 ? spacing : 1 );
1832     lineSymbol->setOutputUnit( context.targetUnit() );
1833     lineSymbol->setDataDefinedProperties( ddProperties );
1834     if ( spacing < 1 )
1835     {
1836       // if spacing isn't specified, it's a central point marker only
1837       lineSymbol->setPlacement( QgsTemplatedLineSymbolLayerBase::CentralPoint );
1838     }
1839 
1840     QgsRasterMarkerSymbolLayer *markerLayer = new QgsRasterMarkerSymbolLayer( );
1841     QSize spriteSize;
1842     QString spriteProperty, spriteSizeProperty;
1843     const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1844     if ( !sprite.isNull() )
1845     {
1846       markerLayer->setPath( sprite );
1847       markerLayer->setSize( spriteSize.width() );
1848       markerLayer->setSizeUnit( context.targetUnit() );
1849 
1850       if ( !spriteProperty.isEmpty() )
1851       {
1852         markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1853         markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth, QgsProperty::fromExpression( spriteSizeProperty ) );
1854       }
1855     }
1856 
1857     if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1858     {
1859       const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1860       double size = 1.0;
1861       QgsProperty property;
1862       switch ( jsonIconSize.type() )
1863       {
1864         case QVariant::Int:
1865         case QVariant::Double:
1866         {
1867           size = jsonIconSize.toDouble();
1868           if ( !spriteSizeProperty.isEmpty() )
1869           {
1870             markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1871                                             QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
1872           }
1873           break;
1874         }
1875 
1876         case QVariant::Map:
1877           property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
1878           break;
1879 
1880         case QVariant::List:
1881         case QVariant::StringList:
1882         default:
1883           context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconSize.type() ) ) );
1884           break;
1885       }
1886       markerLayer->setSize( size * spriteSize.width() );
1887       if ( !property.expressionString().isEmpty() )
1888       {
1889         if ( !spriteSizeProperty.isEmpty() )
1890         {
1891           markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1892                                           QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
1893         }
1894         else
1895         {
1896           markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1897                                           QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
1898         }
1899       }
1900     }
1901 
1902     markerLayer->setDataDefinedProperties( markerDdProperties );
1903     markerLayer->setAngle( rotation );
1904     lineSymbol->setSubSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
1905 
1906     std::unique_ptr< QgsSymbol > symbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << lineSymbol );
1907 
1908     // set render units
1909     symbol->setOutputUnit( context.targetUnit() );
1910     lineSymbol->setOutputUnit( context.targetUnit() );
1911 
1912     rendererStyle.setGeometryType( QgsWkbTypes::LineGeometry );
1913     rendererStyle.setSymbol( symbol.release() );
1914     return true;
1915   }
1916   else if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) )
1917   {
1918     const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
1919 
1920     QSize spriteSize;
1921     QString spriteProperty, spriteSizeProperty;
1922     const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1923     if ( !sprite.isEmpty() )
1924     {
1925       QgsRasterMarkerSymbolLayer *rasterMarker = new QgsRasterMarkerSymbolLayer( );
1926       rasterMarker->setPath( sprite );
1927       rasterMarker->setSize( spriteSize.width() );
1928       rasterMarker->setSizeUnit( context.targetUnit() );
1929 
1930       QgsPropertyCollection markerDdProperties;
1931       if ( !spriteProperty.isEmpty() )
1932       {
1933         markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1934         markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth, QgsProperty::fromExpression( spriteSizeProperty ) );
1935       }
1936 
1937       if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1938       {
1939         const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1940         double size = 1.0;
1941         QgsProperty property;
1942         switch ( jsonIconSize.type() )
1943         {
1944           case QVariant::Int:
1945           case QVariant::Double:
1946           {
1947             size = jsonIconSize.toDouble();
1948             if ( !spriteSizeProperty.isEmpty() )
1949             {
1950               markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1951                                               QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
1952             }
1953             break;
1954           }
1955 
1956           case QVariant::Map:
1957             property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
1958             break;
1959 
1960           case QVariant::List:
1961           case QVariant::StringList:
1962           default:
1963             context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconSize.type() ) ) );
1964             break;
1965         }
1966         rasterMarker->setSize( size * spriteSize.width() );
1967         if ( !property.expressionString().isEmpty() )
1968         {
1969           if ( !spriteSizeProperty.isEmpty() )
1970           {
1971             markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1972                                             QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
1973           }
1974           else
1975           {
1976             markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1977                                             QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
1978           }
1979         }
1980       }
1981 
1982       double rotation = 0.0;
1983       if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
1984       {
1985         const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
1986         switch ( jsonIconRotate.type() )
1987         {
1988           case QVariant::Int:
1989           case QVariant::Double:
1990             rotation = jsonIconRotate.toDouble();
1991             break;
1992 
1993           case QVariant::Map:
1994             markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
1995             break;
1996 
1997           case QVariant::List:
1998           case QVariant::StringList:
1999             markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2000             break;
2001 
2002           default:
2003             context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconRotate.type() ) ) );
2004             break;
2005         }
2006       }
2007 
2008       double iconOpacity = -1.0;
2009       if ( jsonPaint.contains( QStringLiteral( "icon-opacity" ) ) )
2010       {
2011         const QVariant jsonIconOpacity = jsonPaint.value( QStringLiteral( "icon-opacity" ) );
2012         switch ( jsonIconOpacity.type() )
2013         {
2014           case QVariant::Int:
2015           case QVariant::Double:
2016             iconOpacity = jsonIconOpacity.toDouble();
2017             break;
2018 
2019           case QVariant::Map:
2020             markerDdProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseInterpolateByZoom( jsonIconOpacity.toMap(), context, 100, &iconOpacity ) );
2021             break;
2022 
2023           case QVariant::List:
2024           case QVariant::StringList:
2025             markerDdProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseValueList( jsonIconOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &iconOpacity ) );
2026             break;
2027 
2028           default:
2029             context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconOpacity.type() ) ) );
2030             break;
2031         }
2032       }
2033 
2034       rasterMarker->setDataDefinedProperties( markerDdProperties );
2035       rasterMarker->setAngle( rotation );
2036       if ( iconOpacity >= 0 )
2037         rasterMarker->setOpacity( iconOpacity );
2038 
2039       QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << rasterMarker );
2040       rendererStyle.setSymbol( markerSymbol );
2041       rendererStyle.setGeometryType( QgsWkbTypes::PointGeometry );
2042       return true;
2043     }
2044   }
2045 
2046   return false;
2047 }
2048 
parseInterpolateColorByZoom(const QVariantMap & json,QgsMapBoxGlStyleConversionContext & context,QColor * defaultColor)2049 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateColorByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, QColor *defaultColor )
2050 {
2051   const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2052   const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2053   if ( stops.empty() )
2054     return QgsProperty();
2055 
2056   QString caseString = QStringLiteral( "CASE " );
2057   const QString colorComponent( "color_part(%1,'%2')" );
2058 
2059   for ( int i = 0; i < stops.length() - 1; ++i )
2060   {
2061     // step bottom zoom
2062     const QString bz = stops.at( i ).toList().value( 0 ).toString();
2063     // step top zoom
2064     const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
2065 
2066     const QVariant bcVariant = stops.at( i ).toList().value( 1 );
2067     const QVariant tcVariant = stops.at( i + 1 ).toList().value( 1 );
2068 
2069     const QColor bottomColor = parseColor( bcVariant.toString(), context );
2070     const QColor topColor = parseColor( tcVariant.toString(), context );
2071 
2072     if ( i == 0 && bottomColor.isValid() )
2073     {
2074       int bcHue;
2075       int bcSat;
2076       int bcLight;
2077       int bcAlpha;
2078       colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2079       caseString += QStringLiteral( "WHEN @vector_tile_zoom < %1 THEN color_hsla(%2, %3, %4, %5) " )
2080                     .arg( bz ).arg( bcHue ).arg( bcSat ).arg( bcLight ).arg( bcAlpha );
2081     }
2082 
2083     if ( bottomColor.isValid() && topColor.isValid() )
2084     {
2085       int bcHue;
2086       int bcSat;
2087       int bcLight;
2088       int bcAlpha;
2089       colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2090       int tcHue;
2091       int tcSat;
2092       int tcLight;
2093       int tcAlpha;
2094       colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2095       caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2096                                     "%3, %4, %5, %6) " ).arg( bz, tz,
2097                                         interpolateExpression( bz.toDouble(), tz.toDouble(), bcHue, tcHue, base, 1, &context ),
2098                                         interpolateExpression( bz.toDouble(), tz.toDouble(), bcSat, tcSat, base, 1, &context ),
2099                                         interpolateExpression( bz.toDouble(), tz.toDouble(), bcLight, tcLight, base, 1, &context ),
2100                                         interpolateExpression( bz.toDouble(), tz.toDouble(), bcAlpha, tcAlpha, base, 1, &context ) );
2101     }
2102     else
2103     {
2104       const QString bottomColorExpr = parseColorExpression( bcVariant, context );
2105       const QString topColorExpr = parseColorExpression( tcVariant, context );
2106 
2107       caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2108                                     "%3, %4, %5, %6) " ).arg( bz, tz,
2109                                         interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_hue" ), colorComponent.arg( topColorExpr ).arg( "hsl_hue" ), base, 1, &context ),
2110                                         interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_saturation" ), colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ), base, 1, &context ),
2111                                         interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "lightness" ), colorComponent.arg( topColorExpr ).arg( "lightness" ), base, 1, &context ),
2112                                         interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "alpha" ), colorComponent.arg( topColorExpr ).arg( "alpha" ), base, 1, &context ) );
2113     }
2114   }
2115 
2116   // top color
2117   const QString tz = stops.last().toList().value( 0 ).toString();
2118   const QVariant tcVariant = stops.last().toList().value( 1 );
2119   const QColor topColor = parseColor( stops.last().toList().value( 1 ), context );
2120   if ( topColor.isValid() )
2121   {
2122     int tcHue;
2123     int tcSat;
2124     int tcLight;
2125     int tcAlpha;
2126     colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2127     caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2128                                   "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz ).arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );
2129   }
2130   else
2131   {
2132     const QString topColorExpr = parseColorExpression( tcVariant, context );
2133 
2134     caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2135                                   "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
2136                   .arg( colorComponent.arg( topColorExpr ).arg( "hsl_hue" ) ).arg( colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ) ).arg( colorComponent.arg( topColorExpr ).arg( "lightness" ) ).arg( colorComponent.arg( topColorExpr ).arg( "alpha" ) );
2137   }
2138 
2139   if ( !stops.empty() && defaultColor )
2140     *defaultColor = parseColor( stops.value( 0 ).toList().value( 1 ).toString(), context );
2141 
2142   return QgsProperty::fromExpression( caseString );
2143 }
2144 
parseInterpolateByZoom(const QVariantMap & json,QgsMapBoxGlStyleConversionContext & context,double multiplier,double * defaultNumber)2145 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, double *defaultNumber )
2146 {
2147   const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2148   const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2149   if ( stops.empty() )
2150     return QgsProperty();
2151 
2152   QString scaleExpression;
2153   if ( stops.size() <= 2 )
2154   {
2155     scaleExpression = interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2156                       stops.last().toList().value( 0 ).toDouble(),
2157                       stops.value( 0 ).toList().value( 1 ),
2158                       stops.last().toList().value( 1 ), base, multiplier, &context );
2159   }
2160   else
2161   {
2162     scaleExpression = parseStops( base, stops, multiplier, context );
2163   }
2164 
2165   if ( !stops.empty() && defaultNumber )
2166     *defaultNumber = stops.value( 0 ).toList().value( 1 ).toDouble() * multiplier;
2167 
2168   return QgsProperty::fromExpression( scaleExpression );
2169 }
2170 
parseInterpolateOpacityByZoom(const QVariantMap & json,int maxOpacity,QgsMapBoxGlStyleConversionContext * contextPtr)2171 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateOpacityByZoom( const QVariantMap &json, int maxOpacity, QgsMapBoxGlStyleConversionContext *contextPtr )
2172 {
2173   QgsMapBoxGlStyleConversionContext context;
2174   if ( contextPtr )
2175   {
2176     context = *contextPtr;
2177   }
2178   const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2179   const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2180   if ( stops.empty() )
2181     return QgsProperty();
2182 
2183   QString scaleExpression;
2184   if ( stops.length() <= 2 )
2185   {
2186     const QVariant bv = stops.value( 0 ).toList().value( 1 );
2187     const QVariant tv = stops.last().toList().value( 1 );
2188     double bottom = 0.0;
2189     double top = 0.0;
2190     const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2191     scaleExpression = QStringLiteral( "set_color_part(@symbol_color, 'alpha', %1)" )
2192                       .arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2193                             stops.last().toList().value( 0 ).toDouble(),
2194                             numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2195                             numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ), base, 1, &context ) );
2196   }
2197   else
2198   {
2199     scaleExpression = parseOpacityStops( base, stops, maxOpacity, context );
2200   }
2201   return QgsProperty::fromExpression( scaleExpression );
2202 }
2203 
parseOpacityStops(double base,const QVariantList & stops,int maxOpacity,QgsMapBoxGlStyleConversionContext & context)2204 QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context )
2205 {
2206   QString caseString = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
2207                        .arg( stops.value( 0 ).toList().value( 0 ).toString() )
2208                        .arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );
2209 
2210   for ( int i = 0; i < stops.size() - 1; ++i )
2211   {
2212     const QVariant bv = stops.value( i ).toList().value( 1 );
2213     const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2214     double bottom = 0.0;
2215     double top = 0.0;
2216     const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2217 
2218     caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
2219                                   "THEN set_color_part(@symbol_color, 'alpha', %3)" )
2220                   .arg( stops.value( i ).toList().value( 0 ).toString(),
2221                         stops.value( i + 1 ).toList().value( 0 ).toString(),
2222                         interpolateExpression( stops.value( i ).toList().value( 0 ).toDouble(),
2223                             stops.value( i + 1 ).toList().value( 0 ).toDouble(),
2224                             numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2225                             numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2226                             base, 1, &context ) );
2227   }
2228 
2229   caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
2230                                 "THEN set_color_part(@symbol_color, 'alpha', %2) END" )
2231                 .arg( stops.last().toList().value( 0 ).toString() )
2232                 .arg( stops.last().toList().value( 1 ).toDouble() * maxOpacity );
2233   return caseString;
2234 }
2235 
parseInterpolatePointByZoom(const QVariantMap & json,QgsMapBoxGlStyleConversionContext & context,double multiplier,QPointF * defaultPoint)2236 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, QPointF *defaultPoint )
2237 {
2238   const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2239   const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2240   if ( stops.empty() )
2241     return QgsProperty();
2242 
2243   QString scaleExpression;
2244   if ( stops.size() <= 2 )
2245   {
2246     scaleExpression = QStringLiteral( "array(%1,%2)" ).arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2247                       stops.last().toList().value( 0 ).toDouble(),
2248                       stops.value( 0 ).toList().value( 1 ).toList().value( 0 ),
2249                       stops.last().toList().value( 1 ).toList().value( 0 ), base, multiplier, &context ),
2250                       interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2251                           stops.last().toList().value( 0 ).toDouble(),
2252                           stops.value( 0 ).toList().value( 1 ).toList().value( 1 ),
2253                           stops.last().toList().value( 1 ).toList().value( 1 ), base, multiplier, &context )
2254                                                           );
2255   }
2256   else
2257   {
2258     scaleExpression = parsePointStops( base, stops, context, multiplier );
2259   }
2260 
2261   if ( !stops.empty() && defaultPoint )
2262     *defaultPoint = QPointF( stops.value( 0 ).toList().value( 1 ).toList().value( 0 ).toDouble() * multiplier,
2263                              stops.value( 0 ).toList().value( 1 ).toList().value( 1 ).toDouble() * multiplier );
2264 
2265   return QgsProperty::fromExpression( scaleExpression );
2266 }
2267 
parseInterpolateStringByZoom(const QVariantMap & json,QgsMapBoxGlStyleConversionContext & context,const QVariantMap & conversionMap,QString * defaultString)2268 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateStringByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context,
2269     const QVariantMap &conversionMap, QString *defaultString )
2270 {
2271   const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2272   if ( stops.empty() )
2273     return QgsProperty();
2274 
2275   const QString scaleExpression = parseStringStops( stops, context, conversionMap, defaultString );
2276 
2277   return QgsProperty::fromExpression( scaleExpression );
2278 }
2279 
parsePointStops(double base,const QVariantList & stops,QgsMapBoxGlStyleConversionContext & context,double multiplier)2280 QString QgsMapBoxGlStyleConverter::parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier )
2281 {
2282   QString caseString = QStringLiteral( "CASE " );
2283 
2284   for ( int i = 0; i < stops.length() - 1; ++i )
2285   {
2286     // bottom zoom and value
2287     const QVariant bz = stops.value( i ).toList().value( 0 );
2288     const QVariant bv = stops.value( i ).toList().value( 1 );
2289     if ( bv.type() != QVariant::List && bv.type() != QVariant::StringList )
2290     {
2291       context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( bz.type() ) ) );
2292       return QString();
2293     }
2294 
2295     // top zoom and value
2296     const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2297     const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2298     if ( tv.type() != QVariant::List && tv.type() != QVariant::StringList )
2299     {
2300       context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( tz.type() ) ) );
2301       return QString();
2302     }
2303 
2304     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2305                                   "THEN array(%3,%4)" ).arg( bz.toString(),
2306                                       tz.toString(),
2307                                       interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ), tv.toList().value( 0 ), base, multiplier, &context ),
2308                                       interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ), tv.toList().value( 1 ), base, multiplier, &context ) );
2309   }
2310   caseString += QLatin1String( "END" );
2311   return caseString;
2312 }
2313 
parseArrayStops(const QVariantList & stops,QgsMapBoxGlStyleConversionContext &,double multiplier)2314 QString QgsMapBoxGlStyleConverter::parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &, double multiplier )
2315 {
2316   if ( stops.length() < 2 )
2317     return QString();
2318 
2319   QString caseString = QStringLiteral( "CASE " );
2320 
2321   for ( int i = 0; i < stops.length() - 1; ++i )
2322   {
2323     // bottom zoom and value
2324     const QVariant bz = stops.value( i ).toList().value( 0 );
2325     const QList<QVariant> bv = stops.value( i ).toList().value( 1 ).toList();
2326     QStringList bl;
2327     bool ok = false;
2328     for ( const QVariant &value : bv )
2329     {
2330       const double number = value.toDouble( &ok );
2331       if ( ok )
2332         bl << QString::number( number * multiplier );
2333     }
2334 
2335     // top zoom and value
2336     const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2337     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2338                                   "THEN array(%3) " ).arg( bz.toString(),
2339                                       tz.toString(),
2340                                       bl.join( ',' ) );
2341   }
2342   const QVariant lz = stops.value( stops.length() - 1 ).toList().value( 0 );
2343   const QList<QVariant> lv = stops.value( stops.length() - 1 ).toList().value( 1 ).toList();
2344   QStringList ll;
2345   bool ok = false;
2346   for ( const QVariant &value : lv )
2347   {
2348     const double number = value.toDouble( &ok );
2349     if ( ok )
2350       ll << QString::number( number * multiplier );
2351   }
2352   caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2353                                 "THEN array(%2) " ).arg( lz.toString(),
2354                                     ll.join( ',' ) );
2355   caseString += QLatin1String( "END" );
2356   return caseString;
2357 }
2358 
parseStops(double base,const QVariantList & stops,double multiplier,QgsMapBoxGlStyleConversionContext & context)2359 QString QgsMapBoxGlStyleConverter::parseStops( double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context )
2360 {
2361   QString caseString = QStringLiteral( "CASE " );
2362 
2363   for ( int i = 0; i < stops.length() - 1; ++i )
2364   {
2365     // bottom zoom and value
2366     const QVariant bz = stops.value( i ).toList().value( 0 );
2367     const QVariant bv = stops.value( i ).toList().value( 1 );
2368     if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
2369     {
2370       context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2371       return QString();
2372     }
2373 
2374     // top zoom and value
2375     const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2376     const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2377     if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
2378     {
2379       context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2380       return QString();
2381     }
2382 
2383     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2384                                   "THEN %3 " ).arg( bz.toString(),
2385                                       tz.toString(),
2386                                       interpolateExpression( bz.toDouble(), tz.toDouble(), bv, tv, base, multiplier, &context ) );
2387   }
2388 
2389   const QVariant z = stops.last().toList().value( 0 );
2390   const QVariant v = stops.last().toList().value( 1 );
2391   QString vStr = v.toString();
2392   if ( ( QMetaType::Type )v.type() == QMetaType::QVariantList )
2393   {
2394     vStr = parseExpression( v.toList(), context );
2395     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2396                                   "THEN ( ( %2 ) * %3 ) END" ).arg( z.toString() ).arg( vStr ).arg( multiplier );
2397   }
2398   else
2399   {
2400     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2401                                   "THEN %2 END" ).arg( z.toString() ).arg( v.toDouble() * multiplier );
2402   }
2403 
2404   return caseString;
2405 }
2406 
parseStringStops(const QVariantList & stops,QgsMapBoxGlStyleConversionContext & context,const QVariantMap & conversionMap,QString * defaultString)2407 QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
2408 {
2409   QString caseString = QStringLiteral( "CASE " );
2410 
2411   for ( int i = 0; i < stops.length() - 1; ++i )
2412   {
2413     // bottom zoom and value
2414     const QVariant bz = stops.value( i ).toList().value( 0 );
2415     const QString bv = stops.value( i ).toList().value( 1 ).toString();
2416     if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
2417     {
2418       context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2419       return QString();
2420     }
2421 
2422     // top zoom
2423     const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2424     if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
2425     {
2426       context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2427       return QString();
2428     }
2429 
2430     caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2431                                   "THEN %3 " ).arg( bz.toString(),
2432                                       tz.toString(),
2433                                       QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
2434   }
2435   caseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( conversionMap.value( stops.constLast().toList().value( 1 ).toString(),
2436                 stops.constLast().toList().value( 1 ) ) ) );
2437   if ( defaultString )
2438     *defaultString = stops.constLast().toList().value( 1 ).toString();
2439   return caseString;
2440 }
2441 
parseValueList(const QVariantList & json,QgsMapBoxGlStyleConverter::PropertyType type,QgsMapBoxGlStyleConversionContext & context,double multiplier,int maxOpacity,QColor * defaultColor,double * defaultNumber)2442 QgsProperty QgsMapBoxGlStyleConverter::parseValueList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2443 {
2444   const QString method = json.value( 0 ).toString();
2445   if ( method == QLatin1String( "interpolate" ) )
2446   {
2447     return parseInterpolateListByZoom( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2448   }
2449   else if ( method == QLatin1String( "match" ) )
2450   {
2451     return parseMatchList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2452   }
2453   else
2454   {
2455     return QgsProperty::fromExpression( parseExpression( json, context ) );
2456   }
2457 }
2458 
parseMatchList(const QVariantList & json,QgsMapBoxGlStyleConverter::PropertyType type,QgsMapBoxGlStyleConversionContext & context,double multiplier,int maxOpacity,QColor * defaultColor,double * defaultNumber)2459 QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2460 {
2461   const QString attribute = parseExpression( json.value( 1 ).toList(), context );
2462   if ( attribute.isEmpty() )
2463   {
2464     context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
2465     return QgsProperty();
2466   }
2467 
2468   QString caseString = QStringLiteral( "CASE " );
2469 
2470   for ( int i = 2; i < json.length() - 1; i += 2 )
2471   {
2472     const QVariantList keys = json.value( i ).toList();
2473 
2474     QStringList matchString;
2475     for ( const QVariant &key : keys )
2476     {
2477       matchString << QgsExpression::quotedValue( key );
2478     }
2479 
2480     const QVariant value = json.value( i + 1 );
2481 
2482     QString valueString;
2483     switch ( type )
2484     {
2485       case Color:
2486       {
2487         const QColor color = parseColor( value, context );
2488         valueString = QgsExpression::quotedString( color.name() );
2489         break;
2490       }
2491 
2492       case Numeric:
2493       {
2494         const double v = value.toDouble() * multiplier;
2495         valueString = QString::number( v );
2496         break;
2497       }
2498 
2499       case Opacity:
2500       {
2501         const double v = value.toDouble() * maxOpacity;
2502         valueString = QString::number( v );
2503         break;
2504       }
2505 
2506       case Point:
2507       {
2508         valueString = QStringLiteral( "array(%1,%2)" ).arg( value.toList().value( 0 ).toDouble() * multiplier,
2509                       value.toList().value( 0 ).toDouble() * multiplier );
2510         break;
2511       }
2512 
2513     }
2514 
2515     caseString += QStringLiteral( "WHEN %1 IN (%2) THEN %3 " ).arg( attribute,
2516                   matchString.join( ',' ), valueString );
2517   }
2518 
2519 
2520   QString elseValue;
2521   switch ( type )
2522   {
2523     case Color:
2524     {
2525       const QColor color = parseColor( json.constLast(), context );
2526       if ( defaultColor )
2527         *defaultColor = color;
2528 
2529       elseValue = QgsExpression::quotedString( color.name() );
2530       break;
2531     }
2532 
2533     case Numeric:
2534     {
2535       const double v = json.constLast().toDouble() * multiplier;
2536       if ( defaultNumber )
2537         *defaultNumber = v;
2538       elseValue = QString::number( v );
2539       break;
2540     }
2541 
2542     case Opacity:
2543     {
2544       const double v = json.constLast().toDouble() * maxOpacity;
2545       if ( defaultNumber )
2546         *defaultNumber = v;
2547       elseValue = QString::number( v );
2548       break;
2549     }
2550 
2551     case Point:
2552     {
2553       elseValue = QStringLiteral( "array(%1,%2)" ).arg( json.constLast().toList().value( 0 ).toDouble() * multiplier,
2554                   json.constLast().toList().value( 0 ).toDouble() * multiplier );
2555       break;
2556     }
2557 
2558   }
2559 
2560   caseString += QStringLiteral( "ELSE %1 END" ).arg( elseValue );
2561   return QgsProperty::fromExpression( caseString );
2562 }
2563 
parseInterpolateListByZoom(const QVariantList & json,PropertyType type,QgsMapBoxGlStyleConversionContext & context,double multiplier,int maxOpacity,QColor * defaultColor,double * defaultNumber)2564 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateListByZoom( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2565 {
2566   if ( json.value( 0 ).toString() != QLatin1String( "interpolate" ) )
2567   {
2568     context.pushWarning( QObject::tr( "%1: Could not interpret value list" ).arg( context.layerId() ) );
2569     return QgsProperty();
2570   }
2571 
2572   double base = 1;
2573   const QString technique = json.value( 1 ).toList().value( 0 ).toString();
2574   if ( technique == QLatin1String( "linear" ) )
2575     base = 1;
2576   else if ( technique == QLatin1String( "exponential" ) )
2577     base = json.value( 1 ).toList(). value( 1 ).toDouble();
2578   else if ( technique == QLatin1String( "cubic-bezier" ) )
2579   {
2580     context.pushWarning( QObject::tr( "%1: Cubic-bezier interpolation is not supported, linear used instead." ).arg( context.layerId() ) );
2581     base = 1;
2582   }
2583   else
2584   {
2585     context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation method %2" ).arg( context.layerId(), technique ) );
2586     return QgsProperty();
2587   }
2588 
2589   if ( json.value( 2 ).toList().value( 0 ).toString() != QLatin1String( "zoom" ) )
2590   {
2591     context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation input %2" ).arg( context.layerId(), json.value( 2 ).toString() ) );
2592     return QgsProperty();
2593   }
2594 
2595   //  Convert stops into list of lists
2596   QVariantList stops;
2597   for ( int i = 3; i < json.length(); i += 2 )
2598   {
2599     stops.push_back( QVariantList() << json.value( i ).toString() << json.value( i + 1 ) );
2600   }
2601 
2602   QVariantMap props;
2603   props.insert( QStringLiteral( "stops" ), stops );
2604   props.insert( QStringLiteral( "base" ), base );
2605   switch ( type )
2606   {
2607     case PropertyType::Color:
2608       return parseInterpolateColorByZoom( props, context, defaultColor );
2609 
2610     case PropertyType::Numeric:
2611       return parseInterpolateByZoom( props, context, multiplier, defaultNumber );
2612 
2613     case PropertyType::Opacity:
2614       return parseInterpolateOpacityByZoom( props, maxOpacity, &context );
2615 
2616     case PropertyType::Point:
2617       return parseInterpolatePointByZoom( props, context, multiplier );
2618   }
2619   return QgsProperty();
2620 }
2621 
parseColorExpression(const QVariant & colorExpression,QgsMapBoxGlStyleConversionContext & context)2622 QString QgsMapBoxGlStyleConverter::parseColorExpression( const QVariant &colorExpression, QgsMapBoxGlStyleConversionContext &context )
2623 {
2624   if ( ( QMetaType::Type )colorExpression.type() == QMetaType::QVariantList )
2625   {
2626     return parseExpression( colorExpression.toList(), context, true );
2627   }
2628   return parseValue( colorExpression, context, true );
2629 }
2630 
parseColor(const QVariant & color,QgsMapBoxGlStyleConversionContext & context)2631 QColor QgsMapBoxGlStyleConverter::parseColor( const QVariant &color, QgsMapBoxGlStyleConversionContext &context )
2632 {
2633   if ( color.type() != QVariant::String )
2634   {
2635     context.pushWarning( QObject::tr( "%1: Could not parse non-string color %2, skipping" ).arg( context.layerId(), color.toString() ) );
2636     return QColor();
2637   }
2638 
2639   return QgsSymbolLayerUtils::parseColor( color.toString() );
2640 }
2641 
colorAsHslaComponents(const QColor & color,int & hue,int & saturation,int & lightness,int & alpha)2642 void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
2643 {
2644   hue = std::max( 0, color.hslHue() );
2645   saturation = color.hslSaturation() / 255.0 * 100;
2646   lightness = color.lightness() / 255.0 * 100;
2647   alpha = color.alpha();
2648 }
2649 
interpolateExpression(double zoomMin,double zoomMax,QVariant valueMin,QVariant valueMax,double base,double multiplier,QgsMapBoxGlStyleConversionContext * contextPtr)2650 QString QgsMapBoxGlStyleConverter::interpolateExpression( double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier, QgsMapBoxGlStyleConversionContext *contextPtr )
2651 {
2652   QgsMapBoxGlStyleConversionContext context;
2653   if ( contextPtr )
2654   {
2655     context = *contextPtr;
2656   }
2657 
2658   // special case!
2659   if ( valueMin.canConvert( QMetaType::Double ) && valueMax.canConvert( QMetaType::Double ) )
2660   {
2661     bool minDoubleOk = true;
2662     const double min = valueMin.toDouble( &minDoubleOk );
2663     bool maxDoubleOk = true;
2664     const double max = valueMax.toDouble( &maxDoubleOk );
2665     if ( minDoubleOk && maxDoubleOk && qgsDoubleNear( min, max ) )
2666     {
2667       return QString::number( min * multiplier );
2668     }
2669   }
2670 
2671   QString minValueExpr = valueMin.toString();
2672   QString maxValueExpr = valueMax.toString();
2673   if ( ( QMetaType::Type )valueMin.type() == QMetaType::QVariantList )
2674   {
2675     minValueExpr = parseExpression( valueMin.toList(), context );
2676   }
2677   if ( ( QMetaType::Type )valueMax.type() == QMetaType::QVariantList )
2678   {
2679     maxValueExpr = parseExpression( valueMax.toList(), context );
2680   }
2681 
2682   if ( minValueExpr == maxValueExpr )
2683   {
2684     return minValueExpr;
2685   }
2686 
2687   QString expression;
2688   if ( base == 1 )
2689   {
2690     expression = QStringLiteral( "scale_linear(@vector_tile_zoom,%1,%2,%3,%4)" ).arg( zoomMin )
2691                  .arg( zoomMax )
2692                  .arg( minValueExpr )
2693                  .arg( maxValueExpr );
2694   }
2695   else
2696   {
2697     expression = QStringLiteral( "scale_exp(@vector_tile_zoom,%1,%2,%3,%4,%5)" ).arg( zoomMin )
2698                  .arg( zoomMax )
2699                  .arg( minValueExpr )
2700                  .arg( maxValueExpr )
2701                  .arg( base );
2702   }
2703 
2704   if ( multiplier != 1 )
2705     return QStringLiteral( "%1 * %2" ).arg( expression ).arg( multiplier );
2706   else
2707     return expression;
2708 }
2709 
parseCapStyle(const QString & style)2710 Qt::PenCapStyle QgsMapBoxGlStyleConverter::parseCapStyle( const QString &style )
2711 {
2712   if ( style == QLatin1String( "round" ) )
2713     return Qt::RoundCap;
2714   else if ( style == QLatin1String( "square" ) )
2715     return Qt::SquareCap;
2716   else
2717     return Qt::FlatCap; // "butt" is default
2718 }
2719 
parseJoinStyle(const QString & style)2720 Qt::PenJoinStyle QgsMapBoxGlStyleConverter::parseJoinStyle( const QString &style )
2721 {
2722   if ( style == QLatin1String( "bevel" ) )
2723     return Qt::BevelJoin;
2724   else if ( style == QLatin1String( "round" ) )
2725     return Qt::RoundJoin;
2726   else
2727     return Qt::MiterJoin; // "miter" is default
2728 }
2729 
parseExpression(const QVariantList & expression,QgsMapBoxGlStyleConversionContext & context,bool colorExpected)2730 QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
2731 {
2732   QString op = expression.value( 0 ).toString();
2733   if ( op == QLatin1String( "%" ) && expression.size() >= 3 )
2734   {
2735     return QStringLiteral( "%1 %2 %3" ).arg( parseValue( expression.value( 1 ), context ) ).arg( op ).arg( parseValue( expression.value( 2 ), context ) );
2736   }
2737   else if ( op == QLatin1String( "to-number" ) )
2738   {
2739     return QStringLiteral( "to_real(%1)" ).arg( parseValue( expression.value( 1 ), context ) );
2740   }
2741   if ( op == QLatin1String( "literal" ) )
2742   {
2743     return expression.value( 1 ).toString();
2744   }
2745   else if ( op == QLatin1String( "all" )
2746             || op == QLatin1String( "any" )
2747             || op == QLatin1String( "none" ) )
2748   {
2749     QStringList parts;
2750     for ( int i = 1; i < expression.size(); ++i )
2751     {
2752       const QString part = parseValue( expression.at( i ), context );
2753       if ( part.isEmpty() )
2754       {
2755         context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2756         return QString();
2757       }
2758       parts << part;
2759     }
2760 
2761     if ( op == QLatin1String( "none" ) )
2762       return QStringLiteral( "NOT (%1)" ).arg( parts.join( QLatin1String( ") AND NOT (" ) ) );
2763 
2764     QString operatorString;
2765     if ( op == QLatin1String( "all" ) )
2766       operatorString = QStringLiteral( ") AND (" );
2767     else if ( op == QLatin1String( "any" ) )
2768       operatorString = QStringLiteral( ") OR (" );
2769 
2770     return QStringLiteral( "(%1)" ).arg( parts.join( operatorString ) );
2771   }
2772   else if ( op == '!' )
2773   {
2774     // ! inverts next expression's meaning
2775     QVariantList contraJsonExpr = expression.value( 1 ).toList();
2776     contraJsonExpr[0] = QString( op + contraJsonExpr[0].toString() );
2777     // ['!', ['has', 'level']] -> ['!has', 'level']
2778     return parseKey( contraJsonExpr, context );
2779   }
2780   else if ( op == QLatin1String( "==" )
2781             || op == QLatin1String( "!=" )
2782             || op == QLatin1String( ">=" )
2783             || op == '>'
2784             || op == QLatin1String( "<=" )
2785             || op == '<' )
2786   {
2787     // use IS and NOT IS instead of = and != because they can deal with NULL values
2788     if ( op == QLatin1String( "==" ) )
2789       op = QStringLiteral( "IS" );
2790     else if ( op == QLatin1String( "!=" ) )
2791       op = QStringLiteral( "IS NOT" );
2792     return QStringLiteral( "%1 %2 %3" ).arg( parseKey( expression.value( 1 ), context ),
2793            op, parseValue( expression.value( 2 ), context ) );
2794   }
2795   else if ( op == QLatin1String( "has" ) )
2796   {
2797     return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NOT NULL" );
2798   }
2799   else if ( op == QLatin1String( "!has" ) )
2800   {
2801     return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NULL" );
2802   }
2803   else if ( op == QLatin1String( "in" ) || op == QLatin1String( "!in" ) )
2804   {
2805     const QString key = parseKey( expression.value( 1 ), context );
2806     QStringList parts;
2807     for ( int i = 2; i < expression.size(); ++i )
2808     {
2809       const QString part = parseValue( expression.at( i ), context );
2810       if ( part.isEmpty() )
2811       {
2812         context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2813         return QString();
2814       }
2815       parts << part;
2816     }
2817     if ( op == QLatin1String( "in" ) )
2818       return QStringLiteral( "%1 IN (%2)" ).arg( key, parts.join( QLatin1String( ", " ) ) );
2819     else
2820       return QStringLiteral( "(%1 IS NULL OR %1 NOT IN (%2))" ).arg( key, parts.join( QLatin1String( ", " ) ) );
2821   }
2822   else if ( op == QLatin1String( "get" ) )
2823   {
2824     return parseKey( expression.value( 1 ), context );
2825   }
2826   else if ( op == QLatin1String( "match" ) )
2827   {
2828     const QString attribute = expression.value( 1 ).toList().value( 1 ).toString();
2829 
2830     if ( expression.size() == 5
2831          && expression.at( 3 ).type() == QVariant::Bool && expression.at( 3 ).toBool() == true
2832          && expression.at( 4 ).type() == QVariant::Bool && expression.at( 4 ).toBool() == false )
2833     {
2834       // simple case, make a nice simple expression instead of a CASE statement
2835       if ( expression.at( 2 ).type() == QVariant::List || expression.at( 2 ).type() == QVariant::StringList )
2836       {
2837         QStringList parts;
2838         for ( const QVariant &p : expression.at( 2 ).toList() )
2839         {
2840           parts << parseValue( p, context );
2841         }
2842 
2843         if ( parts.size() > 1 )
2844           return QStringLiteral( "%1 IN (%2)" ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
2845         else
2846           return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ).toList().value( 0 ) );
2847       }
2848       else if ( expression.at( 2 ).type() == QVariant::String || expression.at( 2 ).type() == QVariant::Int
2849                 || expression.at( 2 ).type() == QVariant::Double )
2850       {
2851         return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ) );
2852       }
2853       else
2854       {
2855         context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2856         return QString();
2857       }
2858     }
2859     else
2860     {
2861       QString caseString = QStringLiteral( "CASE " );
2862       for ( int i = 2; i < expression.size() - 2; i += 2 )
2863       {
2864         if ( expression.at( i ).type() == QVariant::List || expression.at( i ).type() == QVariant::StringList )
2865         {
2866           QStringList parts;
2867           for ( const QVariant &p : expression.at( i ).toList() )
2868           {
2869             parts << QgsExpression::quotedValue( p );
2870           }
2871 
2872           if ( parts.size() > 1 )
2873             caseString += QStringLiteral( "WHEN %1 IN (%2) " ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
2874           else
2875             caseString += QStringLiteral( "WHEN %1 " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ).toList().value( 0 ) ) );
2876         }
2877         else if ( expression.at( i ).type() == QVariant::String || expression.at( i ).type() == QVariant::Int
2878                   || expression.at( i ).type() == QVariant::Double )
2879         {
2880           caseString += QStringLiteral( "WHEN (%1) " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ) ) );
2881         }
2882 
2883         caseString += QStringLiteral( "THEN %1 " ).arg( parseValue( expression.at( i + 1 ), context, colorExpected ) );
2884       }
2885       caseString += QStringLiteral( "ELSE %1 END" ).arg( parseValue( expression.last(), context, colorExpected ) );
2886       return caseString;
2887     }
2888   }
2889   else if ( op == QLatin1String( "to-string" ) )
2890   {
2891     return QStringLiteral( "to_string(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
2892   }
2893   else
2894   {
2895     context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2896     return QString();
2897   }
2898 }
2899 
retrieveSprite(const QString & name,QgsMapBoxGlStyleConversionContext & context,QSize & spriteSize)2900 QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
2901 {
2902   if ( context.spriteImage().isNull() )
2903   {
2904     context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2905     return QImage();
2906   }
2907 
2908   const QVariantMap spriteDefinition = context.spriteDefinitions().value( name ).toMap();
2909   if ( spriteDefinition.size() == 0 )
2910   {
2911     context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2912     return QImage();
2913   }
2914 
2915   const QImage sprite = context.spriteImage().copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
2916                         spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
2917                         spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
2918                         spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
2919   if ( sprite.isNull() )
2920   {
2921     context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2922     return QImage();
2923   }
2924 
2925   spriteSize = sprite.size() / spriteDefinition.value( QStringLiteral( "pixelRatio" ) ).toDouble() * context.pixelSizeConversionFactor();
2926   return sprite;
2927 }
2928 
retrieveSpriteAsBase64(const QVariant & value,QgsMapBoxGlStyleConversionContext & context,QSize & spriteSize,QString & spriteProperty,QString & spriteSizeProperty)2929 QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty )
2930 {
2931   QString spritePath;
2932 
2933   auto prepareBase64 = []( const QImage & sprite )
2934   {
2935     QString path;
2936     if ( !sprite.isNull() )
2937     {
2938       QByteArray blob;
2939       QBuffer buffer( &blob );
2940       buffer.open( QIODevice::WriteOnly );
2941       sprite.save( &buffer, "PNG" );
2942       buffer.close();
2943       const QByteArray encoded = blob.toBase64();
2944       path = QString( encoded );
2945       path.prepend( QLatin1String( "base64:" ) );
2946     }
2947     return path;
2948   };
2949 
2950   switch ( value.type() )
2951   {
2952     case QVariant::String:
2953     {
2954       QString spriteName = value.toString();
2955       const QRegularExpression fieldNameMatch( QStringLiteral( "{([^}]+)}" ) );
2956       QRegularExpressionMatch match = fieldNameMatch.match( spriteName );
2957       if ( match.hasMatch() )
2958       {
2959         const QString fieldName = match.captured( 1 );
2960         spriteProperty = QStringLiteral( "CASE" );
2961         spriteSizeProperty = QStringLiteral( "CASE" );
2962 
2963         spriteName.replace( "(", QLatin1String( "\\(" ) );
2964         spriteName.replace( ")", QLatin1String( "\\)" ) );
2965         spriteName.replace( fieldNameMatch, QStringLiteral( "([^\\/\\\\]+)" ) );
2966         const QRegularExpression fieldValueMatch( spriteName );
2967         const QStringList spriteNames = context.spriteDefinitions().keys();
2968         for ( const QString &name : spriteNames )
2969         {
2970           match = fieldValueMatch.match( name );
2971           if ( match.hasMatch() )
2972           {
2973             QSize size;
2974             QString path;
2975             const QString fieldValue = match.captured( 1 );
2976             const QImage sprite = retrieveSprite( name, context, size );
2977             path = prepareBase64( sprite );
2978             if ( spritePath.isEmpty() && !path.isEmpty() )
2979             {
2980               spritePath = path;
2981               spriteSize = size;
2982             }
2983 
2984             spriteProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN '%3'" )
2985                               .arg( fieldName, fieldValue, path );
2986             spriteSizeProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN %3" )
2987                                   .arg( fieldName ).arg( fieldValue ).arg( size.width() );
2988           }
2989         }
2990 
2991         spriteProperty += QLatin1String( " END" );
2992         spriteSizeProperty += QLatin1String( " END" );
2993       }
2994       else
2995       {
2996         spriteProperty.clear();
2997         spriteSizeProperty.clear();
2998         const QImage sprite = retrieveSprite( spriteName, context, spriteSize );
2999         spritePath = prepareBase64( sprite );
3000       }
3001       break;
3002     }
3003 
3004     case QVariant::Map:
3005     {
3006       const QVariantList stops = value.toMap().value( QStringLiteral( "stops" ) ).toList();
3007       if ( stops.size() == 0 )
3008         break;
3009 
3010       QString path;
3011       QSize size;
3012       QImage sprite;
3013 
3014       sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
3015       spritePath = prepareBase64( sprite );
3016 
3017       spriteProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN '%2'" )
3018                        .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3019                        .arg( spritePath );
3020       spriteSizeProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN %2" )
3021                            .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3022                            .arg( spriteSize.width() );
3023 
3024       for ( int i = 0; i < stops.size() - 1; ++i )
3025       {
3026         ;
3027         sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
3028         path = prepareBase64( sprite );
3029 
3030         spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3031                                           "THEN '%3'" )
3032                           .arg( stops.value( i ).toList().value( 0 ).toString(),
3033                                 stops.value( i + 1 ).toList().value( 0 ).toString(),
3034                                 path );
3035         spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3036                                               "THEN %3" )
3037                               .arg( stops.value( i ).toList().value( 0 ).toString(),
3038                                     stops.value( i + 1 ).toList().value( 0 ).toString() )
3039                               .arg( size.width() );
3040       }
3041       sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
3042       path = prepareBase64( sprite );
3043 
3044       spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3045                                         "THEN '%2' END" )
3046                         .arg( stops.last().toList().value( 0 ).toString() )
3047                         .arg( path );
3048       spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3049                                             "THEN %2 END" )
3050                             .arg( stops.last().toList().value( 0 ).toString() )
3051                             .arg( size.width() );
3052       break;
3053     }
3054 
3055     case QVariant::List:
3056     {
3057       const QVariantList json = value.toList();
3058       const QString method = json.value( 0 ).toString();
3059       if ( method != QLatin1String( "match" ) )
3060       {
3061         context.pushWarning( QObject::tr( "%1: Could not interpret sprite value list with method %2" ).arg( context.layerId(), method ) );
3062         break;
3063       }
3064 
3065       const QString attribute = parseExpression( json.value( 1 ).toList(), context );
3066       if ( attribute.isEmpty() )
3067       {
3068         context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
3069         break;
3070       }
3071 
3072       spriteProperty = QStringLiteral( "CASE " );
3073       spriteSizeProperty = QStringLiteral( "CASE " );
3074 
3075       for ( int i = 2; i < json.length() - 1; i += 2 )
3076       {
3077         const QVariantList keys = json.value( i ).toList();
3078 
3079         QStringList matchString;
3080         for ( const QVariant &key : keys )
3081         {
3082           matchString << QgsExpression::quotedValue( key );
3083         }
3084 
3085         const QVariant value = json.value( i + 1 );
3086 
3087         const QImage sprite = retrieveSprite( value.toString(), context, spriteSize );
3088         spritePath = prepareBase64( sprite );
3089 
3090         spriteProperty += QStringLiteral( " WHEN %1 IN (%2) "
3091                                           "THEN '%3' " ).arg( attribute,
3092                                               matchString.join( ',' ),
3093                                               spritePath );
3094 
3095         spriteSizeProperty += QStringLiteral( " WHEN %1 IN (%2) "
3096                                               "THEN %3 " ).arg( attribute,
3097                                                   matchString.join( ',' ) ).arg( spriteSize.width() );
3098       }
3099 
3100       const QImage sprite = retrieveSprite( json.constLast().toString(), context, spriteSize );
3101       spritePath = prepareBase64( sprite );
3102 
3103       spriteProperty += QStringLiteral( "ELSE %1 END" ).arg( spritePath );
3104       spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3105       break;
3106     }
3107 
3108     default:
3109       context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( value.type() ) ) );
3110       break;
3111   }
3112 
3113   return spritePath;
3114 }
3115 
parseValue(const QVariant & value,QgsMapBoxGlStyleConversionContext & context,bool colorExpected)3116 QString QgsMapBoxGlStyleConverter::parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3117 {
3118   QColor c;
3119   switch ( value.type() )
3120   {
3121     case QVariant::List:
3122     case QVariant::StringList:
3123       return parseExpression( value.toList(), context, colorExpected );
3124 
3125     case QVariant::Bool:
3126     case QVariant::String:
3127       if ( colorExpected )
3128       {
3129         QColor c = parseColor( value, context );
3130         if ( c.isValid() )
3131         {
3132           return parseValue( c, context );
3133         }
3134       }
3135       return QgsExpression::quotedValue( value );
3136 
3137     case QVariant::Int:
3138     case QVariant::Double:
3139       return value.toString();
3140 
3141     case QVariant::Color:
3142       c = value.value<QColor>();
3143       return QString( "color_rgba(%1,%2,%3,%4)" ).arg( c.red() ).arg( c.green() ).arg( c.blue() ).arg( c.alpha() );
3144 
3145     default:
3146       context.pushWarning( QObject::tr( "%1: Skipping unsupported expression part" ).arg( context.layerId() ) );
3147       break;
3148   }
3149   return QString();
3150 }
3151 
parseKey(const QVariant & value,QgsMapBoxGlStyleConversionContext & context)3152 QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context )
3153 {
3154   if ( value.toString() == QLatin1String( "$type" ) )
3155   {
3156     return QStringLiteral( "_geom_type" );
3157   }
3158   if ( value.toString() == QLatin1String( "level" ) )
3159   {
3160     return QStringLiteral( "level" );
3161   }
3162   else if ( ( value.type() == QVariant::List && value.toList().size() == 1 ) || value.type() == QVariant::StringList )
3163   {
3164     if ( value.toList().size() > 1 )
3165       return value.toList().at( 1 ).toString();
3166     else
3167     {
3168       QString valueString = value.toList().value( 0 ).toString();
3169       if ( valueString == QLatin1String( "geometry-type" ) )
3170       {
3171         return QStringLiteral( "_geom_type" );
3172       }
3173       return valueString;
3174     }
3175   }
3176   else if ( value.type() == QVariant::List && value.toList().size() > 1 )
3177   {
3178     return parseExpression( value.toList(), context );
3179   }
3180   return QgsExpression::quotedColumnRef( value.toString() );
3181 }
3182 
renderer() const3183 QgsVectorTileRenderer *QgsMapBoxGlStyleConverter::renderer() const
3184 {
3185   return mRenderer ? mRenderer->clone() : nullptr;
3186 }
3187 
labeling() const3188 QgsVectorTileLabeling *QgsMapBoxGlStyleConverter::labeling() const
3189 {
3190   return mLabeling ? mLabeling->clone() : nullptr;
3191 }
3192 
numericArgumentsOnly(const QVariant & bottomVariant,const QVariant & topVariant,double & bottom,double & top)3193 bool QgsMapBoxGlStyleConverter::numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top )
3194 {
3195   if ( bottomVariant.canConvert( QMetaType::Double ) && topVariant.canConvert( QMetaType::Double ) )
3196   {
3197     bool bDoubleOk, tDoubleOk;
3198     bottom = bottomVariant.toDouble( &bDoubleOk );
3199     top = topVariant.toDouble( &tDoubleOk );
3200     return ( bDoubleOk && tDoubleOk );
3201   }
3202   return false;
3203 }
3204 
3205 //
3206 // QgsMapBoxGlStyleConversionContext
3207 //
pushWarning(const QString & warning)3208 void QgsMapBoxGlStyleConversionContext::pushWarning( const QString &warning )
3209 {
3210   QgsDebugMsg( warning );
3211   mWarnings << warning;
3212 }
3213 
targetUnit() const3214 QgsUnitTypes::RenderUnit QgsMapBoxGlStyleConversionContext::targetUnit() const
3215 {
3216   return mTargetUnit;
3217 }
3218 
setTargetUnit(QgsUnitTypes::RenderUnit targetUnit)3219 void QgsMapBoxGlStyleConversionContext::setTargetUnit( QgsUnitTypes::RenderUnit targetUnit )
3220 {
3221   mTargetUnit = targetUnit;
3222 }
3223 
pixelSizeConversionFactor() const3224 double QgsMapBoxGlStyleConversionContext::pixelSizeConversionFactor() const
3225 {
3226   return mSizeConversionFactor;
3227 }
3228 
setPixelSizeConversionFactor(double sizeConversionFactor)3229 void QgsMapBoxGlStyleConversionContext::setPixelSizeConversionFactor( double sizeConversionFactor )
3230 {
3231   mSizeConversionFactor = sizeConversionFactor;
3232 }
3233 
spriteImage() const3234 QImage QgsMapBoxGlStyleConversionContext::spriteImage() const
3235 {
3236   return mSpriteImage;
3237 }
3238 
spriteDefinitions() const3239 QVariantMap QgsMapBoxGlStyleConversionContext::spriteDefinitions() const
3240 {
3241   return mSpriteDefinitions;
3242 }
3243 
setSprites(const QImage & image,const QVariantMap & definitions)3244 void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions )
3245 {
3246   mSpriteImage = image;
3247   mSpriteDefinitions = definitions;
3248 }
3249 
setSprites(const QImage & image,const QString & definitions)3250 void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions )
3251 {
3252   setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap() );
3253 }
3254 
layerId() const3255 QString QgsMapBoxGlStyleConversionContext::layerId() const
3256 {
3257   return mLayerId;
3258 }
3259 
setLayerId(const QString & value)3260 void QgsMapBoxGlStyleConversionContext::setLayerId( const QString &value )
3261 {
3262   mLayerId = value;
3263 }
3264