QGIS API Documentation  3.24.2-Tisler (13c1a02865)
qgsmapboxglstyleconverter.cpp
Go to the documentation of this file.
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 
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"
36 #include "qgsfillsymbol.h"
37 #include "qgsmarkersymbol.h"
38 #include "qgslinesymbol.h"
39 
40 #include <QBuffer>
41 #include <QRegularExpression>
42 
44 {
45 }
46 
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 
64 {
65  return convert( QgsJsonUtils::parseJson( style ).toMap(), context );
66 }
67 
69 
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 
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 ) )
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
373  rasterFill->setImageFilePath( sprite );
374  rasterFill->setWidth( spriteSize.width() );
375  rasterFill->setWidthUnit( context.targetUnit() );
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 
420  style.setSymbol( symbol.release() );
421  return true;
422 }
423 
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  QgsPropertyCollection ddProperties;
433  QString rasterLineSprite;
434 
435  const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
436  if ( jsonPaint.contains( QStringLiteral( "line-pattern" ) ) )
437  {
438  const QVariant jsonLinePattern = jsonPaint.value( QStringLiteral( "line-pattern" ) );
439  switch ( jsonLinePattern.type() )
440  {
441  case QVariant::Map:
442  case QVariant::String:
443  {
444  QSize spriteSize;
445  QString spriteProperty, spriteSizeProperty;
446  rasterLineSprite = retrieveSpriteAsBase64( jsonLinePattern, context, spriteSize, spriteProperty, spriteSizeProperty );
447  ddProperties.setProperty( QgsSymbolLayer::PropertyFile, QgsProperty::fromExpression( spriteProperty ) );
448  break;
449  }
450 
451  case QVariant::List:
452  case QVariant::StringList:
453  default:
454  break;
455  }
456 
457  if ( rasterLineSprite.isEmpty() )
458  {
459  // unsupported line-pattern definition, moving on
460  context.pushWarning( QObject::tr( "%1: Skipping unsupported line-pattern property" ).arg( context.layerId() ) );
461  return false;
462  }
463  }
464 
465  // line color
466  QColor lineColor;
467  if ( jsonPaint.contains( QStringLiteral( "line-color" ) ) )
468  {
469  const QVariant jsonLineColor = jsonPaint.value( QStringLiteral( "line-color" ) );
470  switch ( jsonLineColor.type() )
471  {
472  case QVariant::Map:
473  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonLineColor.toMap(), context, &lineColor ) );
475  break;
476 
477  case QVariant::List:
478  case QVariant::StringList:
479  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonLineColor.toList(), PropertyType::Color, context, 1, 255, &lineColor ) );
481  break;
482 
483  case QVariant::String:
484  lineColor = parseColor( jsonLineColor.toString(), context );
485  break;
486 
487  default:
488  context.pushWarning( QObject::tr( "%1: Skipping unsupported line-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineColor.type() ) ) );
489  break;
490  }
491  }
492  else
493  {
494  // defaults to #000000
495  lineColor = QColor( 0, 0, 0 );
496  }
497 
498 
499  double lineWidth = 1.0;
500  QgsProperty lineWidthProperty;
501  if ( jsonPaint.contains( QStringLiteral( "line-width" ) ) )
502  {
503  const QVariant jsonLineWidth = jsonPaint.value( QStringLiteral( "line-width" ) );
504  switch ( jsonLineWidth.type() )
505  {
506  case QVariant::Int:
507  case QVariant::Double:
508  lineWidth = jsonLineWidth.toDouble() * context.pixelSizeConversionFactor();
509  break;
510 
511  case QVariant::Map:
512  lineWidth = -1;
513  lineWidthProperty = parseInterpolateByZoom( jsonLineWidth.toMap(), context, context.pixelSizeConversionFactor(), &lineWidth );
514  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, lineWidthProperty );
515  break;
516 
517  case QVariant::List:
518  case QVariant::StringList:
519  lineWidthProperty = parseValueList( jsonLineWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &lineWidth );
520  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, lineWidthProperty );
521  break;
522 
523  default:
524  context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineWidth.type() ) ) );
525  break;
526  }
527  }
528 
529  double lineOffset = 0.0;
530  if ( jsonPaint.contains( QStringLiteral( "line-offset" ) ) )
531  {
532  const QVariant jsonLineOffset = jsonPaint.value( QStringLiteral( "line-offset" ) );
533  switch ( jsonLineOffset.type() )
534  {
535  case QVariant::Int:
536  case QVariant::Double:
537  lineOffset = -jsonLineOffset.toDouble() * context.pixelSizeConversionFactor();
538  break;
539 
540  case QVariant::Map:
541  lineWidth = -1;
542  ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolateByZoom( jsonLineOffset.toMap(), context, context.pixelSizeConversionFactor() * -1, &lineOffset ) );
543  break;
544 
545  case QVariant::List:
546  case QVariant::StringList:
547  ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseValueList( jsonLineOffset.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * -1, 255, nullptr, &lineOffset ) );
548  break;
549 
550  default:
551  context.pushWarning( QObject::tr( "%1: Skipping unsupported line-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineOffset.type() ) ) );
552  break;
553  }
554  }
555 
556  double lineOpacity = -1.0;
557  if ( jsonPaint.contains( QStringLiteral( "line-opacity" ) ) )
558  {
559  const QVariant jsonLineOpacity = jsonPaint.value( QStringLiteral( "line-opacity" ) );
560  switch ( jsonLineOpacity.type() )
561  {
562  case QVariant::Int:
563  case QVariant::Double:
564  lineOpacity = jsonLineOpacity.toDouble();
565  break;
566 
567  case QVariant::Map:
568  if ( ddProperties.isActive( QgsSymbolLayer::PropertyStrokeColor ) )
569  {
570  context.pushWarning( QObject::tr( "%1: Could not set opacity of layer, opacity already defined in stroke color" ).arg( context.layerId() ) );
571  }
572  else
573  {
574  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonLineOpacity.toMap(), lineColor.isValid() ? lineColor.alpha() : 255, &context ) );
575  }
576  break;
577 
578  case QVariant::List:
579  case QVariant::StringList:
580  if ( ddProperties.isActive( QgsSymbolLayer::PropertyStrokeColor ) )
581  {
582  context.pushWarning( QObject::tr( "%1: Could not set opacity of layer, opacity already defined in stroke color" ).arg( context.layerId() ) );
583  }
584  else
585  {
586  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonLineOpacity.toList(), PropertyType::Opacity, context, 1, lineColor.isValid() ? lineColor.alpha() : 255 ) );
587  }
588  break;
589 
590  default:
591  context.pushWarning( QObject::tr( "%1: Skipping unsupported line-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineOpacity.type() ) ) );
592  break;
593  }
594  }
595 
596  QVector< double > dashVector;
597  if ( jsonPaint.contains( QStringLiteral( "line-dasharray" ) ) )
598  {
599  const QVariant jsonLineDashArray = jsonPaint.value( QStringLiteral( "line-dasharray" ) );
600  switch ( jsonLineDashArray.type() )
601  {
602  case QVariant::Map:
603  {
604  QString arrayExpression;
605  if ( !lineWidthProperty.asExpression().isEmpty() )
606  {
607  arrayExpression = QStringLiteral( "array_to_string(array_foreach(%1,@element * (%2)), ';')" ) // skip-keyword-check
608  .arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, 1 ),
609  lineWidthProperty.asExpression() );
610  }
611  else
612  {
613  arrayExpression = QStringLiteral( "array_to_string(%1, ';')" ).arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, lineWidth ) );
614  }
616 
617  const QVariantList dashSource = jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList().first().toList().value( 1 ).toList();
618  for ( const QVariant &v : dashSource )
619  {
620  dashVector << v.toDouble() * lineWidth;
621  }
622  break;
623  }
624 
625  case QVariant::List:
626  case QVariant::StringList:
627  {
628  if ( ( !lineWidthProperty.asExpression().isEmpty() ) )
629  {
630  QString arrayExpression = QStringLiteral( "array_to_string(array_foreach(array(%1),@element * (%2)), ';')" ) // skip-keyword-check
631  .arg( jsonLineDashArray.toStringList().join( ',' ),
632  lineWidthProperty.asExpression() );
634  }
635  const QVariantList dashSource = jsonLineDashArray.toList();
636  for ( const QVariant &v : dashSource )
637  {
638  dashVector << v.toDouble() * lineWidth;
639  }
640  break;
641  }
642 
643  default:
644  context.pushWarning( QObject::tr( "%1: Skipping unsupported line-dasharray type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonLineDashArray.type() ) ) );
645  break;
646  }
647  }
648 
649  Qt::PenCapStyle penCapStyle = Qt::FlatCap;
650  Qt::PenJoinStyle penJoinStyle = Qt::MiterJoin;
651  if ( jsonLayer.contains( QStringLiteral( "layout" ) ) )
652  {
653  const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
654  if ( jsonLayout.contains( QStringLiteral( "line-cap" ) ) )
655  {
656  penCapStyle = parseCapStyle( jsonLayout.value( QStringLiteral( "line-cap" ) ).toString() );
657  }
658  if ( jsonLayout.contains( QStringLiteral( "line-join" ) ) )
659  {
660  penJoinStyle = parseJoinStyle( jsonLayout.value( QStringLiteral( "line-join" ) ).toString() );
661  }
662  }
663 
664  std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsLineSymbol >() );
665  symbol->setOutputUnit( context.targetUnit() );
666 
667  if ( !rasterLineSprite.isEmpty() )
668  {
669  QgsRasterLineSymbolLayer *lineSymbol = new QgsRasterLineSymbolLayer( rasterLineSprite );
670  lineSymbol->setOutputUnit( context.targetUnit() );
671  lineSymbol->setPenCapStyle( penCapStyle );
672  lineSymbol->setPenJoinStyle( penJoinStyle );
673  lineSymbol->setDataDefinedProperties( ddProperties );
674  lineSymbol->setOffset( lineOffset );
675  lineSymbol->setOffsetUnit( context.targetUnit() );
676 
677  if ( lineOpacity != -1 )
678  {
679  symbol->setOpacity( lineOpacity );
680  }
681  if ( lineWidth != -1 )
682  {
683  lineSymbol->setWidth( lineWidth );
684  }
685  symbol->changeSymbolLayer( 0, lineSymbol );
686  }
687  else
688  {
689  QgsSimpleLineSymbolLayer *lineSymbol = dynamic_cast< QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) );
690  Q_ASSERT( lineSymbol ); // should not fail since QgsLineSymbol() constructor instantiates a QgsSimpleLineSymbolLayer
691 
692  // set render units
693  lineSymbol->setOutputUnit( context.targetUnit() );
694  lineSymbol->setPenCapStyle( penCapStyle );
695  lineSymbol->setPenJoinStyle( penJoinStyle );
696  lineSymbol->setDataDefinedProperties( ddProperties );
697  lineSymbol->setOffset( lineOffset );
698  lineSymbol->setOffsetUnit( context.targetUnit() );
699 
700  if ( lineOpacity != -1 )
701  {
702  symbol->setOpacity( lineOpacity );
703  }
704  if ( lineColor.isValid() )
705  {
706  lineSymbol->setColor( lineColor );
707  }
708  if ( lineWidth != -1 )
709  {
710  lineSymbol->setWidth( lineWidth );
711  }
712  if ( !dashVector.empty() )
713  {
714  lineSymbol->setUseCustomDashPattern( true );
715  lineSymbol->setCustomDashVector( dashVector );
716  }
717  }
718 
720  style.setSymbol( symbol.release() );
721  return true;
722 }
723 
725 {
726  if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
727  {
728  context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
729  return false;
730  }
731 
732  const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
733  QgsPropertyCollection ddProperties;
734 
735  // circle color
736  QColor circleFillColor;
737  if ( jsonPaint.contains( QStringLiteral( "circle-color" ) ) )
738  {
739  const QVariant jsonCircleColor = jsonPaint.value( QStringLiteral( "circle-color" ) );
740  switch ( jsonCircleColor.type() )
741  {
742  case QVariant::Map:
743  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonCircleColor.toMap(), context, &circleFillColor ) );
744  break;
745 
746  case QVariant::List:
747  case QVariant::StringList:
748  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleColor.toList(), PropertyType::Color, context, 1, 255, &circleFillColor ) );
749  break;
750 
751  case QVariant::String:
752  circleFillColor = parseColor( jsonCircleColor.toString(), context );
753  break;
754 
755  default:
756  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleColor.type() ) ) );
757  break;
758  }
759  }
760  else
761  {
762  // defaults to #000000
763  circleFillColor = QColor( 0, 0, 0 );
764  }
765 
766  // circle radius
767  double circleDiameter = 10.0;
768  if ( jsonPaint.contains( QStringLiteral( "circle-radius" ) ) )
769  {
770  const QVariant jsonCircleRadius = jsonPaint.value( QStringLiteral( "circle-radius" ) );
771  switch ( jsonCircleRadius.type() )
772  {
773  case QVariant::Int:
774  case QVariant::Double:
775  circleDiameter = jsonCircleRadius.toDouble() * context.pixelSizeConversionFactor() * 2;
776  break;
777 
778  case QVariant::Map:
779  circleDiameter = -1;
780  ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseInterpolateByZoom( jsonCircleRadius.toMap(), context, context.pixelSizeConversionFactor() * 2, &circleDiameter ) );
781  break;
782 
783  case QVariant::List:
784  case QVariant::StringList:
785  ddProperties.setProperty( QgsSymbolLayer::PropertyWidth, parseValueList( jsonCircleRadius.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * 2, 255, nullptr, &circleDiameter ) );
786  break;
787 
788  default:
789  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-radius type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleRadius.type() ) ) );
790  break;
791  }
792  }
793 
794  double circleOpacity = -1.0;
795  if ( jsonPaint.contains( QStringLiteral( "circle-opacity" ) ) )
796  {
797  const QVariant jsonCircleOpacity = jsonPaint.value( QStringLiteral( "circle-opacity" ) );
798  switch ( jsonCircleOpacity.type() )
799  {
800  case QVariant::Int:
801  case QVariant::Double:
802  circleOpacity = jsonCircleOpacity.toDouble();
803  break;
804 
805  case QVariant::Map:
806  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateOpacityByZoom( jsonCircleOpacity.toMap(), circleFillColor.isValid() ? circleFillColor.alpha() : 255, &context ) );
807  break;
808 
809  case QVariant::List:
810  case QVariant::StringList:
811  ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseValueList( jsonCircleOpacity.toList(), PropertyType::Opacity, context, 1, circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) );
812  break;
813 
814  default:
815  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleOpacity.type() ) ) );
816  break;
817  }
818  }
819  if ( ( circleOpacity != -1 ) && circleFillColor.isValid() )
820  {
821  circleFillColor.setAlphaF( circleOpacity );
822  }
823 
824  // circle stroke color
825  QColor circleStrokeColor;
826  if ( jsonPaint.contains( QStringLiteral( "circle-stroke-color" ) ) )
827  {
828  const QVariant jsonCircleStrokeColor = jsonPaint.value( QStringLiteral( "circle-stroke-color" ) );
829  switch ( jsonCircleStrokeColor.type() )
830  {
831  case QVariant::Map:
832  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateColorByZoom( jsonCircleStrokeColor.toMap(), context, &circleStrokeColor ) );
833  break;
834 
835  case QVariant::List:
836  case QVariant::StringList:
837  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeColor.toList(), PropertyType::Color, context, 1, 255, &circleStrokeColor ) );
838  break;
839 
840  case QVariant::String:
841  circleStrokeColor = parseColor( jsonCircleStrokeColor.toString(), context );
842  break;
843 
844  default:
845  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeColor.type() ) ) );
846  break;
847  }
848  }
849 
850  // circle stroke width
851  double circleStrokeWidth = -1.0;
852  if ( jsonPaint.contains( QStringLiteral( "circle-stroke-width" ) ) )
853  {
854  const QVariant circleStrokeWidthJson = jsonPaint.value( QStringLiteral( "circle-stroke-width" ) );
855  switch ( circleStrokeWidthJson.type() )
856  {
857  case QVariant::Int:
858  case QVariant::Double:
859  circleStrokeWidth = circleStrokeWidthJson.toDouble() * context.pixelSizeConversionFactor();
860  break;
861 
862  case QVariant::Map:
863  circleStrokeWidth = -1.0;
864  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseInterpolateByZoom( circleStrokeWidthJson.toMap(), context, context.pixelSizeConversionFactor(), &circleStrokeWidth ) );
865  break;
866 
867  case QVariant::List:
868  case QVariant::StringList:
869  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeWidth, parseValueList( circleStrokeWidthJson.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &circleStrokeWidth ) );
870  break;
871 
872  default:
873  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( circleStrokeWidthJson.type() ) ) );
874  break;
875  }
876  }
877 
878  double circleStrokeOpacity = -1.0;
879  if ( jsonPaint.contains( QStringLiteral( "circle-stroke-opacity" ) ) )
880  {
881  const QVariant jsonCircleStrokeOpacity = jsonPaint.value( QStringLiteral( "circle-stroke-opacity" ) );
882  switch ( jsonCircleStrokeOpacity.type() )
883  {
884  case QVariant::Int:
885  case QVariant::Double:
886  circleStrokeOpacity = jsonCircleStrokeOpacity.toDouble();
887  break;
888 
889  case QVariant::Map:
890  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseInterpolateOpacityByZoom( jsonCircleStrokeOpacity.toMap(), circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255, &context ) );
891  break;
892 
893  case QVariant::List:
894  case QVariant::StringList:
895  ddProperties.setProperty( QgsSymbolLayer::PropertyStrokeColor, parseValueList( jsonCircleStrokeOpacity.toList(), PropertyType::Opacity, context, 1, circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) );
896  break;
897 
898  default:
899  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleStrokeOpacity.type() ) ) );
900  break;
901  }
902  }
903  if ( ( circleStrokeOpacity != -1 ) && circleStrokeColor.isValid() )
904  {
905  circleStrokeColor.setAlphaF( circleStrokeOpacity );
906  }
907 
908  // translate
909  QPointF circleTranslate;
910  if ( jsonPaint.contains( QStringLiteral( "circle-translate" ) ) )
911  {
912  const QVariant jsonCircleTranslate = jsonPaint.value( QStringLiteral( "circle-translate" ) );
913  switch ( jsonCircleTranslate.type() )
914  {
915 
916  case QVariant::Map:
917  ddProperties.setProperty( QgsSymbolLayer::PropertyOffset, parseInterpolatePointByZoom( jsonCircleTranslate.toMap(), context, context.pixelSizeConversionFactor(), &circleTranslate ) );
918  break;
919 
920  case QVariant::List:
921  case QVariant::StringList:
922  circleTranslate = QPointF( jsonCircleTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(),
923  jsonCircleTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
924  break;
925 
926  default:
927  context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonCircleTranslate.type() ) ) );
928  break;
929  }
930  }
931 
932  std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsMarkerSymbol >() );
933  QgsSimpleMarkerSymbolLayer *markerSymbolLayer = dynamic_cast< QgsSimpleMarkerSymbolLayer * >( symbol->symbolLayer( 0 ) );
934  Q_ASSERT( markerSymbolLayer );
935 
936  // set render units
937  symbol->setOutputUnit( context.targetUnit() );
938  symbol->setDataDefinedProperties( ddProperties );
939 
940  if ( !circleTranslate.isNull() )
941  {
942  markerSymbolLayer->setOffset( circleTranslate );
943  markerSymbolLayer->setOffsetUnit( context.targetUnit() );
944  }
945 
946  if ( circleFillColor.isValid() )
947  {
948  markerSymbolLayer->setFillColor( circleFillColor );
949  }
950  if ( circleDiameter != -1 )
951  {
952  markerSymbolLayer->setSize( circleDiameter );
953  markerSymbolLayer->setSizeUnit( context.targetUnit() );
954  }
955  if ( circleStrokeColor.isValid() )
956  {
957  markerSymbolLayer->setStrokeColor( circleStrokeColor );
958  }
959  if ( circleStrokeWidth != -1 )
960  {
961  markerSymbolLayer->setStrokeWidth( circleStrokeWidth );
962  markerSymbolLayer->setStrokeWidthUnit( context.targetUnit() );
963  }
964 
966  style.setSymbol( symbol.release() );
967  return true;
968 }
969 
970 void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &renderer, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context )
971 {
972  hasLabeling = false;
973  hasRenderer = false;
974 
975  if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
976  {
977  context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
978  return;
979  }
980  const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
981  if ( !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
982  {
983  hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
984  return;
985  }
986 
987  if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
988  {
989  context.pushWarning( QObject::tr( "%1: Style layer has no paint property, skipping" ).arg( context.layerId() ) );
990  return;
991  }
992  const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
993 
994  QgsPropertyCollection ddLabelProperties;
995 
996  double textSize = 16.0 * context.pixelSizeConversionFactor();
997  QgsProperty textSizeProperty;
998  if ( jsonLayout.contains( QStringLiteral( "text-size" ) ) )
999  {
1000  const QVariant jsonTextSize = jsonLayout.value( QStringLiteral( "text-size" ) );
1001  switch ( jsonTextSize.type() )
1002  {
1003  case QVariant::Int:
1004  case QVariant::Double:
1005  textSize = jsonTextSize.toDouble() * context.pixelSizeConversionFactor();
1006  break;
1007 
1008  case QVariant::Map:
1009  textSize = -1;
1010  textSizeProperty = parseInterpolateByZoom( jsonTextSize.toMap(), context, context.pixelSizeConversionFactor(), &textSize );
1011 
1012  break;
1013 
1014  case QVariant::List:
1015  case QVariant::StringList:
1016  textSize = -1;
1017  textSizeProperty = parseValueList( jsonTextSize.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &textSize );
1018  break;
1019 
1020  default:
1021  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextSize.type() ) ) );
1022  break;
1023  }
1024 
1025  if ( textSizeProperty )
1026  {
1027  ddLabelProperties.setProperty( QgsPalLayerSettings::Size, textSizeProperty );
1028  }
1029  }
1030 
1031  // a rough average of ems to character count conversion for a variety of fonts
1032  constexpr double EM_TO_CHARS = 2.0;
1033 
1034  double textMaxWidth = -1;
1035  if ( jsonLayout.contains( QStringLiteral( "text-max-width" ) ) )
1036  {
1037  const QVariant jsonTextMaxWidth = jsonLayout.value( QStringLiteral( "text-max-width" ) );
1038  switch ( jsonTextMaxWidth.type() )
1039  {
1040  case QVariant::Int:
1041  case QVariant::Double:
1042  textMaxWidth = jsonTextMaxWidth.toDouble() * EM_TO_CHARS;
1043  break;
1044 
1045  case QVariant::Map:
1046  ddLabelProperties.setProperty( QgsPalLayerSettings::AutoWrapLength, parseInterpolateByZoom( jsonTextMaxWidth.toMap(), context, EM_TO_CHARS, &textMaxWidth ) );
1047  break;
1048 
1049  case QVariant::List:
1050  case QVariant::StringList:
1051  ddLabelProperties.setProperty( QgsPalLayerSettings::AutoWrapLength, parseValueList( jsonTextMaxWidth.toList(), PropertyType::Numeric, context, EM_TO_CHARS, 255, nullptr, &textMaxWidth ) );
1052  break;
1053 
1054  default:
1055  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-max-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextMaxWidth.type() ) ) );
1056  break;
1057  }
1058  }
1059  else
1060  {
1061  // defaults to 10
1062  textMaxWidth = 10 * EM_TO_CHARS;
1063  }
1064 
1065  double textLetterSpacing = -1;
1066  if ( jsonLayout.contains( QStringLiteral( "text-letter-spacing" ) ) )
1067  {
1068  const QVariant jsonTextLetterSpacing = jsonLayout.value( QStringLiteral( "text-letter-spacing" ) );
1069  switch ( jsonTextLetterSpacing.type() )
1070  {
1071  case QVariant::Int:
1072  case QVariant::Double:
1073  textLetterSpacing = jsonTextLetterSpacing.toDouble();
1074  break;
1075 
1076  case QVariant::Map:
1077  ddLabelProperties.setProperty( QgsPalLayerSettings::FontLetterSpacing, parseInterpolateByZoom( jsonTextLetterSpacing.toMap(), context, 1, &textLetterSpacing ) );
1078  break;
1079 
1080  case QVariant::List:
1081  case QVariant::StringList:
1082  ddLabelProperties.setProperty( QgsPalLayerSettings::FontLetterSpacing, parseValueList( jsonTextLetterSpacing.toList(), PropertyType::Numeric, context, 1, 255, nullptr, &textLetterSpacing ) );
1083  break;
1084 
1085  default:
1086  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-letter-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextLetterSpacing.type() ) ) );
1087  break;
1088  }
1089  }
1090 
1091  QFont textFont;
1092  bool foundFont = false;
1093  QString fontName;
1094  QString fontStyleName;
1095 
1096  if ( jsonLayout.contains( QStringLiteral( "text-font" ) ) )
1097  {
1098  auto splitFontFamily = []( const QString & fontName, QString & family, QString & style ) -> bool
1099  {
1100  const QStringList textFontParts = fontName.split( ' ' );
1101  for ( int i = 1; i < textFontParts.size(); ++i )
1102  {
1103  const QString candidateFontName = textFontParts.mid( 0, i ).join( ' ' );
1104  const QString candidateFontStyle = textFontParts.mid( i ).join( ' ' );
1105  if ( QgsFontUtils::fontFamilyHasStyle( candidateFontName, candidateFontStyle ) )
1106  {
1107  family = candidateFontName;
1108  style = candidateFontStyle;
1109  return true;
1110  }
1111  }
1112 
1113  if ( QFontDatabase().hasFamily( fontName ) )
1114  {
1115  // the json isn't following the spec correctly!!
1116  family = fontName;
1117  style.clear();
1118  return true;
1119  }
1120  return false;
1121  };
1122 
1123  const QVariant jsonTextFont = jsonLayout.value( QStringLiteral( "text-font" ) );
1124  if ( jsonTextFont.type() != QVariant::List && jsonTextFont.type() != QVariant::StringList && jsonTextFont.type() != QVariant::String
1125  && jsonTextFont.type() != QVariant::Map )
1126  {
1127  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-font type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextFont.type() ) ) );
1128  }
1129  else
1130  {
1131  switch ( jsonTextFont.type() )
1132  {
1133  case QVariant::List:
1134  case QVariant::StringList:
1135  fontName = jsonTextFont.toList().value( 0 ).toString();
1136  break;
1137 
1138  case QVariant::String:
1139  fontName = jsonTextFont.toString();
1140  break;
1141 
1142  case QVariant::Map:
1143  {
1144  QString familyCaseString = QStringLiteral( "CASE " );
1145  QString styleCaseString = QStringLiteral( "CASE " );
1146  QString fontFamily;
1147  const QVariantList stops = jsonTextFont.toMap().value( QStringLiteral( "stops" ) ).toList();
1148 
1149  bool error = false;
1150  for ( int i = 0; i < stops.length() - 1; ++i )
1151  {
1152  // bottom zoom and value
1153  const QVariant bz = stops.value( i ).toList().value( 0 );
1154  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();
1155  if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
1156  {
1157  context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1158  error = true;
1159  break;
1160  }
1161 
1162  // top zoom
1163  const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
1164  if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
1165  {
1166  context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1167  error = true;
1168  break;
1169  }
1170 
1171  if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1172  {
1173  familyCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1174  "THEN %3 " ).arg( bz.toString(),
1175  tz.toString(),
1176  QgsExpression::quotedValue( fontFamily ) );
1177  styleCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1178  "THEN %3 " ).arg( bz.toString(),
1179  tz.toString(),
1180  QgsExpression::quotedValue( fontStyleName ) );
1181  }
1182  else
1183  {
1184  context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1185  }
1186  }
1187  if ( error )
1188  break;
1189 
1190  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();
1191  if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1192  {
1193  familyCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontFamily ) );
1194  styleCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontStyleName ) );
1195  }
1196  else
1197  {
1198  context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1199  }
1200 
1201  ddLabelProperties.setProperty( QgsPalLayerSettings::Family, QgsProperty::fromExpression( familyCaseString ) );
1202  ddLabelProperties.setProperty( QgsPalLayerSettings::FontStyle, QgsProperty::fromExpression( styleCaseString ) );
1203 
1204  foundFont = true;
1205  fontName = fontFamily;
1206 
1207  break;
1208  }
1209 
1210  default:
1211  break;
1212  }
1213 
1214  QString fontFamily;
1215  if ( splitFontFamily( fontName, fontFamily, fontStyleName ) )
1216  {
1217  textFont = QFont( fontFamily );
1218  if ( !fontStyleName.isEmpty() )
1219  textFont.setStyleName( fontStyleName );
1220  foundFont = true;
1221  }
1222  }
1223  }
1224  else
1225  {
1226  // Defaults to ["Open Sans Regular","Arial Unicode MS Regular"].
1227  if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Open Sans" ), QStringLiteral( "Regular" ) ) )
1228  {
1229  fontName = QStringLiteral( "Open Sans" );
1230  textFont = QFont( fontName );
1231  textFont.setStyleName( QStringLiteral( "Regular" ) );
1232  fontStyleName = QStringLiteral( "Regular" );
1233  foundFont = true;
1234  }
1235  else if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Arial Unicode MS" ), QStringLiteral( "Regular" ) ) )
1236  {
1237  fontName = QStringLiteral( "Arial Unicode MS" );
1238  textFont = QFont( fontName );
1239  textFont.setStyleName( QStringLiteral( "Regular" ) );
1240  fontStyleName = QStringLiteral( "Regular" );
1241  foundFont = true;
1242  }
1243  else
1244  {
1245  fontName = QStringLiteral( "Open Sans, Arial Unicode MS" );
1246  }
1247  }
1248  if ( !foundFont && !fontName.isEmpty() )
1249  {
1250  context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), fontName ) );
1251  }
1252 
1253  // text color
1254  QColor textColor;
1255  if ( jsonPaint.contains( QStringLiteral( "text-color" ) ) )
1256  {
1257  const QVariant jsonTextColor = jsonPaint.value( QStringLiteral( "text-color" ) );
1258  switch ( jsonTextColor.type() )
1259  {
1260  case QVariant::Map:
1261  ddLabelProperties.setProperty( QgsPalLayerSettings::Color, parseInterpolateColorByZoom( jsonTextColor.toMap(), context, &textColor ) );
1262  break;
1263 
1264  case QVariant::List:
1265  case QVariant::StringList:
1266  ddLabelProperties.setProperty( QgsPalLayerSettings::Color, parseValueList( jsonTextColor.toList(), PropertyType::Color, context, 1, 255, &textColor ) );
1267  break;
1268 
1269  case QVariant::String:
1270  textColor = parseColor( jsonTextColor.toString(), context );
1271  break;
1272 
1273  default:
1274  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextColor.type() ) ) );
1275  break;
1276  }
1277  }
1278  else
1279  {
1280  // defaults to #000000
1281  textColor = QColor( 0, 0, 0 );
1282  }
1283 
1284  // buffer color
1285  QColor bufferColor;
1286  if ( jsonPaint.contains( QStringLiteral( "text-halo-color" ) ) )
1287  {
1288  const QVariant jsonBufferColor = jsonPaint.value( QStringLiteral( "text-halo-color" ) );
1289  switch ( jsonBufferColor.type() )
1290  {
1291  case QVariant::Map:
1292  ddLabelProperties.setProperty( QgsPalLayerSettings::BufferColor, parseInterpolateColorByZoom( jsonBufferColor.toMap(), context, &bufferColor ) );
1293  break;
1294 
1295  case QVariant::List:
1296  case QVariant::StringList:
1297  ddLabelProperties.setProperty( QgsPalLayerSettings::BufferColor, parseValueList( jsonBufferColor.toList(), PropertyType::Color, context, 1, 255, &bufferColor ) );
1298  break;
1299 
1300  case QVariant::String:
1301  bufferColor = parseColor( jsonBufferColor.toString(), context );
1302  break;
1303 
1304  default:
1305  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonBufferColor.type() ) ) );
1306  break;
1307  }
1308  }
1309 
1310  double bufferSize = 0.0;
1311  // the pixel based text buffers appear larger when rendered on the web - so automatically scale
1312  // them up when converting to a QGIS style
1313  // (this number is based on trial-and-error comparisons only!)
1314  constexpr double BUFFER_SIZE_SCALE = 2.0;
1315  if ( jsonPaint.contains( QStringLiteral( "text-halo-width" ) ) )
1316  {
1317  const QVariant jsonHaloWidth = jsonPaint.value( QStringLiteral( "text-halo-width" ) );
1318  switch ( jsonHaloWidth.type() )
1319  {
1320  case QVariant::Int:
1321  case QVariant::Double:
1322  bufferSize = jsonHaloWidth.toDouble() * context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE;
1323  break;
1324 
1325  case QVariant::Map:
1326  bufferSize = 1;
1327  ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseInterpolateByZoom( jsonHaloWidth.toMap(), context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, &bufferSize ) );
1328  break;
1329 
1330  case QVariant::List:
1331  case QVariant::StringList:
1332  bufferSize = 1;
1333  ddLabelProperties.setProperty( QgsPalLayerSettings::BufferSize, parseValueList( jsonHaloWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, 255, nullptr, &bufferSize ) );
1334  break;
1335 
1336  default:
1337  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonHaloWidth.type() ) ) );
1338  break;
1339  }
1340  }
1341 
1342  double haloBlurSize = 0;
1343  if ( jsonPaint.contains( QStringLiteral( "text-halo-blur" ) ) )
1344  {
1345  const QVariant jsonTextHaloBlur = jsonPaint.value( QStringLiteral( "text-halo-blur" ) );
1346  switch ( jsonTextHaloBlur.type() )
1347  {
1348  case QVariant::Int:
1349  case QVariant::Double:
1350  {
1351  haloBlurSize = jsonTextHaloBlur.toDouble() * context.pixelSizeConversionFactor();
1352  break;
1353  }
1354 
1355  default:
1356  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-blur type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextHaloBlur.type() ) ) );
1357  break;
1358  }
1359  }
1360 
1361  QgsTextFormat format;
1362  format.setSizeUnit( context.targetUnit() );
1363  if ( textColor.isValid() )
1364  format.setColor( textColor );
1365  if ( textSize >= 0 )
1366  format.setSize( textSize );
1367  if ( foundFont )
1368  {
1369  format.setFont( textFont );
1370  if ( !fontStyleName.isEmpty() )
1371  format.setNamedStyle( fontStyleName );
1372  }
1373  if ( textLetterSpacing > 0 )
1374  {
1375  QFont f = format.font();
1376  f.setLetterSpacing( QFont::AbsoluteSpacing, textLetterSpacing );
1377  format.setFont( f );
1378  }
1379 
1380  if ( bufferSize > 0 )
1381  {
1382  format.buffer().setEnabled( true );
1383  format.buffer().setSize( bufferSize );
1384  format.buffer().setSizeUnit( context.targetUnit() );
1385  format.buffer().setColor( bufferColor );
1386 
1387  if ( haloBlurSize > 0 )
1388  {
1389  QgsEffectStack *stack = new QgsEffectStack();
1390  QgsBlurEffect *blur = new QgsBlurEffect() ;
1391  blur->setEnabled( true );
1392  blur->setBlurUnit( context.targetUnit() );
1393  blur->setBlurLevel( haloBlurSize );
1395  stack->appendEffect( blur );
1396  stack->setEnabled( true );
1397  format.buffer().setPaintEffect( stack );
1398  }
1399  }
1400 
1401  QgsPalLayerSettings labelSettings;
1402 
1403  if ( textMaxWidth > 0 )
1404  {
1405  labelSettings.autoWrapLength = textMaxWidth;
1406  }
1407 
1408  // convert field name
1409 
1410  auto processLabelField = []( const QString & string, bool & isExpression )->QString
1411  {
1412  // {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
1413  // but if single field is covered in {}, return it directly
1414  const QRegularExpression singleFieldRx( QStringLiteral( "^{([^}]+)}$" ) );
1415  const QRegularExpressionMatch match = singleFieldRx.match( string );
1416  if ( match.hasMatch() )
1417  {
1418  isExpression = false;
1419  return match.captured( 1 );
1420  }
1421 
1422  const QRegularExpression multiFieldRx( QStringLiteral( "(?={[^}]+})" ) );
1423  const QStringList parts = string.split( multiFieldRx );
1424  if ( parts.size() > 1 )
1425  {
1426  isExpression = true;
1427 
1428  QStringList res;
1429  for ( const QString &part : parts )
1430  {
1431  if ( part.isEmpty() )
1432  continue;
1433 
1434  if ( !part.contains( '{' ) )
1435  {
1436  res << QgsExpression::quotedValue( part );
1437  continue;
1438  }
1439 
1440  // part will start at a {field} reference
1441  const QStringList split = part.split( '}' );
1442  res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
1443  if ( !split.at( 1 ).isEmpty() )
1444  res << QgsExpression::quotedValue( split.at( 1 ) );
1445  }
1446  return QStringLiteral( "concat(%1)" ).arg( res.join( ',' ) );
1447  }
1448  else
1449  {
1450  isExpression = false;
1451  return string;
1452  }
1453  };
1454 
1455  if ( jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1456  {
1457  const QVariant jsonTextField = jsonLayout.value( QStringLiteral( "text-field" ) );
1458  switch ( jsonTextField.type() )
1459  {
1460  case QVariant::String:
1461  {
1462  labelSettings.fieldName = processLabelField( jsonTextField.toString(), labelSettings.isExpression );
1463  break;
1464  }
1465 
1466  case QVariant::List:
1467  case QVariant::StringList:
1468  {
1469  const QVariantList textFieldList = jsonTextField.toList();
1470  /*
1471  * e.g.
1472  * "text-field": ["format",
1473  * "foo", { "font-scale": 1.2 },
1474  * "bar", { "font-scale": 0.8 }
1475  * ]
1476  */
1477  if ( textFieldList.size() > 2 && textFieldList.at( 0 ).toString() == QLatin1String( "format" ) )
1478  {
1479  QStringList parts;
1480  for ( int i = 1; i < textFieldList.size(); ++i )
1481  {
1482  bool isExpression = false;
1483  const QString part = processLabelField( textFieldList.at( i ).toString(), isExpression );
1484  if ( !isExpression )
1485  parts << QgsExpression::quotedColumnRef( part );
1486  else
1487  parts << part;
1488  // TODO -- we could also translate font color, underline, overline, strikethrough to HTML tags!
1489  i += 1;
1490  }
1491  labelSettings.fieldName = QStringLiteral( "concat(%1)" ).arg( parts.join( ',' ) );
1492  labelSettings.isExpression = true;
1493  }
1494  else
1495  {
1496  /*
1497  * e.g.
1498  * "text-field": ["to-string", ["get", "name"]]
1499  */
1500  labelSettings.fieldName = parseExpression( textFieldList, context );
1501  labelSettings.isExpression = true;
1502  }
1503  break;
1504  }
1505 
1506  default:
1507  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextField.type() ) ) );
1508  break;
1509  }
1510  }
1511 
1512  if ( jsonLayout.contains( QStringLiteral( "text-transform" ) ) )
1513  {
1514  const QString textTransform = jsonLayout.value( QStringLiteral( "text-transform" ) ).toString();
1515  if ( textTransform == QLatin1String( "uppercase" ) )
1516  {
1517  labelSettings.fieldName = QStringLiteral( "upper(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1518  }
1519  else if ( textTransform == QLatin1String( "lowercase" ) )
1520  {
1521  labelSettings.fieldName = QStringLiteral( "lower(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1522  }
1523  labelSettings.isExpression = true;
1524  }
1525 
1526  labelSettings.placement = QgsPalLayerSettings::OverPoint;
1528  if ( jsonLayout.contains( QStringLiteral( "symbol-placement" ) ) )
1529  {
1530  const QString symbolPlacement = jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString();
1531  if ( symbolPlacement == QLatin1String( "line" ) )
1532  {
1533  labelSettings.placement = QgsPalLayerSettings::Curved;
1535  geometryType = QgsWkbTypes::LineGeometry;
1536 
1537  if ( jsonLayout.contains( QStringLiteral( "text-rotation-alignment" ) ) )
1538  {
1539  const QString textRotationAlignment = jsonLayout.value( QStringLiteral( "text-rotation-alignment" ) ).toString();
1540  if ( textRotationAlignment == QLatin1String( "viewport" ) )
1541  {
1543  }
1544  }
1545 
1546  if ( labelSettings.placement == QgsPalLayerSettings::Curved )
1547  {
1548  QPointF textOffset;
1549  QgsProperty textOffsetProperty;
1550  if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1551  {
1552  const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1553 
1554  // units are ems!
1555  switch ( jsonTextOffset.type() )
1556  {
1557  case QVariant::Map:
1558  textOffsetProperty = parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, !textSizeProperty ? textSize : 1.0, &textOffset );
1559  if ( !textSizeProperty )
1560  {
1561  ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "abs(array_get(%1,1))-%2" ).arg( textOffsetProperty ).arg( textSize ) );
1562  }
1563  else
1564  {
1565  ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "with_variable('text_size',%2,abs(array_get(%1,1))*@text_size-@text_size)" ).arg( textOffsetProperty.asExpression(), textSizeProperty.asExpression() ) );
1566  }
1567  ddLabelProperties.setProperty( QgsPalLayerSettings::LinePlacementOptions, QStringLiteral( "if(array_get(%1,1)>0,'BL','AL')" ).arg( textOffsetProperty ) );
1568  break;
1569 
1570  case QVariant::List:
1571  case QVariant::StringList:
1572  textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1573  jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1574  break;
1575 
1576  default:
1577  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextOffset.type() ) ) );
1578  break;
1579  }
1580 
1581  if ( !textOffset.isNull() )
1582  {
1583  labelSettings.distUnits = context.targetUnit();
1584  labelSettings.dist = std::abs( textOffset.y() ) - textSize;
1585  labelSettings.lineSettings().setPlacementFlags( textOffset.y() > 0.0 ? QgsLabeling::BelowLine : QgsLabeling::AboveLine );
1586  if ( textSizeProperty && !textOffsetProperty )
1587  {
1588  ddLabelProperties.setProperty( QgsPalLayerSettings::LabelDistance, QStringLiteral( "with_variable('text_size',%2,%1*@text_size-@text_size)" ).arg( std::abs( textOffset.y() / textSize ) ).arg( textSizeProperty.asExpression() ) );
1589  }
1590  }
1591  }
1592 
1593  if ( textOffset.isNull() )
1594  {
1596  }
1597  }
1598  }
1599  }
1600 
1601  if ( jsonLayout.contains( QStringLiteral( "text-justify" ) ) )
1602  {
1603  const QVariant jsonTextJustify = jsonLayout.value( QStringLiteral( "text-justify" ) );
1604 
1605  // default is center
1606  QString textAlign = QStringLiteral( "center" );
1607 
1608  const QVariantMap conversionMap
1609  {
1610  { QStringLiteral( "left" ), QStringLiteral( "left" ) },
1611  { QStringLiteral( "center" ), QStringLiteral( "center" ) },
1612  { QStringLiteral( "right" ), QStringLiteral( "right" ) },
1613  { QStringLiteral( "auto" ), QStringLiteral( "follow" ) }
1614  };
1615 
1616  switch ( jsonTextJustify.type() )
1617  {
1618  case QVariant::String:
1619  textAlign = jsonTextJustify.toString();
1620  break;
1621 
1622  case QVariant::List:
1623  ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextJustify.toList(), context, conversionMap, &textAlign ) ) );
1624  break;
1625 
1626  case QVariant::Map:
1627  ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, parseInterpolateStringByZoom( jsonTextJustify.toMap(), context, conversionMap, &textAlign ) );
1628  break;
1629 
1630  default:
1631  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-justify type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextJustify.type() ) ) );
1632  break;
1633  }
1634 
1635  if ( textAlign == QLatin1String( "left" ) )
1637  else if ( textAlign == QLatin1String( "right" ) )
1639  else if ( textAlign == QLatin1String( "center" ) )
1641  else if ( textAlign == QLatin1String( "follow" ) )
1643  }
1644  else
1645  {
1647  }
1648 
1649  if ( labelSettings.placement == QgsPalLayerSettings::OverPoint )
1650  {
1651  if ( jsonLayout.contains( QStringLiteral( "text-anchor" ) ) )
1652  {
1653  const QVariant jsonTextAnchor = jsonLayout.value( QStringLiteral( "text-anchor" ) );
1654  QString textAnchor;
1655 
1656  const QVariantMap conversionMap
1657  {
1658  { QStringLiteral( "center" ), 4 },
1659  { QStringLiteral( "left" ), 5 },
1660  { QStringLiteral( "right" ), 3 },
1661  { QStringLiteral( "top" ), 7 },
1662  { QStringLiteral( "bottom" ), 1 },
1663  { QStringLiteral( "top-left" ), 8 },
1664  { QStringLiteral( "top-right" ), 6 },
1665  { QStringLiteral( "bottom-left" ), 2 },
1666  { QStringLiteral( "bottom-right" ), 0 },
1667  };
1668 
1669  switch ( jsonTextAnchor.type() )
1670  {
1671  case QVariant::String:
1672  textAnchor = jsonTextAnchor.toString();
1673  break;
1674 
1675  case QVariant::List:
1676  ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextAnchor.toList(), context, conversionMap, &textAnchor ) ) );
1677  break;
1678 
1679  case QVariant::Map:
1680  ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetQuad, parseInterpolateStringByZoom( jsonTextAnchor.toMap(), context, conversionMap, &textAnchor ) );
1681  break;
1682 
1683  default:
1684  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-anchor type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextAnchor.type() ) ) );
1685  break;
1686  }
1687 
1688  if ( textAnchor == QLatin1String( "center" ) )
1690  else if ( textAnchor == QLatin1String( "left" ) )
1692  else if ( textAnchor == QLatin1String( "right" ) )
1694  else if ( textAnchor == QLatin1String( "top" ) )
1696  else if ( textAnchor == QLatin1String( "bottom" ) )
1698  else if ( textAnchor == QLatin1String( "top-left" ) )
1700  else if ( textAnchor == QLatin1String( "top-right" ) )
1702  else if ( textAnchor == QLatin1String( "bottom-left" ) )
1704  else if ( textAnchor == QLatin1String( "bottom-right" ) )
1706  }
1707 
1708  QPointF textOffset;
1709  if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1710  {
1711  const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1712 
1713  // units are ems!
1714  switch ( jsonTextOffset.type() )
1715  {
1716  case QVariant::Map:
1717  ddLabelProperties.setProperty( QgsPalLayerSettings::OffsetXY, parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, textSize, &textOffset ) );
1718  break;
1719 
1720  case QVariant::List:
1721  case QVariant::StringList:
1722  textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1723  jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1724  break;
1725 
1726  default:
1727  context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonTextOffset.type() ) ) );
1728  break;
1729  }
1730 
1731  if ( !textOffset.isNull() )
1732  {
1733  labelSettings.offsetUnits = context.targetUnit();
1734  labelSettings.xOffset = textOffset.x();
1735  labelSettings.yOffset = textOffset.y();
1736  }
1737  }
1738  }
1739 
1740  if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) &&
1741  ( labelSettings.placement == QgsPalLayerSettings::Horizontal || labelSettings.placement == QgsPalLayerSettings::Curved ) )
1742  {
1743  QSize spriteSize;
1744  QString spriteProperty, spriteSizeProperty;
1745  const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1746  if ( !sprite.isEmpty() )
1747  {
1749  markerLayer->setPath( sprite );
1750  markerLayer->setSize( spriteSize.width() );
1751  markerLayer->setSizeUnit( context.targetUnit() );
1752 
1753  if ( !spriteProperty.isEmpty() )
1754  {
1755  QgsPropertyCollection markerDdProperties;
1756  markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1757  markerLayer->setDataDefinedProperties( markerDdProperties );
1758 
1759  ddLabelProperties.setProperty( QgsPalLayerSettings::ShapeSizeX, QgsProperty::fromExpression( spriteSizeProperty ) );
1760  }
1761 
1762  QgsTextBackgroundSettings backgroundSettings;
1763  backgroundSettings.setEnabled( true );
1765  backgroundSettings.setSize( spriteSize );
1766  backgroundSettings.setSizeUnit( context.targetUnit() );
1767  backgroundSettings.setSizeType( QgsTextBackgroundSettings::SizeFixed );
1768  backgroundSettings.setMarkerSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
1769  format.setBackground( backgroundSettings );
1770  }
1771  }
1772 
1773  if ( textSize >= 0 )
1774  {
1775  // TODO -- this probably needs revisiting -- it was copied from the MapTiler code, but may be wrong...
1776  labelSettings.priority = std::min( textSize / ( context.pixelSizeConversionFactor() * 3 ), 10.0 );
1777  }
1778 
1779  labelSettings.setFormat( format );
1780 
1781  // use a low obstacle weight for layers by default -- we'd rather have more labels for these layers, even if placement isn't ideal
1782  labelSettings.obstacleSettings().setFactor( 0.1 );
1783 
1784  labelSettings.setDataDefinedProperties( ddLabelProperties );
1785 
1786  labelingStyle.setGeometryType( geometryType );
1787  labelingStyle.setLabelSettings( labelSettings );
1788 
1789  hasLabeling = true;
1790 
1791  hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
1792 }
1793 
1795 {
1796  if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
1797  {
1798  context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
1799  return false;
1800  }
1801  const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
1802 
1803  if ( jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString() == QLatin1String( "line" ) && !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1804  {
1805  QgsPropertyCollection ddProperties;
1806 
1807  double spacing = -1.0;
1808  if ( jsonLayout.contains( QStringLiteral( "symbol-spacing" ) ) )
1809  {
1810  const QVariant jsonSpacing = jsonLayout.value( QStringLiteral( "symbol-spacing" ) );
1811  switch ( jsonSpacing.type() )
1812  {
1813  case QVariant::Int:
1814  case QVariant::Double:
1815  spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
1816  break;
1817 
1818  case QVariant::Map:
1819  ddProperties.setProperty( QgsSymbolLayer::PropertyInterval, parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing ) );
1820  break;
1821 
1822  case QVariant::List:
1823  case QVariant::StringList:
1824  ddProperties.setProperty( QgsSymbolLayer::PropertyInterval, parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing ) );
1825  break;
1826 
1827  default:
1828  context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonSpacing.type() ) ) );
1829  break;
1830  }
1831  }
1832  else
1833  {
1834  // defaults to 250
1835  spacing = 250 * context.pixelSizeConversionFactor();
1836  }
1837 
1838  bool rotateMarkers = true;
1839  if ( jsonLayout.contains( QStringLiteral( "icon-rotation-alignment" ) ) )
1840  {
1841  const QString alignment = jsonLayout.value( QStringLiteral( "icon-rotation-alignment" ) ).toString();
1842  if ( alignment == QLatin1String( "map" ) || alignment == QLatin1String( "auto" ) )
1843  {
1844  rotateMarkers = true;
1845  }
1846  else if ( alignment == QLatin1String( "viewport" ) )
1847  {
1848  rotateMarkers = false;
1849  }
1850  }
1851 
1852  QgsPropertyCollection markerDdProperties;
1853  double rotation = 0.0;
1854  if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
1855  {
1856  const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
1857  switch ( jsonIconRotate.type() )
1858  {
1859  case QVariant::Int:
1860  case QVariant::Double:
1861  rotation = jsonIconRotate.toDouble();
1862  break;
1863 
1864  case QVariant::Map:
1865  markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
1866  break;
1867 
1868  case QVariant::List:
1869  case QVariant::StringList:
1870  markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
1871  break;
1872 
1873  default:
1874  context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconRotate.type() ) ) );
1875  break;
1876  }
1877  }
1878 
1879  QgsMarkerLineSymbolLayer *lineSymbol = new QgsMarkerLineSymbolLayer( rotateMarkers, spacing > 0 ? spacing : 1 );
1880  lineSymbol->setOutputUnit( context.targetUnit() );
1881  lineSymbol->setDataDefinedProperties( ddProperties );
1882  if ( spacing < 1 )
1883  {
1884  // if spacing isn't specified, it's a central point marker only
1886  }
1887 
1889  QSize spriteSize;
1890  QString spriteProperty, spriteSizeProperty;
1891  const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1892  if ( !sprite.isNull() )
1893  {
1894  markerLayer->setPath( sprite );
1895  markerLayer->setSize( spriteSize.width() );
1896  markerLayer->setSizeUnit( context.targetUnit() );
1897 
1898  if ( !spriteProperty.isEmpty() )
1899  {
1900  markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1901  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth, QgsProperty::fromExpression( spriteSizeProperty ) );
1902  }
1903  }
1904 
1905  if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1906  {
1907  const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1908  double size = 1.0;
1909  QgsProperty property;
1910  switch ( jsonIconSize.type() )
1911  {
1912  case QVariant::Int:
1913  case QVariant::Double:
1914  {
1915  size = jsonIconSize.toDouble();
1916  if ( !spriteSizeProperty.isEmpty() )
1917  {
1918  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1919  QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
1920  }
1921  break;
1922  }
1923 
1924  case QVariant::Map:
1925  property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
1926  break;
1927 
1928  case QVariant::List:
1929  case QVariant::StringList:
1930  default:
1931  context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconSize.type() ) ) );
1932  break;
1933  }
1934  markerLayer->setSize( size * spriteSize.width() );
1935  if ( !property.expressionString().isEmpty() )
1936  {
1937  if ( !spriteSizeProperty.isEmpty() )
1938  {
1939  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1940  QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
1941  }
1942  else
1943  {
1944  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1945  QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
1946  }
1947  }
1948  }
1949 
1950  markerLayer->setDataDefinedProperties( markerDdProperties );
1951  markerLayer->setAngle( rotation );
1952  lineSymbol->setSubSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
1953 
1954  std::unique_ptr< QgsSymbol > symbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << lineSymbol );
1955 
1956  // set render units
1957  symbol->setOutputUnit( context.targetUnit() );
1958  lineSymbol->setOutputUnit( context.targetUnit() );
1959 
1960  rendererStyle.setGeometryType( QgsWkbTypes::LineGeometry );
1961  rendererStyle.setSymbol( symbol.release() );
1962  return true;
1963  }
1964  else if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) )
1965  {
1966  const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
1967 
1968  QSize spriteSize;
1969  QString spriteProperty, spriteSizeProperty;
1970  const QString sprite = retrieveSpriteAsBase64( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1971  if ( !sprite.isEmpty() )
1972  {
1974  rasterMarker->setPath( sprite );
1975  rasterMarker->setSize( spriteSize.width() );
1976  rasterMarker->setSizeUnit( context.targetUnit() );
1977 
1978  QgsPropertyCollection markerDdProperties;
1979  if ( !spriteProperty.isEmpty() )
1980  {
1981  markerDdProperties.setProperty( QgsSymbolLayer::PropertyName, QgsProperty::fromExpression( spriteProperty ) );
1982  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth, QgsProperty::fromExpression( spriteSizeProperty ) );
1983  }
1984 
1985  if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1986  {
1987  const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1988  double size = 1.0;
1989  QgsProperty property;
1990  switch ( jsonIconSize.type() )
1991  {
1992  case QVariant::Int:
1993  case QVariant::Double:
1994  {
1995  size = jsonIconSize.toDouble();
1996  if ( !spriteSizeProperty.isEmpty() )
1997  {
1998  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
1999  QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
2000  }
2001  break;
2002  }
2003 
2004  case QVariant::Map:
2005  property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2006  break;
2007 
2008  case QVariant::List:
2009  case QVariant::StringList:
2010  default:
2011  context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconSize.type() ) ) );
2012  break;
2013  }
2014  rasterMarker->setSize( size * spriteSize.width() );
2015  if ( !property.expressionString().isEmpty() )
2016  {
2017  if ( !spriteSizeProperty.isEmpty() )
2018  {
2019  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
2020  QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2021  }
2022  else
2023  {
2024  markerDdProperties.setProperty( QgsSymbolLayer::PropertyWidth,
2025  QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2026  }
2027  }
2028  }
2029 
2030  double rotation = 0.0;
2031  if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
2032  {
2033  const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
2034  switch ( jsonIconRotate.type() )
2035  {
2036  case QVariant::Int:
2037  case QVariant::Double:
2038  rotation = jsonIconRotate.toDouble();
2039  break;
2040 
2041  case QVariant::Map:
2042  markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2043  break;
2044 
2045  case QVariant::List:
2046  case QVariant::StringList:
2047  markerDdProperties.setProperty( QgsSymbolLayer::PropertyAngle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2048  break;
2049 
2050  default:
2051  context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconRotate.type() ) ) );
2052  break;
2053  }
2054  }
2055 
2056  double iconOpacity = -1.0;
2057  if ( jsonPaint.contains( QStringLiteral( "icon-opacity" ) ) )
2058  {
2059  const QVariant jsonIconOpacity = jsonPaint.value( QStringLiteral( "icon-opacity" ) );
2060  switch ( jsonIconOpacity.type() )
2061  {
2062  case QVariant::Int:
2063  case QVariant::Double:
2064  iconOpacity = jsonIconOpacity.toDouble();
2065  break;
2066 
2067  case QVariant::Map:
2068  markerDdProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseInterpolateByZoom( jsonIconOpacity.toMap(), context, 100, &iconOpacity ) );
2069  break;
2070 
2071  case QVariant::List:
2072  case QVariant::StringList:
2073  markerDdProperties.setProperty( QgsSymbolLayer::PropertyOpacity, parseValueList( jsonIconOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &iconOpacity ) );
2074  break;
2075 
2076  default:
2077  context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( jsonIconOpacity.type() ) ) );
2078  break;
2079  }
2080  }
2081 
2082  rasterMarker->setDataDefinedProperties( markerDdProperties );
2083  rasterMarker->setAngle( rotation );
2084  if ( iconOpacity >= 0 )
2085  rasterMarker->setOpacity( iconOpacity );
2086 
2087  QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << rasterMarker );
2088  rendererStyle.setSymbol( markerSymbol );
2090  return true;
2091  }
2092  }
2093 
2094  return false;
2095 }
2096 
2098 {
2099  const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2100  const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2101  if ( stops.empty() )
2102  return QgsProperty();
2103 
2104  QString caseString = QStringLiteral( "CASE " );
2105  const QString colorComponent( "color_part(%1,'%2')" );
2106 
2107  for ( int i = 0; i < stops.length() - 1; ++i )
2108  {
2109  // step bottom zoom
2110  const QString bz = stops.at( i ).toList().value( 0 ).toString();
2111  // step top zoom
2112  const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
2113 
2114  const QVariant bcVariant = stops.at( i ).toList().value( 1 );
2115  const QVariant tcVariant = stops.at( i + 1 ).toList().value( 1 );
2116 
2117  const QColor bottomColor = parseColor( bcVariant.toString(), context );
2118  const QColor topColor = parseColor( tcVariant.toString(), context );
2119 
2120  if ( i == 0 && bottomColor.isValid() )
2121  {
2122  int bcHue;
2123  int bcSat;
2124  int bcLight;
2125  int bcAlpha;
2126  colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2127  caseString += QStringLiteral( "WHEN @vector_tile_zoom < %1 THEN color_hsla(%2, %3, %4, %5) " )
2128  .arg( bz ).arg( bcHue ).arg( bcSat ).arg( bcLight ).arg( bcAlpha );
2129  }
2130 
2131  if ( bottomColor.isValid() && topColor.isValid() )
2132  {
2133  int bcHue;
2134  int bcSat;
2135  int bcLight;
2136  int bcAlpha;
2137  colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2138  int tcHue;
2139  int tcSat;
2140  int tcLight;
2141  int tcAlpha;
2142  colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2143  caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2144  "%3, %4, %5, %6) " ).arg( bz, tz,
2145  interpolateExpression( bz.toDouble(), tz.toDouble(), bcHue, tcHue, base, 1, &context ),
2146  interpolateExpression( bz.toDouble(), tz.toDouble(), bcSat, tcSat, base, 1, &context ),
2147  interpolateExpression( bz.toDouble(), tz.toDouble(), bcLight, tcLight, base, 1, &context ),
2148  interpolateExpression( bz.toDouble(), tz.toDouble(), bcAlpha, tcAlpha, base, 1, &context ) );
2149  }
2150  else
2151  {
2152  const QString bottomColorExpr = parseColorExpression( bcVariant, context );
2153  const QString topColorExpr = parseColorExpression( tcVariant, context );
2154 
2155  caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2156  "%3, %4, %5, %6) " ).arg( bz, tz,
2157  interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_hue" ), colorComponent.arg( topColorExpr ).arg( "hsl_hue" ), base, 1, &context ),
2158  interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_saturation" ), colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ), base, 1, &context ),
2159  interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "lightness" ), colorComponent.arg( topColorExpr ).arg( "lightness" ), base, 1, &context ),
2160  interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "alpha" ), colorComponent.arg( topColorExpr ).arg( "alpha" ), base, 1, &context ) );
2161  }
2162  }
2163 
2164  // top color
2165  const QString tz = stops.last().toList().value( 0 ).toString();
2166  const QVariant tcVariant = stops.last().toList().value( 1 );
2167  const QColor topColor = parseColor( stops.last().toList().value( 1 ), context );
2168  if ( topColor.isValid() )
2169  {
2170  int tcHue;
2171  int tcSat;
2172  int tcLight;
2173  int tcAlpha;
2174  colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2175  caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2176  "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz ).arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );
2177  }
2178  else
2179  {
2180  const QString topColorExpr = parseColorExpression( tcVariant, context );
2181 
2182  caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2183  "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
2184  .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" ) );
2185  }
2186 
2187  if ( !stops.empty() && defaultColor )
2188  *defaultColor = parseColor( stops.value( 0 ).toList().value( 1 ).toString(), context );
2189 
2190  return QgsProperty::fromExpression( caseString );
2191 }
2192 
2193 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, double *defaultNumber )
2194 {
2195  const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2196  const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2197  if ( stops.empty() )
2198  return QgsProperty();
2199 
2200  QString scaleExpression;
2201  if ( stops.size() <= 2 )
2202  {
2203  scaleExpression = interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2204  stops.last().toList().value( 0 ).toDouble(),
2205  stops.value( 0 ).toList().value( 1 ),
2206  stops.last().toList().value( 1 ), base, multiplier, &context );
2207  }
2208  else
2209  {
2210  scaleExpression = parseStops( base, stops, multiplier, context );
2211  }
2212 
2213  if ( !stops.empty() && defaultNumber )
2214  *defaultNumber = stops.value( 0 ).toList().value( 1 ).toDouble() * multiplier;
2215 
2216  return QgsProperty::fromExpression( scaleExpression );
2217 }
2218 
2220 {
2222  if ( contextPtr )
2223  {
2224  context = *contextPtr;
2225  }
2226  const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2227  const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2228  if ( stops.empty() )
2229  return QgsProperty();
2230 
2231  QString scaleExpression;
2232  if ( stops.length() <= 2 )
2233  {
2234  const QVariant bv = stops.value( 0 ).toList().value( 1 );
2235  const QVariant tv = stops.last().toList().value( 1 );
2236  double bottom = 0.0;
2237  double top = 0.0;
2238  const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2239  scaleExpression = QStringLiteral( "set_color_part(@symbol_color, 'alpha', %1)" )
2240  .arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2241  stops.last().toList().value( 0 ).toDouble(),
2242  numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2243  numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ), base, 1, &context ) );
2244  }
2245  else
2246  {
2247  scaleExpression = parseOpacityStops( base, stops, maxOpacity, context );
2248  }
2249  return QgsProperty::fromExpression( scaleExpression );
2250 }
2251 
2252 QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context )
2253 {
2254  QString caseString = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
2255  .arg( stops.value( 0 ).toList().value( 0 ).toString() )
2256  .arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );
2257 
2258  for ( int i = 0; i < stops.size() - 1; ++i )
2259  {
2260  const QVariant bv = stops.value( i ).toList().value( 1 );
2261  const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2262  double bottom = 0.0;
2263  double top = 0.0;
2264  const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2265 
2266  caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
2267  "THEN set_color_part(@symbol_color, 'alpha', %3)" )
2268  .arg( stops.value( i ).toList().value( 0 ).toString(),
2269  stops.value( i + 1 ).toList().value( 0 ).toString(),
2270  interpolateExpression( stops.value( i ).toList().value( 0 ).toDouble(),
2271  stops.value( i + 1 ).toList().value( 0 ).toDouble(),
2272  numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2273  numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2274  base, 1, &context ) );
2275  }
2276 
2277  caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
2278  "THEN set_color_part(@symbol_color, 'alpha', %2) END" )
2279  .arg( stops.last().toList().value( 0 ).toString() )
2280  .arg( stops.last().toList().value( 1 ).toDouble() * maxOpacity );
2281  return caseString;
2282 }
2283 
2284 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, QPointF *defaultPoint )
2285 {
2286  const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2287  const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2288  if ( stops.empty() )
2289  return QgsProperty();
2290 
2291  QString scaleExpression;
2292  if ( stops.size() <= 2 )
2293  {
2294  scaleExpression = QStringLiteral( "array(%1,%2)" ).arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2295  stops.last().toList().value( 0 ).toDouble(),
2296  stops.value( 0 ).toList().value( 1 ).toList().value( 0 ),
2297  stops.last().toList().value( 1 ).toList().value( 0 ), base, multiplier, &context ),
2298  interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2299  stops.last().toList().value( 0 ).toDouble(),
2300  stops.value( 0 ).toList().value( 1 ).toList().value( 1 ),
2301  stops.last().toList().value( 1 ).toList().value( 1 ), base, multiplier, &context )
2302  );
2303  }
2304  else
2305  {
2306  scaleExpression = parsePointStops( base, stops, context, multiplier );
2307  }
2308 
2309  if ( !stops.empty() && defaultPoint )
2310  *defaultPoint = QPointF( stops.value( 0 ).toList().value( 1 ).toList().value( 0 ).toDouble() * multiplier,
2311  stops.value( 0 ).toList().value( 1 ).toList().value( 1 ).toDouble() * multiplier );
2312 
2313  return QgsProperty::fromExpression( scaleExpression );
2314 }
2315 
2317  const QVariantMap &conversionMap, QString *defaultString )
2318 {
2319  const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2320  if ( stops.empty() )
2321  return QgsProperty();
2322 
2323  const QString scaleExpression = parseStringStops( stops, context, conversionMap, defaultString );
2324 
2325  return QgsProperty::fromExpression( scaleExpression );
2326 }
2327 
2328 QString QgsMapBoxGlStyleConverter::parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier )
2329 {
2330  QString caseString = QStringLiteral( "CASE " );
2331 
2332  for ( int i = 0; i < stops.length() - 1; ++i )
2333  {
2334  // bottom zoom and value
2335  const QVariant bz = stops.value( i ).toList().value( 0 );
2336  const QVariant bv = stops.value( i ).toList().value( 1 );
2337  if ( bv.type() != QVariant::List && bv.type() != QVariant::StringList )
2338  {
2339  context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( bz.type() ) ) );
2340  return QString();
2341  }
2342 
2343  // top zoom and value
2344  const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2345  const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2346  if ( tv.type() != QVariant::List && tv.type() != QVariant::StringList )
2347  {
2348  context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( tz.type() ) ) );
2349  return QString();
2350  }
2351 
2352  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2353  "THEN array(%3,%4)" ).arg( bz.toString(),
2354  tz.toString(),
2355  interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ), tv.toList().value( 0 ), base, multiplier, &context ),
2356  interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ), tv.toList().value( 1 ), base, multiplier, &context ) );
2357  }
2358  caseString += QLatin1String( "END" );
2359  return caseString;
2360 }
2361 
2362 QString QgsMapBoxGlStyleConverter::parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &, double multiplier )
2363 {
2364  if ( stops.length() < 2 )
2365  return QString();
2366 
2367  QString caseString = QStringLiteral( "CASE " );
2368 
2369  for ( int i = 0; i < stops.length() - 1; ++i )
2370  {
2371  // bottom zoom and value
2372  const QVariant bz = stops.value( i ).toList().value( 0 );
2373  const QList<QVariant> bv = stops.value( i ).toList().value( 1 ).toList();
2374  QStringList bl;
2375  bool ok = false;
2376  for ( const QVariant &value : bv )
2377  {
2378  const double number = value.toDouble( &ok );
2379  if ( ok )
2380  bl << QString::number( number * multiplier );
2381  }
2382 
2383  // top zoom and value
2384  const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2385  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2386  "THEN array(%3) " ).arg( bz.toString(),
2387  tz.toString(),
2388  bl.join( ',' ) );
2389  }
2390  const QVariant lz = stops.value( stops.length() - 1 ).toList().value( 0 );
2391  const QList<QVariant> lv = stops.value( stops.length() - 1 ).toList().value( 1 ).toList();
2392  QStringList ll;
2393  bool ok = false;
2394  for ( const QVariant &value : lv )
2395  {
2396  const double number = value.toDouble( &ok );
2397  if ( ok )
2398  ll << QString::number( number * multiplier );
2399  }
2400  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2401  "THEN array(%2) " ).arg( lz.toString(),
2402  ll.join( ',' ) );
2403  caseString += QLatin1String( "END" );
2404  return caseString;
2405 }
2406 
2407 QString QgsMapBoxGlStyleConverter::parseStops( double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context )
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 QVariant bv = stops.value( i ).toList().value( 1 );
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 and value
2423  const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2424  const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2425  if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
2426  {
2427  context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2428  return QString();
2429  }
2430 
2431  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2432  "THEN %3 " ).arg( bz.toString(),
2433  tz.toString(),
2434  interpolateExpression( bz.toDouble(), tz.toDouble(), bv, tv, base, multiplier, &context ) );
2435  }
2436 
2437  const QVariant z = stops.last().toList().value( 0 );
2438  const QVariant v = stops.last().toList().value( 1 );
2439  QString vStr = v.toString();
2440  if ( ( QMetaType::Type )v.type() == QMetaType::QVariantList )
2441  {
2442  vStr = parseExpression( v.toList(), context );
2443  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2444  "THEN ( ( %2 ) * %3 ) END" ).arg( z.toString() ).arg( vStr ).arg( multiplier );
2445  }
2446  else
2447  {
2448  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2449  "THEN %2 END" ).arg( z.toString() ).arg( v.toDouble() * multiplier );
2450  }
2451 
2452  return caseString;
2453 }
2454 
2455 QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
2456 {
2457  QString caseString = QStringLiteral( "CASE " );
2458 
2459  for ( int i = 0; i < stops.length() - 1; ++i )
2460  {
2461  // bottom zoom and value
2462  const QVariant bz = stops.value( i ).toList().value( 0 );
2463  const QString bv = stops.value( i ).toList().value( 1 ).toString();
2464  if ( bz.type() == QVariant::List || bz.type() == QVariant::StringList )
2465  {
2466  context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2467  return QString();
2468  }
2469 
2470  // top zoom
2471  const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2472  if ( tz.type() == QVariant::List || tz.type() == QVariant::StringList )
2473  {
2474  context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2475  return QString();
2476  }
2477 
2478  caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2479  "THEN %3 " ).arg( bz.toString(),
2480  tz.toString(),
2481  QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
2482  }
2483  caseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( conversionMap.value( stops.constLast().toList().value( 1 ).toString(),
2484  stops.constLast().toList().value( 1 ) ) ) );
2485  if ( defaultString )
2486  *defaultString = stops.constLast().toList().value( 1 ).toString();
2487  return caseString;
2488 }
2489 
2490 QgsProperty QgsMapBoxGlStyleConverter::parseValueList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2491 {
2492  const QString method = json.value( 0 ).toString();
2493  if ( method == QLatin1String( "interpolate" ) )
2494  {
2495  return parseInterpolateListByZoom( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2496  }
2497  else if ( method == QLatin1String( "match" ) )
2498  {
2499  return parseMatchList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2500  }
2501  else
2502  {
2503  return QgsProperty::fromExpression( parseExpression( json, context ) );
2504  }
2505 }
2506 
2507 QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2508 {
2509  const QString attribute = parseExpression( json.value( 1 ).toList(), context );
2510  if ( attribute.isEmpty() )
2511  {
2512  context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
2513  return QgsProperty();
2514  }
2515 
2516  QString caseString = QStringLiteral( "CASE " );
2517 
2518  for ( int i = 2; i < json.length() - 1; i += 2 )
2519  {
2520  const QVariantList keys = json.value( i ).toList();
2521 
2522  QStringList matchString;
2523  for ( const QVariant &key : keys )
2524  {
2525  matchString << QgsExpression::quotedValue( key );
2526  }
2527 
2528  const QVariant value = json.value( i + 1 );
2529 
2530  QString valueString;
2531  switch ( type )
2532  {
2533  case Color:
2534  {
2535  const QColor color = parseColor( value, context );
2536  valueString = QgsExpression::quotedString( color.name() );
2537  break;
2538  }
2539 
2540  case Numeric:
2541  {
2542  const double v = value.toDouble() * multiplier;
2543  valueString = QString::number( v );
2544  break;
2545  }
2546 
2547  case Opacity:
2548  {
2549  const double v = value.toDouble() * maxOpacity;
2550  valueString = QString::number( v );
2551  break;
2552  }
2553 
2554  case Point:
2555  {
2556  valueString = QStringLiteral( "array(%1,%2)" ).arg( value.toList().value( 0 ).toDouble() * multiplier,
2557  value.toList().value( 0 ).toDouble() * multiplier );
2558  break;
2559  }
2560 
2561  }
2562 
2563  caseString += QStringLiteral( "WHEN %1 IN (%2) THEN %3 " ).arg( attribute,
2564  matchString.join( ',' ), valueString );
2565  }
2566 
2567 
2568  QString elseValue;
2569  switch ( type )
2570  {
2571  case Color:
2572  {
2573  const QColor color = parseColor( json.constLast(), context );
2574  if ( defaultColor )
2575  *defaultColor = color;
2576 
2577  elseValue = QgsExpression::quotedString( color.name() );
2578  break;
2579  }
2580 
2581  case Numeric:
2582  {
2583  const double v = json.constLast().toDouble() * multiplier;
2584  if ( defaultNumber )
2585  *defaultNumber = v;
2586  elseValue = QString::number( v );
2587  break;
2588  }
2589 
2590  case Opacity:
2591  {
2592  const double v = json.constLast().toDouble() * maxOpacity;
2593  if ( defaultNumber )
2594  *defaultNumber = v;
2595  elseValue = QString::number( v );
2596  break;
2597  }
2598 
2599  case Point:
2600  {
2601  elseValue = QStringLiteral( "array(%1,%2)" ).arg( json.constLast().toList().value( 0 ).toDouble() * multiplier,
2602  json.constLast().toList().value( 0 ).toDouble() * multiplier );
2603  break;
2604  }
2605 
2606  }
2607 
2608  caseString += QStringLiteral( "ELSE %1 END" ).arg( elseValue );
2609  return QgsProperty::fromExpression( caseString );
2610 }
2611 
2612 QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateListByZoom( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2613 {
2614  if ( json.value( 0 ).toString() != QLatin1String( "interpolate" ) )
2615  {
2616  context.pushWarning( QObject::tr( "%1: Could not interpret value list" ).arg( context.layerId() ) );
2617  return QgsProperty();
2618  }
2619 
2620  double base = 1;
2621  const QString technique = json.value( 1 ).toList().value( 0 ).toString();
2622  if ( technique == QLatin1String( "linear" ) )
2623  base = 1;
2624  else if ( technique == QLatin1String( "exponential" ) )
2625  base = json.value( 1 ).toList(). value( 1 ).toDouble();
2626  else if ( technique == QLatin1String( "cubic-bezier" ) )
2627  {
2628  context.pushWarning( QObject::tr( "%1: Cubic-bezier interpolation is not supported, linear used instead." ).arg( context.layerId() ) );
2629  base = 1;
2630  }
2631  else
2632  {
2633  context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation method %2" ).arg( context.layerId(), technique ) );
2634  return QgsProperty();
2635  }
2636 
2637  if ( json.value( 2 ).toList().value( 0 ).toString() != QLatin1String( "zoom" ) )
2638  {
2639  context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation input %2" ).arg( context.layerId(), json.value( 2 ).toString() ) );
2640  return QgsProperty();
2641  }
2642 
2643  // Convert stops into list of lists
2644  QVariantList stops;
2645  for ( int i = 3; i < json.length(); i += 2 )
2646  {
2647  stops.push_back( QVariantList() << json.value( i ).toString() << json.value( i + 1 ) );
2648  }
2649 
2650  QVariantMap props;
2651  props.insert( QStringLiteral( "stops" ), stops );
2652  props.insert( QStringLiteral( "base" ), base );
2653  switch ( type )
2654  {
2655  case PropertyType::Color:
2656  return parseInterpolateColorByZoom( props, context, defaultColor );
2657 
2658  case PropertyType::Numeric:
2659  return parseInterpolateByZoom( props, context, multiplier, defaultNumber );
2660 
2661  case PropertyType::Opacity:
2662  return parseInterpolateOpacityByZoom( props, maxOpacity, &context );
2663 
2664  case PropertyType::Point:
2665  return parseInterpolatePointByZoom( props, context, multiplier );
2666  }
2667  return QgsProperty();
2668 }
2669 
2671 {
2672  if ( ( QMetaType::Type )colorExpression.type() == QMetaType::QVariantList )
2673  {
2674  return parseExpression( colorExpression.toList(), context, true );
2675  }
2676  return parseValue( colorExpression, context, true );
2677 }
2678 
2680 {
2681  if ( color.type() != QVariant::String )
2682  {
2683  context.pushWarning( QObject::tr( "%1: Could not parse non-string color %2, skipping" ).arg( context.layerId(), color.toString() ) );
2684  return QColor();
2685  }
2686 
2687  return QgsSymbolLayerUtils::parseColor( color.toString() );
2688 }
2689 
2690 void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
2691 {
2692  hue = std::max( 0, color.hslHue() );
2693  saturation = color.hslSaturation() / 255.0 * 100;
2694  lightness = color.lightness() / 255.0 * 100;
2695  alpha = color.alpha();
2696 }
2697 
2698 QString QgsMapBoxGlStyleConverter::interpolateExpression( double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier, QgsMapBoxGlStyleConversionContext *contextPtr )
2699 {
2701  if ( contextPtr )
2702  {
2703  context = *contextPtr;
2704  }
2705 
2706  // special case!
2707  if ( valueMin.canConvert( QMetaType::Double ) && valueMax.canConvert( QMetaType::Double ) )
2708  {
2709  bool minDoubleOk = true;
2710  const double min = valueMin.toDouble( &minDoubleOk );
2711  bool maxDoubleOk = true;
2712  const double max = valueMax.toDouble( &maxDoubleOk );
2713  if ( minDoubleOk && maxDoubleOk && qgsDoubleNear( min, max ) )
2714  {
2715  return QString::number( min * multiplier );
2716  }
2717  }
2718 
2719  QString minValueExpr = valueMin.toString();
2720  QString maxValueExpr = valueMax.toString();
2721  if ( ( QMetaType::Type )valueMin.type() == QMetaType::QVariantList )
2722  {
2723  minValueExpr = parseExpression( valueMin.toList(), context );
2724  }
2725  if ( ( QMetaType::Type )valueMax.type() == QMetaType::QVariantList )
2726  {
2727  maxValueExpr = parseExpression( valueMax.toList(), context );
2728  }
2729 
2730  if ( minValueExpr == maxValueExpr )
2731  {
2732  return minValueExpr;
2733  }
2734 
2735  QString expression;
2736  if ( base == 1 )
2737  {
2738  expression = QStringLiteral( "scale_linear(@vector_tile_zoom,%1,%2,%3,%4)" ).arg( zoomMin )
2739  .arg( zoomMax )
2740  .arg( minValueExpr )
2741  .arg( maxValueExpr );
2742  }
2743  else
2744  {
2745  expression = QStringLiteral( "scale_exp(@vector_tile_zoom,%1,%2,%3,%4,%5)" ).arg( zoomMin )
2746  .arg( zoomMax )
2747  .arg( minValueExpr )
2748  .arg( maxValueExpr )
2749  .arg( base );
2750  }
2751 
2752  if ( multiplier != 1 )
2753  return QStringLiteral( "%1 * %2" ).arg( expression ).arg( multiplier );
2754  else
2755  return expression;
2756 }
2757 
2758 Qt::PenCapStyle QgsMapBoxGlStyleConverter::parseCapStyle( const QString &style )
2759 {
2760  if ( style == QLatin1String( "round" ) )
2761  return Qt::RoundCap;
2762  else if ( style == QLatin1String( "square" ) )
2763  return Qt::SquareCap;
2764  else
2765  return Qt::FlatCap; // "butt" is default
2766 }
2767 
2768 Qt::PenJoinStyle QgsMapBoxGlStyleConverter::parseJoinStyle( const QString &style )
2769 {
2770  if ( style == QLatin1String( "bevel" ) )
2771  return Qt::BevelJoin;
2772  else if ( style == QLatin1String( "round" ) )
2773  return Qt::RoundJoin;
2774  else
2775  return Qt::MiterJoin; // "miter" is default
2776 }
2777 
2778 QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
2779 {
2780  QString op = expression.value( 0 ).toString();
2781  if ( op == QLatin1String( "%" ) && expression.size() >= 3 )
2782  {
2783  return QStringLiteral( "%1 %2 %3" ).arg( parseValue( expression.value( 1 ), context ) ).arg( op ).arg( parseValue( expression.value( 2 ), context ) );
2784  }
2785  else if ( op == QLatin1String( "to-number" ) )
2786  {
2787  return QStringLiteral( "to_real(%1)" ).arg( parseValue( expression.value( 1 ), context ) );
2788  }
2789  if ( op == QLatin1String( "literal" ) )
2790  {
2791  return expression.value( 1 ).toString();
2792  }
2793  else if ( op == QLatin1String( "all" )
2794  || op == QLatin1String( "any" )
2795  || op == QLatin1String( "none" ) )
2796  {
2797  QStringList parts;
2798  for ( int i = 1; i < expression.size(); ++i )
2799  {
2800  const QString part = parseValue( expression.at( i ), context );
2801  if ( part.isEmpty() )
2802  {
2803  context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2804  return QString();
2805  }
2806  parts << part;
2807  }
2808 
2809  if ( op == QLatin1String( "none" ) )
2810  return QStringLiteral( "NOT (%1)" ).arg( parts.join( QLatin1String( ") AND NOT (" ) ) );
2811 
2812  QString operatorString;
2813  if ( op == QLatin1String( "all" ) )
2814  operatorString = QStringLiteral( ") AND (" );
2815  else if ( op == QLatin1String( "any" ) )
2816  operatorString = QStringLiteral( ") OR (" );
2817 
2818  return QStringLiteral( "(%1)" ).arg( parts.join( operatorString ) );
2819  }
2820  else if ( op == '!' )
2821  {
2822  // ! inverts next expression's meaning
2823  QVariantList contraJsonExpr = expression.value( 1 ).toList();
2824  contraJsonExpr[0] = QString( op + contraJsonExpr[0].toString() );
2825  // ['!', ['has', 'level']] -> ['!has', 'level']
2826  return parseKey( contraJsonExpr, context );
2827  }
2828  else if ( op == QLatin1String( "==" )
2829  || op == QLatin1String( "!=" )
2830  || op == QLatin1String( ">=" )
2831  || op == '>'
2832  || op == QLatin1String( "<=" )
2833  || op == '<' )
2834  {
2835  // use IS and NOT IS instead of = and != because they can deal with NULL values
2836  if ( op == QLatin1String( "==" ) )
2837  op = QStringLiteral( "IS" );
2838  else if ( op == QLatin1String( "!=" ) )
2839  op = QStringLiteral( "IS NOT" );
2840  return QStringLiteral( "%1 %2 %3" ).arg( parseKey( expression.value( 1 ), context ),
2841  op, parseValue( expression.value( 2 ), context ) );
2842  }
2843  else if ( op == QLatin1String( "has" ) )
2844  {
2845  return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NOT NULL" );
2846  }
2847  else if ( op == QLatin1String( "!has" ) )
2848  {
2849  return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NULL" );
2850  }
2851  else if ( op == QLatin1String( "in" ) || op == QLatin1String( "!in" ) )
2852  {
2853  const QString key = parseKey( expression.value( 1 ), context );
2854  QStringList parts;
2855  for ( int i = 2; i < expression.size(); ++i )
2856  {
2857  const QString part = parseValue( expression.at( i ), context );
2858  if ( part.isEmpty() )
2859  {
2860  context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2861  return QString();
2862  }
2863  parts << part;
2864  }
2865  if ( op == QLatin1String( "in" ) )
2866  return QStringLiteral( "%1 IN (%2)" ).arg( key, parts.join( QLatin1String( ", " ) ) );
2867  else
2868  return QStringLiteral( "(%1 IS NULL OR %1 NOT IN (%2))" ).arg( key, parts.join( QLatin1String( ", " ) ) );
2869  }
2870  else if ( op == QLatin1String( "get" ) )
2871  {
2872  return parseKey( expression.value( 1 ), context );
2873  }
2874  else if ( op == QLatin1String( "match" ) )
2875  {
2876  const QString attribute = expression.value( 1 ).toList().value( 1 ).toString();
2877 
2878  if ( expression.size() == 5
2879  && expression.at( 3 ).type() == QVariant::Bool && expression.at( 3 ).toBool() == true
2880  && expression.at( 4 ).type() == QVariant::Bool && expression.at( 4 ).toBool() == false )
2881  {
2882  // simple case, make a nice simple expression instead of a CASE statement
2883  if ( expression.at( 2 ).type() == QVariant::List || expression.at( 2 ).type() == QVariant::StringList )
2884  {
2885  QStringList parts;
2886  for ( const QVariant &p : expression.at( 2 ).toList() )
2887  {
2888  parts << parseValue( p, context );
2889  }
2890 
2891  if ( parts.size() > 1 )
2892  return QStringLiteral( "%1 IN (%2)" ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
2893  else
2894  return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ).toList().value( 0 ) );
2895  }
2896  else if ( expression.at( 2 ).type() == QVariant::String || expression.at( 2 ).type() == QVariant::Int
2897  || expression.at( 2 ).type() == QVariant::Double )
2898  {
2899  return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ) );
2900  }
2901  else
2902  {
2903  context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2904  return QString();
2905  }
2906  }
2907  else
2908  {
2909  QString caseString = QStringLiteral( "CASE " );
2910  for ( int i = 2; i < expression.size() - 2; i += 2 )
2911  {
2912  if ( expression.at( i ).type() == QVariant::List || expression.at( i ).type() == QVariant::StringList )
2913  {
2914  QStringList parts;
2915  for ( const QVariant &p : expression.at( i ).toList() )
2916  {
2917  parts << QgsExpression::quotedValue( p );
2918  }
2919 
2920  if ( parts.size() > 1 )
2921  caseString += QStringLiteral( "WHEN %1 IN (%2) " ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
2922  else
2923  caseString += QStringLiteral( "WHEN %1 " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ).toList().value( 0 ) ) );
2924  }
2925  else if ( expression.at( i ).type() == QVariant::String || expression.at( i ).type() == QVariant::Int
2926  || expression.at( i ).type() == QVariant::Double )
2927  {
2928  caseString += QStringLiteral( "WHEN (%1) " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ) ) );
2929  }
2930 
2931  caseString += QStringLiteral( "THEN %1 " ).arg( parseValue( expression.at( i + 1 ), context, colorExpected ) );
2932  }
2933  caseString += QStringLiteral( "ELSE %1 END" ).arg( parseValue( expression.last(), context, colorExpected ) );
2934  return caseString;
2935  }
2936  }
2937  else if ( op == QLatin1String( "to-string" ) )
2938  {
2939  return QStringLiteral( "to_string(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
2940  }
2941  else
2942  {
2943  context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
2944  return QString();
2945  }
2946 }
2947 
2948 QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
2949 {
2950  if ( context.spriteImage().isNull() )
2951  {
2952  context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2953  return QImage();
2954  }
2955 
2956  const QVariantMap spriteDefinition = context.spriteDefinitions().value( name ).toMap();
2957  if ( spriteDefinition.size() == 0 )
2958  {
2959  context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2960  return QImage();
2961  }
2962 
2963  const QImage sprite = context.spriteImage().copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
2964  spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
2965  spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
2966  spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
2967  if ( sprite.isNull() )
2968  {
2969  context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
2970  return QImage();
2971  }
2972 
2973  spriteSize = sprite.size() / spriteDefinition.value( QStringLiteral( "pixelRatio" ) ).toDouble() * context.pixelSizeConversionFactor();
2974  return sprite;
2975 }
2976 
2977 QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty )
2978 {
2979  QString spritePath;
2980 
2981  auto prepareBase64 = []( const QImage & sprite )
2982  {
2983  QString path;
2984  if ( !sprite.isNull() )
2985  {
2986  QByteArray blob;
2987  QBuffer buffer( &blob );
2988  buffer.open( QIODevice::WriteOnly );
2989  sprite.save( &buffer, "PNG" );
2990  buffer.close();
2991  const QByteArray encoded = blob.toBase64();
2992  path = QString( encoded );
2993  path.prepend( QLatin1String( "base64:" ) );
2994  }
2995  return path;
2996  };
2997 
2998  switch ( value.type() )
2999  {
3000  case QVariant::String:
3001  {
3002  QString spriteName = value.toString();
3003  const QRegularExpression fieldNameMatch( QStringLiteral( "{([^}]+)}" ) );
3004  QRegularExpressionMatch match = fieldNameMatch.match( spriteName );
3005  if ( match.hasMatch() )
3006  {
3007  const QString fieldName = match.captured( 1 );
3008  spriteProperty = QStringLiteral( "CASE" );
3009  spriteSizeProperty = QStringLiteral( "CASE" );
3010 
3011  spriteName.replace( "(", QLatin1String( "\\(" ) );
3012  spriteName.replace( ")", QLatin1String( "\\)" ) );
3013  spriteName.replace( fieldNameMatch, QStringLiteral( "([^\\/\\\\]+)" ) );
3014  const QRegularExpression fieldValueMatch( spriteName );
3015  const QStringList spriteNames = context.spriteDefinitions().keys();
3016  for ( const QString &name : spriteNames )
3017  {
3018  match = fieldValueMatch.match( name );
3019  if ( match.hasMatch() )
3020  {
3021  QSize size;
3022  QString path;
3023  const QString fieldValue = match.captured( 1 );
3024  const QImage sprite = retrieveSprite( name, context, size );
3025  path = prepareBase64( sprite );
3026  if ( spritePath.isEmpty() && !path.isEmpty() )
3027  {
3028  spritePath = path;
3029  spriteSize = size;
3030  }
3031 
3032  spriteProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN '%3'" )
3033  .arg( fieldName, fieldValue, path );
3034  spriteSizeProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN %3" )
3035  .arg( fieldName ).arg( fieldValue ).arg( size.width() );
3036  }
3037  }
3038 
3039  spriteProperty += QLatin1String( " END" );
3040  spriteSizeProperty += QLatin1String( " END" );
3041  }
3042  else
3043  {
3044  spriteProperty.clear();
3045  spriteSizeProperty.clear();
3046  const QImage sprite = retrieveSprite( spriteName, context, spriteSize );
3047  spritePath = prepareBase64( sprite );
3048  }
3049  break;
3050  }
3051 
3052  case QVariant::Map:
3053  {
3054  const QVariantList stops = value.toMap().value( QStringLiteral( "stops" ) ).toList();
3055  if ( stops.size() == 0 )
3056  break;
3057 
3058  QString path;
3059  QSize size;
3060  QImage sprite;
3061 
3062  sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
3063  spritePath = prepareBase64( sprite );
3064 
3065  spriteProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN '%2'" )
3066  .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3067  .arg( spritePath );
3068  spriteSizeProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN %2" )
3069  .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3070  .arg( spriteSize.width() );
3071 
3072  for ( int i = 0; i < stops.size() - 1; ++i )
3073  {
3074  ;
3075  sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
3076  path = prepareBase64( sprite );
3077 
3078  spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3079  "THEN '%3'" )
3080  .arg( stops.value( i ).toList().value( 0 ).toString(),
3081  stops.value( i + 1 ).toList().value( 0 ).toString(),
3082  path );
3083  spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3084  "THEN %3" )
3085  .arg( stops.value( i ).toList().value( 0 ).toString(),
3086  stops.value( i + 1 ).toList().value( 0 ).toString() )
3087  .arg( size.width() );
3088  }
3089  sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
3090  path = prepareBase64( sprite );
3091 
3092  spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3093  "THEN '%2' END" )
3094  .arg( stops.last().toList().value( 0 ).toString() )
3095  .arg( path );
3096  spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3097  "THEN %2 END" )
3098  .arg( stops.last().toList().value( 0 ).toString() )
3099  .arg( size.width() );
3100  break;
3101  }
3102 
3103  case QVariant::List:
3104  {
3105  const QVariantList json = value.toList();
3106  const QString method = json.value( 0 ).toString();
3107  if ( method != QLatin1String( "match" ) )
3108  {
3109  context.pushWarning( QObject::tr( "%1: Could not interpret sprite value list with method %2" ).arg( context.layerId(), method ) );
3110  break;
3111  }
3112 
3113  const QString attribute = parseExpression( json.value( 1 ).toList(), context );
3114  if ( attribute.isEmpty() )
3115  {
3116  context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
3117  break;
3118  }
3119 
3120  spriteProperty = QStringLiteral( "CASE " );
3121  spriteSizeProperty = QStringLiteral( "CASE " );
3122 
3123  for ( int i = 2; i < json.length() - 1; i += 2 )
3124  {
3125  const QVariantList keys = json.value( i ).toList();
3126 
3127  QStringList matchString;
3128  for ( const QVariant &key : keys )
3129  {
3130  matchString << QgsExpression::quotedValue( key );
3131  }
3132 
3133  const QVariant value = json.value( i + 1 );
3134 
3135  const QImage sprite = retrieveSprite( value.toString(), context, spriteSize );
3136  spritePath = prepareBase64( sprite );
3137 
3138  spriteProperty += QStringLiteral( " WHEN %1 IN (%2) "
3139  "THEN '%3' " ).arg( attribute,
3140  matchString.join( ',' ),
3141  spritePath );
3142 
3143  spriteSizeProperty += QStringLiteral( " WHEN %1 IN (%2) "
3144  "THEN %3 " ).arg( attribute,
3145  matchString.join( ',' ) ).arg( spriteSize.width() );
3146  }
3147 
3148  const QImage sprite = retrieveSprite( json.constLast().toString(), context, spriteSize );
3149  spritePath = prepareBase64( sprite );
3150 
3151  spriteProperty += QStringLiteral( "ELSE %1 END" ).arg( spritePath );
3152  spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3153  break;
3154  }
3155 
3156  default:
3157  context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( value.type() ) ) );
3158  break;
3159  }
3160 
3161  return spritePath;
3162 }
3163 
3164 QString QgsMapBoxGlStyleConverter::parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3165 {
3166  QColor c;
3167  switch ( value.type() )
3168  {
3169  case QVariant::List:
3170  case QVariant::StringList:
3171  return parseExpression( value.toList(), context, colorExpected );
3172 
3173  case QVariant::Bool:
3174  case QVariant::String:
3175  if ( colorExpected )
3176  {
3177  QColor c = parseColor( value, context );
3178  if ( c.isValid() )
3179  {
3180  return parseValue( c, context );
3181  }
3182  }
3183  return QgsExpression::quotedValue( value );
3184 
3185  case QVariant::Int:
3186  case QVariant::Double:
3187  return value.toString();
3188 
3189  case QVariant::Color:
3190  c = value.value<QColor>();
3191  return QString( "color_rgba(%1,%2,%3,%4)" ).arg( c.red() ).arg( c.green() ).arg( c.blue() ).arg( c.alpha() );
3192 
3193  default:
3194  context.pushWarning( QObject::tr( "%1: Skipping unsupported expression part" ).arg( context.layerId() ) );
3195  break;
3196  }
3197  return QString();
3198 }
3199 
3200 QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context )
3201 {
3202  if ( value.toString() == QLatin1String( "$type" ) )
3203  {
3204  return QStringLiteral( "_geom_type" );
3205  }
3206  if ( value.toString() == QLatin1String( "level" ) )
3207  {
3208  return QStringLiteral( "level" );
3209  }
3210  else if ( ( value.type() == QVariant::List && value.toList().size() == 1 ) || value.type() == QVariant::StringList )
3211  {
3212  if ( value.toList().size() > 1 )
3213  return value.toList().at( 1 ).toString();
3214  else
3215  {
3216  QString valueString = value.toList().value( 0 ).toString();
3217  if ( valueString == QLatin1String( "geometry-type" ) )
3218  {
3219  return QStringLiteral( "_geom_type" );
3220  }
3221  return valueString;
3222  }
3223  }
3224  else if ( value.type() == QVariant::List && value.toList().size() > 1 )
3225  {
3226  return parseExpression( value.toList(), context );
3227  }
3228  return QgsExpression::quotedColumnRef( value.toString() );
3229 }
3230 
3232 {
3233  return mRenderer ? mRenderer->clone() : nullptr;
3234 }
3235 
3237 {
3238  return mLabeling ? mLabeling->clone() : nullptr;
3239 }
3240 
3241 bool QgsMapBoxGlStyleConverter::numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top )
3242 {
3243  if ( bottomVariant.canConvert( QMetaType::Double ) && topVariant.canConvert( QMetaType::Double ) )
3244  {
3245  bool bDoubleOk, tDoubleOk;
3246  bottom = bottomVariant.toDouble( &bDoubleOk );
3247  top = topVariant.toDouble( &tDoubleOk );
3248  return ( bDoubleOk && tDoubleOk );
3249  }
3250  return false;
3251 }
3252 
3253 //
3254 // QgsMapBoxGlStyleConversionContext
3255 //
3257 {
3258  QgsDebugMsg( warning );
3259  mWarnings << warning;
3260 }
3261 
3263 {
3264  return mTargetUnit;
3265 }
3266 
3268 {
3269  mTargetUnit = targetUnit;
3270 }
3271 
3273 {
3274  return mSizeConversionFactor;
3275 }
3276 
3278 {
3279  mSizeConversionFactor = sizeConversionFactor;
3280 }
3281 
3283 {
3284  return mSpriteImage;
3285 }
3286 
3288 {
3289  return mSpriteDefinitions;
3290 }
3291 
3292 void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions )
3293 {
3294  mSpriteImage = image;
3295  mSpriteDefinitions = definitions;
3296 }
3297 
3298 void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions )
3299 {
3300  setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap() );
3301 }
3302 
3304 {
3305  return mLayerId;
3306 }
3307 
3309 {
3310  mLayerId = value;
3311 }
@ CentralPoint
Place symbols at the mid point of the line.
@ Viewport
Relative to the whole viewport/output device.
void setPenJoinStyle(Qt::PenJoinStyle style)
Sets the pen join style used to render the line (e.g.
void setPenCapStyle(Qt::PenCapStyle style)
Sets the pen cap style used to render the line (e.g.
A paint effect which blurs a source picture, using a number of different blur methods.
Definition: qgsblureffect.h:38
@ StackBlur
Stack blur, a fast but low quality blur. Valid blur level values are between 0 - 16.
Definition: qgsblureffect.h:45
void setBlurUnit(const QgsUnitTypes::RenderUnit unit)
Sets the units used for the blur level (radius).
Definition: qgsblureffect.h:96
void setBlurMethod(const BlurMethod method)
Sets the blur method (algorithm) to use for performing the blur.
void setBlurLevel(const double level)
Sets blur level (radius)
Definition: qgsblureffect.h:75
A paint effect which consists of a stack of other chained paint effects.
void appendEffect(QgsPaintEffect *effect)
Appends an effect to the end of the stack.
static QString quotedValue(const QVariant &value)
Returns a string representation of a literal value, including appropriate quotations where required.
static QString quotedString(QString text)
Returns a quoted version of a string (in single quotes)
static QString createFieldEqualityExpression(const QString &fieldName, const QVariant &value, QVariant::Type fieldType=QVariant::Type::Invalid)
Create an expression allowing to evaluate if a field is equal to a value.
static QString quotedColumnRef(QString name)
Returns a quoted column reference (in double quotes)
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
void setPlacementFlags(QgsLabeling::LinePlacementFlags flags)
Returns the line placement flags, which dictate how line labels can be placed above or below the line...
void setFactor(double factor)
Sets the obstacle factor, where 1.0 = default, < 1.0 more likely to be covered by labels,...
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition: qgslabeling.h:41
@ OnLine
Labels can be placed directly over a line feature.
Definition: qgslabeling.h:40
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition: qgslabeling.h:42
virtual void setWidth(double width)
Sets the width of the line symbol layer.
void setOffsetUnit(QgsUnitTypes::RenderUnit unit)
Sets the unit for the line's offset.
void setOffset(double offset)
Sets the line's offset.
Context for a MapBox GL style conversion operation.
void setLayerId(const QString &value)
Sets the layer ID of the layer currently being converted.
QStringList warnings() const
Returns a list of warning messages generated during the conversion.
void pushWarning(const QString &warning)
Pushes a warning message generated during the conversion.
double pixelSizeConversionFactor() const
Returns the pixel size conversion factor, used to scale the original pixel sizes when converting styl...
QgsUnitTypes::RenderUnit targetUnit() const
Returns the target unit type.
void setPixelSizeConversionFactor(double sizeConversionFactor)
Sets the pixel size conversion factor, used to scale the original pixel sizes when converting styles.
void setSprites(const QImage &image, const QVariantMap &definitions)
Sets the sprite image and definitions JSON to use during conversion.
QString layerId() const
Returns the layer ID of the layer currently being converted.
void setTargetUnit(QgsUnitTypes::RenderUnit targetUnit)
Sets the target unit type.
QImage spriteImage() const
Returns the sprite image to use during conversion, or an invalid image if this is not set.
void clearWarnings()
Clears the list of warning messages.
QVariantMap spriteDefinitions() const
Returns the sprite definitions to use during conversion.
static QString parseOpacityStops(double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context)
Takes values from stops and uses either scale_linear() or scale_exp() functions to interpolate alpha ...
static QString parseColorExpression(const QVariant &colorExpression, QgsMapBoxGlStyleConversionContext &context)
Converts an expression representing a color to a string (can be color string or an expression where a...
static QString parseStops(double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context)
Parses a list of interpolation stops.
QgsVectorTileRenderer * renderer() const
Returns a new instance of a vector tile renderer representing the converted style,...
static QString parseExpression(const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected=false)
Converts a MapBox GL expression to a QGIS expression.
PropertyType
Property types, for interpolated value conversion.
@ Numeric
Numeric property (e.g. line width, text size)
QgsVectorTileLabeling * labeling() const
Returns a new instance of a vector tile labeling representing the converted style,...
static QgsProperty parseInterpolateByZoom(const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier=1, double *defaultNumber=nullptr)
Parses a numeric value which is interpolated by zoom range.
static Qt::PenJoinStyle parseJoinStyle(const QString &style)
Converts a value to Qt::PenJoinStyle enum from JSON value.
static QgsProperty parseInterpolateStringByZoom(const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString=nullptr)
Interpolates a string by zoom.
static QString interpolateExpression(double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier=1, QgsMapBoxGlStyleConversionContext *contextPtr=0)
Generates an interpolation for values between valueMin and valueMax, scaled between the ranges zoomMi...
static QgsProperty parseInterpolatePointByZoom(const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier=1, QPointF *defaultPoint=nullptr)
Interpolates a point/offset with either scale_linear() or scale_exp() (depending on base value).
static bool parseCircleLayer(const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context)
Parses a circle layer.
Result convert(const QVariantMap &style, QgsMapBoxGlStyleConversionContext *context=nullptr)
Converts a JSON style map, and returns the resultant status of the conversion.
static QgsProperty parseInterpolateListByZoom(const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier=1, int maxOpacity=255, QColor *defaultColor=nullptr, double *defaultNumber=nullptr)
Interpolates a list which starts with the interpolate function.
@ Success
Conversion was successful.
@ NoLayerList
No layer list was found in JSON input.
QgsMapBoxGlStyleConverter()
Constructor for QgsMapBoxGlStyleConverter.
static QImage retrieveSprite(const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize)
Retrieves the sprite image with the specified name, taken from the specified context.
void parseLayers(const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context=nullptr)
Parse list of layers from JSON.
static QgsProperty parseInterpolateColorByZoom(const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, QColor *defaultColor=nullptr)
Parses a color value which is interpolated by zoom range.
static QgsProperty parseInterpolateOpacityByZoom(const QVariantMap &json, int maxOpacity, QgsMapBoxGlStyleConversionContext *contextPtr=0)
Interpolates opacity with either scale_linear() or scale_exp() (depending on base value).
static QString retrieveSpriteAsBase64(const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty)
Retrieves the sprite image with the specified name, taken from the specified context as a base64 enco...
static QColor parseColor(const QVariant &color, QgsMapBoxGlStyleConversionContext &context)
Parses a color in one of these supported formats:
static bool parseSymbolLayerAsRenderer(const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &rendererStyle, QgsMapBoxGlStyleConversionContext &context)
Parses a symbol layer as a renderer.
static bool parseFillLayer(const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context, bool isBackgroundStyle=false)
Parses a fill layer.
static void parseSymbolLayer(const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &rendererStyle, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context)
Parses a symbol layer as renderer or labeling.
static bool parseLineLayer(const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context)
Parses a line layer.
static void colorAsHslaComponents(const QColor &color, int &hue, int &saturation, int &lightness, int &alpha)
Takes a QColor object and returns HSLA components in required format for QGIS color_hsla() expression...
static Qt::PenCapStyle parseCapStyle(const QString &style)
Converts a value to Qt::PenCapStyle enum from JSON value.
static QString parseStringStops(const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString=nullptr)
Parses a list of interpolation stops containing string values.
static QgsProperty parseMatchList(const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier=1, int maxOpacity=255, QColor *defaultColor=nullptr, double *defaultNumber=nullptr)
Parses and converts a match function value list.
static QgsProperty parseValueList(const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier=1, int maxOpacity=255, QColor *defaultColor=nullptr, double *defaultNumber=nullptr)
Parses and converts a value list (e.g.
static QString parseArrayStops(const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier=1)
Takes numerical arrays from stops.
static QString parsePointStops(double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier=1)
Takes values from stops and uses either scale_linear() or scale_exp() functions to interpolate point/...
Line symbol layer type which draws repeating marker symbols along a line feature.
bool setSubSymbol(QgsSymbol *symbol) override
Sets layer's subsymbol. takes ownership of the passed symbol.
void setOutputUnit(QgsUnitTypes::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
virtual void setSize(double size)
Sets the symbol size.
void setAngle(double angle)
Sets the rotation angle for the marker.
void setSizeUnit(QgsUnitTypes::RenderUnit unit)
Sets the units for the symbol's size.
void setOffset(QPointF offset)
Sets the marker's offset, which is the horizontal and vertical displacement which the rendered marker...
void setOffsetUnit(QgsUnitTypes::RenderUnit unit)
Sets the units for the symbol's offset.
A marker symbol type, for rendering Point and MultiPoint geometries.
void setEnabled(bool enabled)
Sets whether the effect is enabled.
Contains settings for how a map layer will be labeled.
double yOffset
Vertical offset of label.
void setFormat(const QgsTextFormat &format)
Sets the label text formatting settings, e.g., font settings, buffer settings, etc.
double xOffset
Horizontal offset of label.
QuadrantPosition quadOffset
Sets the quadrant in which to offset labels from feature.
QgsUnitTypes::RenderUnit offsetUnits
Units for offsets of label.
@ Curved
Arranges candidates following the curvature of a line feature. Applies to line layers only.
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature. Applies to polygon layers only...
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
int priority
Label priority.
QgsUnitTypes::RenderUnit distUnits
Units the distance from feature to the label.
MultiLineAlign multilineAlign
Horizontal alignment of multi-line labels.
@ FontStyle
Font style name.
@ FontLetterSpacing
Letter spacing.
@ Family
Font family.
@ LinePlacementOptions
Line placement flags.
const QgsLabelLineSettings & lineSettings() const
Returns the label line settings, which contain settings related to how the label engine places and fo...
const QgsLabelObstacleSettings & obstacleSettings() const
Returns the label obstacle settings.
void setDataDefinedProperties(const QgsPropertyCollection &collection)
Sets the label's property collection, used for data defined overrides.
bool isExpression
true if this label is made from a expression string, e.g., FieldName || 'mm'
double dist
Distance from feature to the label.
QString fieldName
Name of field (or an expression) to use for label text.
int autoWrapLength
If non-zero, indicates that label text should be automatically wrapped to (ideally) the specified num...
A grouped map of multiple QgsProperty objects, each referenced by a integer key value.
QgsProperty property(int key) const override
Returns a matching property from the collection, if one exists.
QVariant value(int key, const QgsExpressionContext &context, const QVariant &defaultValue=QVariant()) const override
Returns the calculated value of the property with the specified key from within the collection.
void setProperty(int key, const QgsProperty &property)
Adds a property to the collection and takes ownership of it.
bool isActive(int key) const override
Returns true if the collection contains an active property with the specified key.
A store for object properties.
Definition: qgsproperty.h:231
QString asExpression() const
Returns an expression string representing the state of the property, or an empty string if the proper...
QString expressionString() const
Returns the expression used for the property value.
QVariant value(const QgsExpressionContext &context, const QVariant &defaultValue=QVariant(), bool *ok=nullptr) const
Calculates the current value of the property, including any transforms which are set for the property...
static QgsProperty fromExpression(const QString &expression, bool isActive=true)
Returns a new ExpressionBasedProperty created from the specified expression.
A class for filling symbols with a repeated raster image.
void setWidthUnit(const QgsUnitTypes::RenderUnit unit)
Sets the units for the image's width.
void setWidth(const double width)
Sets the width for scaling the image used in the fill.
void setOpacity(double opacity)
Sets the opacity for the raster image used in the fill.
void setImageFilePath(const QString &imagePath)
Sets the path to the raster image used for the fill.
void setCoordinateMode(Qgis::SymbolCoordinateReference mode)
Set the coordinate mode for fill.
Line symbol layer type which draws line sections using a raster image file.
void setOutputUnit(QgsUnitTypes::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
Raster marker symbol layer class.
void setOpacity(double opacity)
Set the marker opacity.
void setPath(const QString &path)
Set the marker raster image path.
void setBrushStyle(Qt::BrushStyle style)
void setStrokeStyle(Qt::PenStyle strokeStyle)
void setFillColor(const QColor &color) override
Sets the fill color for the symbol layer.
void setOffset(QPointF offset)
Sets an offset by which polygons will be translated during rendering.
void setStrokeColor(const QColor &strokeColor) override
Sets the stroke color for the symbol layer.
void setOutputUnit(QgsUnitTypes::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
void setOffsetUnit(QgsUnitTypes::RenderUnit unit)
Sets the unit for the fill's offset.
A simple line symbol layer, which renders lines using a line in a variety of styles (e....
void setPenCapStyle(Qt::PenCapStyle style)
Sets the pen cap style used to render the line (e.g.
void setUseCustomDashPattern(bool b)
Sets whether the line uses a custom dash pattern.
void setCustomDashVector(const QVector< qreal > &vector)
Sets the custom dash vector, which is the pattern of alternating drawn/skipped lengths used while ren...
void setPenJoinStyle(Qt::PenJoinStyle style)
Sets the pen join style used to render the line (e.g.
void setOutputUnit(QgsUnitTypes::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
Simple marker symbol layer, consisting of a rendered shape with solid fill color and an stroke.
void setStrokeWidthUnit(QgsUnitTypes::RenderUnit u)
Sets the unit for the width of the marker's stroke.
void setFillColor(const QColor &color) override
Sets the fill color for the symbol layer.
void setStrokeWidth(double w)
Sets the width of the marker's stroke.
void setStrokeColor(const QColor &color) override
Sets the marker's stroke color.
static QColor parseColor(const QString &colorStr, bool strictEval=false)
Attempts to parse a string as a color using a variety of common formats, including hex codes,...
@ PropertyFile
Filename, eg for svg files.
@ PropertyAngle
Symbol angle.
@ PropertyCustomDash
Custom dash pattern.
@ PropertyOpacity
Opacity.
@ PropertyOffset
Symbol offset.
@ PropertyStrokeWidth
Stroke width.
@ PropertyFillColor
Fill color.
@ PropertyName
Name, eg shape name for simple markers.
@ PropertyInterval
Line marker interval.
@ PropertyStrokeColor
Stroke color.
@ PropertyWidth
Symbol width.
virtual void setColor(const QColor &color)
Sets the "representative" color for the symbol layer.
void setDataDefinedProperties(const QgsPropertyCollection &collection)
Sets the symbol layer's property collection, used for data defined overrides.
@ PropertyOpacity
Opacity.
Definition: qgssymbol.h:76
void setPlacements(Qgis::MarkerLinePlacements placements)
Sets the placement of the symbols.
Container for settings relating to a text background object.
void setMarkerSymbol(QgsMarkerSymbol *symbol)
Sets the current marker symbol for the background shape.
void setSizeType(SizeType type)
Sets the method used to determine the size of the background shape (e.g., fixed size or buffer around...
void setType(ShapeType type)
Sets the type of background shape to draw (e.g., square, ellipse, SVG).
void setEnabled(bool enabled)
Sets whether the text background will be drawn.
void setSize(QSizeF size)
Sets the size of the background shape.
void setSizeUnit(QgsUnitTypes::RenderUnit unit)
Sets the units used for the shape's size.
void setColor(const QColor &color)
Sets the color for the buffer.
void setEnabled(bool enabled)
Sets whether the text buffer will be drawn.
void setPaintEffect(QgsPaintEffect *effect)
Sets the current paint effect for the buffer.
void setSizeUnit(QgsUnitTypes::RenderUnit unit)
Sets the units used for the buffer size.
void setSize(double size)
Sets the size of the buffer.
Container for all settings relating to text rendering.
Definition: qgstextformat.h:41
void setColor(const QColor &color)
Sets the color that text will be rendered in.
void setSize(double size)
Sets the size for rendered text.
void setFont(const QFont &font)
Sets the font used for rendering text.
void setSizeUnit(QgsUnitTypes::RenderUnit unit)
Sets the units for the size of rendered text.
void setBackground(const QgsTextBackgroundSettings &backgroundSettings)
Sets the text's background settings.q.
void setNamedStyle(const QString &style)
Sets the named style for the font used for rendering text.
QFont font() const
Returns the font used for rendering text.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
RenderUnit
Rendering size units.
Definition: qgsunittypes.h:168
Configuration of a single style within QgsVectorTileBasicLabeling.
void setGeometryType(QgsWkbTypes::GeometryType geomType)
Sets type of the geometry that will be used (point / line / polygon)
void setLayerName(const QString &name)
Sets name of the sub-layer to render (empty layer means that all layers match)
void setMinZoomLevel(int minZoom)
Sets minimum zoom level index (negative number means no limit)
void setFilterExpression(const QString &expr)
Sets filter expression (empty filter means that all features match)
void setMaxZoomLevel(int maxZoom)
Sets maximum zoom level index (negative number means no limit)
void setStyleName(const QString &name)
Sets human readable name of this style.
void setLabelSettings(const QgsPalLayerSettings &settings)
Sets labeling configuration of this style.
void setEnabled(bool enabled)
Sets whether this style is enabled (used for rendering)
Basic labeling configuration for vector tile layers.
Definition of map rendering of a subset of vector tile data.
void setEnabled(bool enabled)
Sets whether this style is enabled (used for rendering)
void setMinZoomLevel(int minZoom)
Sets minimum zoom level index (negative number means no limit)
void setLayerName(const QString &name)
Sets name of the sub-layer to render (empty layer means that all layers match)
void setGeometryType(QgsWkbTypes::GeometryType geomType)
Sets type of the geometry that will be used (point / line / polygon)
void setFilterExpression(const QString &expr)
Sets filter expression (empty filter means that all features match)
void setSymbol(QgsSymbol *sym)
Sets symbol for rendering. Takes ownership of the symbol.
void setStyleName(const QString &name)
Sets human readable name of this style.
void setMaxZoomLevel(int maxZoom)
Sets maximum zoom level index (negative number means no limit)
The default vector tile renderer implementation.
Base class for labeling configuration classes for vector tile layers.
Abstract base class for all vector tile renderer implementations.
GeometryType
The geometry types are used to group QgsWkbTypes::Type in a coarse way.
Definition: qgswkbtypes.h:141
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1578
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition: qgssymbol.h:27
const QString & typeName