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