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