QGIS API Documentation 3.39.0-Master (bca3cdb6021)
Loading...
Searching...
No Matches
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"
36#include "qgsfillsymbol.h"
37#include "qgsmarkersymbol.h"
38#include "qgslinesymbol.h"
39#include "qgsapplication.h"
40#include "qgsfontmanager.h"
41#include "qgis.h"
42#include "qgsrasterlayer.h"
43#include "qgsproviderregistry.h"
44#include "qgsrasterpipe.h"
45
46#include <QBuffer>
47#include <QRegularExpression>
48
52
54{
55 mError.clear();
56 mWarnings.clear();
57
58 if ( style.contains( QStringLiteral( "sources" ) ) )
59 {
60 parseSources( style.value( QStringLiteral( "sources" ) ).toMap(), context );
61 }
62
63 if ( style.contains( QStringLiteral( "layers" ) ) )
64 {
65 parseLayers( style.value( QStringLiteral( "layers" ) ).toList(), context );
66 }
67 else
68 {
69 mError = QObject::tr( "Could not find layers list in JSON" );
70 return NoLayerList;
71 }
72 return Success;
73}
74
79
81{
82 qDeleteAll( mSources );
83}
84
86{
87 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
88 if ( !context )
89 {
90 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
91 context = tmpContext.get();
92 }
93
94 QList<QgsVectorTileBasicRendererStyle> rendererStyles;
95 QList<QgsVectorTileBasicLabelingStyle> labelingStyles;
96
97 QgsVectorTileBasicRendererStyle rendererBackgroundStyle;
98 bool hasRendererBackgroundStyle = false;
99
100 for ( const QVariant &layer : layers )
101 {
102 const QVariantMap jsonLayer = layer.toMap();
103
104 const QString layerType = jsonLayer.value( QStringLiteral( "type" ) ).toString();
105 if ( layerType == QLatin1String( "background" ) )
106 {
107 hasRendererBackgroundStyle = parseFillLayer( jsonLayer, rendererBackgroundStyle, *context, true );
108 if ( hasRendererBackgroundStyle )
109 {
110 rendererBackgroundStyle.setStyleName( layerType );
111 rendererBackgroundStyle.setLayerName( layerType );
112 rendererBackgroundStyle.setFilterExpression( QString() );
113 rendererBackgroundStyle.setEnabled( true );
114 }
115 continue;
116 }
117
118 const QString styleId = jsonLayer.value( QStringLiteral( "id" ) ).toString();
119 context->setLayerId( styleId );
120
121 if ( layerType.compare( QLatin1String( "raster" ), Qt::CaseInsensitive ) == 0 )
122 {
123 QgsMapBoxGlStyleRasterSubLayer raster( styleId, jsonLayer.value( QStringLiteral( "source" ) ).toString() );
124 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
125 if ( jsonPaint.contains( QStringLiteral( "raster-opacity" ) ) )
126 {
127 const QVariant jsonRasterOpacity = jsonPaint.value( QStringLiteral( "raster-opacity" ) );
128 double defaultOpacity = 1;
129 raster.dataDefinedProperties().setProperty( QgsRasterPipe::Property::RendererOpacity, parseInterpolateByZoom( jsonRasterOpacity.toMap(), *context, 100, &defaultOpacity ) );
130 }
131
132 mRasterSubLayers.append( raster );
133 continue;
134 }
135
136 const QString layerName = jsonLayer.value( QStringLiteral( "source-layer" ) ).toString();
137
138 const int minZoom = jsonLayer.value( QStringLiteral( "minzoom" ), QStringLiteral( "-1" ) ).toInt();
139
140 // WARNING -- the QGIS renderers for vector tiles treat maxzoom different to the MapBox Style Specifications.
141 // from the MapBox Specifications:
142 //
143 // "The maximum zoom level for the layer. At zoom levels equal to or greater than the maxzoom, the layer will be hidden."
144 //
145 // However the QGIS styles will be hidden if the zoom level is GREATER THAN (not equal to) maxzoom.
146 // Accordingly we need to subtract 1 from the maxzoom value in the JSON:
147 int maxZoom = jsonLayer.value( QStringLiteral( "maxzoom" ), QStringLiteral( "-1" ) ).toInt();
148 if ( maxZoom != -1 )
149 maxZoom--;
150
151 const bool enabled = jsonLayer.value( QStringLiteral( "visibility" ) ).toString() != QLatin1String( "none" );
152
153 QString filterExpression;
154 if ( jsonLayer.contains( QStringLiteral( "filter" ) ) )
155 {
156 filterExpression = parseExpression( jsonLayer.value( QStringLiteral( "filter" ) ).toList(), *context );
157 }
158
161
162 bool hasRendererStyle = false;
163 bool hasLabelingStyle = false;
164 if ( layerType == QLatin1String( "fill" ) )
165 {
166 hasRendererStyle = parseFillLayer( jsonLayer, rendererStyle, *context );
167 }
168 else if ( layerType == QLatin1String( "line" ) )
169 {
170 hasRendererStyle = parseLineLayer( jsonLayer, rendererStyle, *context );
171 }
172 else if ( layerType == QLatin1String( "circle" ) )
173 {
174 hasRendererStyle = parseCircleLayer( jsonLayer, rendererStyle, *context );
175 }
176 else if ( layerType == QLatin1String( "symbol" ) )
177 {
178 parseSymbolLayer( jsonLayer, rendererStyle, hasRendererStyle, labelingStyle, hasLabelingStyle, *context );
179 }
180 else
181 {
182 mWarnings << QObject::tr( "%1: Skipping unknown layer type %2" ).arg( context->layerId(), layerType );
183 QgsDebugError( mWarnings.constLast() );
184 continue;
185 }
186
187 if ( hasRendererStyle )
188 {
189 rendererStyle.setStyleName( styleId );
190 rendererStyle.setLayerName( layerName );
191 rendererStyle.setFilterExpression( filterExpression );
192 rendererStyle.setMinZoomLevel( minZoom );
193 rendererStyle.setMaxZoomLevel( maxZoom );
194 rendererStyle.setEnabled( enabled );
195 rendererStyles.append( rendererStyle );
196 }
197
198 if ( hasLabelingStyle )
199 {
200 labelingStyle.setStyleName( styleId );
201 labelingStyle.setLayerName( layerName );
202 labelingStyle.setFilterExpression( filterExpression );
203 labelingStyle.setMinZoomLevel( minZoom );
204 labelingStyle.setMaxZoomLevel( maxZoom );
205 labelingStyle.setEnabled( enabled );
206 labelingStyles.append( labelingStyle );
207 }
208
209 mWarnings.append( context->warnings() );
210 context->clearWarnings();
211 }
212
213 if ( hasRendererBackgroundStyle )
214 rendererStyles.prepend( rendererBackgroundStyle );
215
216 mRenderer = std::make_unique< QgsVectorTileBasicRenderer >();
217 QgsVectorTileBasicRenderer *renderer = dynamic_cast< QgsVectorTileBasicRenderer *>( mRenderer.get() );
218 renderer->setStyles( rendererStyles );
219
220 mLabeling = std::make_unique< QgsVectorTileBasicLabeling >();
221 QgsVectorTileBasicLabeling *labeling = dynamic_cast< QgsVectorTileBasicLabeling * >( mLabeling.get() );
222 labeling->setStyles( labelingStyles );
223}
224
225bool QgsMapBoxGlStyleConverter::parseFillLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &style, QgsMapBoxGlStyleConversionContext &context, bool isBackgroundStyle )
226{
227 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
228
229 QgsPropertyCollection ddProperties;
230 QgsPropertyCollection ddRasterProperties;
231
232 bool colorIsDataDefined = false;
233
234 std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsFillSymbol >() );
235
236 // fill color
237 QColor fillColor;
238 if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-color" ) : QStringLiteral( "fill-color" ) ) )
239 {
240 const QVariant jsonFillColor = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-color" ) : QStringLiteral( "fill-color" ) );
241 switch ( jsonFillColor.userType() )
242 {
243 case QMetaType::Type::QVariantMap:
244 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateColorByZoom( jsonFillColor.toMap(), context, &fillColor ) );
245 break;
246
247 case QMetaType::Type::QVariantList:
248 case QMetaType::Type::QStringList:
249 colorIsDataDefined = true;
250 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonFillColor.toList(), PropertyType::Color, context, 1, 255, &fillColor ) );
251 break;
252
253 case QMetaType::Type::QString:
254 fillColor = parseColor( jsonFillColor.toString(), context );
255 break;
256
257 default:
258 {
259 context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonFillColor.userType() ) ) ) );
260 break;
261 }
262 }
263 }
264 else
265 {
266 // defaults to #000000
267 fillColor = QColor( 0, 0, 0 );
268 }
269
270 QColor fillOutlineColor;
271 if ( !isBackgroundStyle )
272 {
273 if ( !jsonPaint.contains( QStringLiteral( "fill-outline-color" ) ) )
274 {
275 if ( fillColor.isValid() )
276 fillOutlineColor = fillColor;
277
278 // match fill color data defined property when active
279 if ( ddProperties.isActive( QgsSymbolLayer::Property::FillColor ) )
281 }
282 else
283 {
284 const QVariant jsonFillOutlineColor = jsonPaint.value( QStringLiteral( "fill-outline-color" ) );
285 switch ( jsonFillOutlineColor.userType() )
286 {
287 case QMetaType::Type::QVariantMap:
288 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateColorByZoom( jsonFillOutlineColor.toMap(), context, &fillOutlineColor ) );
289 break;
290
291 case QMetaType::Type::QVariantList:
292 case QMetaType::Type::QStringList:
293 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonFillOutlineColor.toList(), PropertyType::Color, context, 1, 255, &fillOutlineColor ) );
294 break;
295
296 case QMetaType::Type::QString:
297 fillOutlineColor = parseColor( jsonFillOutlineColor.toString(), context );
298 break;
299
300 default:
301 context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-outline-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonFillOutlineColor.userType() ) ) ) );
302 break;
303 }
304 }
305 }
306
307 double fillOpacity = -1.0;
308 double rasterOpacity = -1.0;
309 if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-opacity" ) : QStringLiteral( "fill-opacity" ) ) )
310 {
311 const QVariant jsonFillOpacity = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-opacity" ) : QStringLiteral( "fill-opacity" ) );
312 switch ( jsonFillOpacity.userType() )
313 {
314 case QMetaType::Type::Int:
315 case QMetaType::Type::LongLong:
316 case QMetaType::Type::Double:
317 fillOpacity = jsonFillOpacity.toDouble();
318 rasterOpacity = fillOpacity;
319 break;
320
321 case QMetaType::Type::QVariantMap:
322 if ( ddProperties.isActive( QgsSymbolLayer::Property::FillColor ) )
323 {
324 symbol->setDataDefinedProperty( QgsSymbol::Property::Opacity, parseInterpolateByZoom( jsonFillOpacity.toMap(), context, 100 ) );
325 }
326 else
327 {
328 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateOpacityByZoom( jsonFillOpacity.toMap(), fillColor.isValid() ? fillColor.alpha() : 255, &context ) );
329 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateOpacityByZoom( jsonFillOpacity.toMap(), fillOutlineColor.isValid() ? fillOutlineColor.alpha() : 255, &context ) );
330 ddRasterProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseInterpolateByZoom( jsonFillOpacity.toMap(), context, 100, &rasterOpacity ) );
331 }
332 break;
333
334 case QMetaType::Type::QVariantList:
335 case QMetaType::Type::QStringList:
336 if ( ddProperties.isActive( QgsSymbolLayer::Property::FillColor ) )
337 {
338 symbol->setDataDefinedProperty( QgsSymbol::Property::Opacity, parseValueList( jsonFillOpacity.toList(), PropertyType::Numeric, context, 100, 100 ) );
339 }
340 else
341 {
342 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonFillOpacity.toList(), PropertyType::Opacity, context, 1, fillColor.isValid() ? fillColor.alpha() : 255 ) );
343 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonFillOpacity.toList(), PropertyType::Opacity, context, 1, fillOutlineColor.isValid() ? fillOutlineColor.alpha() : 255 ) );
344 ddRasterProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseValueList( jsonFillOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &rasterOpacity ) );
345 }
346 break;
347
348 default:
349 context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonFillOpacity.userType() ) ) ) );
350 break;
351 }
352 }
353
354 // fill-translate
355 QPointF fillTranslate;
356 if ( jsonPaint.contains( QStringLiteral( "fill-translate" ) ) )
357 {
358 const QVariant jsonFillTranslate = jsonPaint.value( QStringLiteral( "fill-translate" ) );
359 switch ( jsonFillTranslate.userType() )
360 {
361
362 case QMetaType::Type::QVariantMap:
363 ddProperties.setProperty( QgsSymbolLayer::Property::Offset, parseInterpolatePointByZoom( jsonFillTranslate.toMap(), context, context.pixelSizeConversionFactor(), &fillTranslate ) );
364 break;
365
366 case QMetaType::Type::QVariantList:
367 case QMetaType::Type::QStringList:
368 fillTranslate = QPointF( jsonFillTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(),
369 jsonFillTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
370 break;
371
372 default:
373 context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonFillTranslate.userType() ) ) ) );
374 break;
375 }
376 }
377
378 QgsSimpleFillSymbolLayer *fillSymbol = dynamic_cast< QgsSimpleFillSymbolLayer * >( symbol->symbolLayer( 0 ) );
379 Q_ASSERT( fillSymbol ); // should not fail since QgsFillSymbol() constructor instantiates a QgsSimpleFillSymbolLayer
380
381 // set render units
382 symbol->setOutputUnit( context.targetUnit() );
383 fillSymbol->setOutputUnit( context.targetUnit() );
384
385 if ( !fillTranslate.isNull() )
386 {
387 fillSymbol->setOffset( fillTranslate );
388 }
389 fillSymbol->setOffsetUnit( context.targetUnit() );
390
391 if ( jsonPaint.contains( isBackgroundStyle ? QStringLiteral( "background-pattern" ) : QStringLiteral( "fill-pattern" ) ) )
392 {
393 // get fill-pattern to set sprite
394
395 const QVariant fillPatternJson = jsonPaint.value( isBackgroundStyle ? QStringLiteral( "background-pattern" ) : QStringLiteral( "fill-pattern" ) );
396
397 // fill-pattern disabled dillcolor
398 fillColor = QColor();
399 fillOutlineColor = QColor();
400
401 // fill-pattern can be String or Object
402 // String: {"fill-pattern": "dash-t"}
403 // Object: {"fill-pattern":{"stops":[[11,"wetland8"],[12,"wetland16"]]}}
404
405 QSize spriteSize;
406 QString spriteProperty, spriteSizeProperty;
407 const QString sprite = retrieveSpriteAsBase64WithProperties( fillPatternJson, context, spriteSize, spriteProperty, spriteSizeProperty );
408 if ( !sprite.isEmpty() )
409 {
410 // when fill-pattern exists, set and insert QgsRasterFillSymbolLayer
412 rasterFill->setImageFilePath( sprite );
413 rasterFill->setWidth( spriteSize.width() );
414 rasterFill->setSizeUnit( context.targetUnit() );
416
417 if ( rasterOpacity >= 0 )
418 {
419 rasterFill->setOpacity( rasterOpacity );
420 }
421
422 if ( !spriteProperty.isEmpty() )
423 {
424 ddRasterProperties.setProperty( QgsSymbolLayer::Property::File, QgsProperty::fromExpression( spriteProperty ) );
425 ddRasterProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
426 }
427
428 rasterFill->setDataDefinedProperties( ddRasterProperties );
429 symbol->appendSymbolLayer( rasterFill );
430 }
431 }
432
433 fillSymbol->setDataDefinedProperties( ddProperties );
434
435 if ( fillOpacity != -1 )
436 {
437 symbol->setOpacity( fillOpacity );
438 }
439
440 // some complex logic here!
441 // by default a MapBox fill style will share the same stroke color as the fill color.
442 // This is generally desirable and the 1px stroke can help to hide boundaries between features which
443 // would otherwise be visible due to antialiasing effects.
444 // BUT if the outline color is semi-transparent, then drawing the stroke will result in a double rendering
445 // of strokes for adjacent polygons, resulting in visible seams between tiles. Accordingly, we only
446 // set the stroke color if it's a completely different color to the fill (ie the style designer explicitly
447 // wants a visible stroke) OR the stroke color is opaque and the double-rendering artifacts aren't an issue
448 if ( fillOutlineColor.isValid() && ( fillOutlineColor.alpha() == 255 || fillOutlineColor != fillColor ) )
449 {
450 // mapbox fill strokes are always 1 px wide
451 fillSymbol->setStrokeWidth( 0 );
452 fillSymbol->setStrokeColor( fillOutlineColor );
453 }
454 else
455 {
456 fillSymbol->setStrokeStyle( Qt::NoPen );
457 }
458
459 if ( fillColor.isValid() )
460 {
461 fillSymbol->setFillColor( fillColor );
462 }
463 else if ( colorIsDataDefined )
464 {
465 fillSymbol->setFillColor( QColor( Qt::transparent ) );
466 }
467 else
468 {
469 fillSymbol->setBrushStyle( Qt::NoBrush );
470 }
471
473 style.setSymbol( symbol.release() );
474 return true;
475}
476
478{
479 if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
480 {
481 context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
482 return false;
483 }
484
485 QgsPropertyCollection ddProperties;
486 QString rasterLineSprite;
487
488 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
489 if ( jsonPaint.contains( QStringLiteral( "line-pattern" ) ) )
490 {
491 const QVariant jsonLinePattern = jsonPaint.value( QStringLiteral( "line-pattern" ) );
492 switch ( jsonLinePattern.userType() )
493 {
494 case QMetaType::Type::QVariantMap:
495 case QMetaType::Type::QString:
496 {
497 QSize spriteSize;
498 QString spriteProperty, spriteSizeProperty;
499 rasterLineSprite = retrieveSpriteAsBase64WithProperties( jsonLinePattern, context, spriteSize, spriteProperty, spriteSizeProperty );
501 break;
502 }
503
504 case QMetaType::Type::QVariantList:
505 case QMetaType::Type::QStringList:
506 default:
507 break;
508 }
509
510 if ( rasterLineSprite.isEmpty() )
511 {
512 // unsupported line-pattern definition, moving on
513 context.pushWarning( QObject::tr( "%1: Skipping unsupported line-pattern property" ).arg( context.layerId() ) );
514 return false;
515 }
516 }
517
518 // line color
519 QColor lineColor;
520 if ( jsonPaint.contains( QStringLiteral( "line-color" ) ) )
521 {
522 const QVariant jsonLineColor = jsonPaint.value( QStringLiteral( "line-color" ) );
523 switch ( jsonLineColor.userType() )
524 {
525 case QMetaType::Type::QVariantMap:
526 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateColorByZoom( jsonLineColor.toMap(), context, &lineColor ) );
528 break;
529
530 case QMetaType::Type::QVariantList:
531 case QMetaType::Type::QStringList:
532 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonLineColor.toList(), PropertyType::Color, context, 1, 255, &lineColor ) );
534 break;
535
536 case QMetaType::Type::QString:
537 lineColor = parseColor( jsonLineColor.toString(), context );
538 break;
539
540 default:
541 context.pushWarning( QObject::tr( "%1: Skipping unsupported line-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineColor.userType() ) ) ) );
542 break;
543 }
544 }
545 else
546 {
547 // defaults to #000000
548 lineColor = QColor( 0, 0, 0 );
549 }
550
551
552 double lineWidth = 1.0 * context.pixelSizeConversionFactor();
553 QgsProperty lineWidthProperty;
554 if ( jsonPaint.contains( QStringLiteral( "line-width" ) ) )
555 {
556 const QVariant jsonLineWidth = jsonPaint.value( QStringLiteral( "line-width" ) );
557 switch ( jsonLineWidth.userType() )
558 {
559 case QMetaType::Type::Int:
560 case QMetaType::Type::LongLong:
561 case QMetaType::Type::Double:
562 lineWidth = jsonLineWidth.toDouble() * context.pixelSizeConversionFactor();
563 break;
564
565 case QMetaType::Type::QVariantMap:
566 {
567 lineWidth = -1;
568 lineWidthProperty = parseInterpolateByZoom( jsonLineWidth.toMap(), context, context.pixelSizeConversionFactor(), &lineWidth );
569 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, lineWidthProperty );
570 // set symbol layer visibility depending on line width since QGIS displays line with 0 width as hairlines
571 QgsProperty layerEnabledProperty = QgsProperty( lineWidthProperty );
572 layerEnabledProperty.setExpressionString( QStringLiteral( "(%1) > 0" ).arg( lineWidthProperty.expressionString() ) );
573 ddProperties.setProperty( QgsSymbolLayer::Property::LayerEnabled, layerEnabledProperty );
574 break;
575 }
576
577 case QMetaType::Type::QVariantList:
578 case QMetaType::Type::QStringList:
579 {
580 lineWidthProperty = parseValueList( jsonLineWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &lineWidth );
581 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, lineWidthProperty );
582 // set symbol layer visibility depending on line width since QGIS displays line with 0 width as hairlines
583 QgsProperty layerEnabledProperty = QgsProperty( lineWidthProperty );
584 layerEnabledProperty.setExpressionString( QStringLiteral( "(%1) > 0" ).arg( lineWidthProperty.expressionString() ) );
585 ddProperties.setProperty( QgsSymbolLayer::Property::LayerEnabled, layerEnabledProperty );
586 break;
587 }
588
589 default:
590 context.pushWarning( QObject::tr( "%1: Skipping unsupported fill-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineWidth.userType() ) ) ) );
591 break;
592 }
593 }
594
595 double lineOffset = 0.0;
596 if ( jsonPaint.contains( QStringLiteral( "line-offset" ) ) )
597 {
598 const QVariant jsonLineOffset = jsonPaint.value( QStringLiteral( "line-offset" ) );
599 switch ( jsonLineOffset.userType() )
600 {
601 case QMetaType::Type::Int:
602 case QMetaType::Type::LongLong:
603 case QMetaType::Type::Double:
604 lineOffset = -jsonLineOffset.toDouble() * context.pixelSizeConversionFactor();
605 break;
606
607 case QMetaType::Type::QVariantMap:
608 lineWidth = -1;
609 ddProperties.setProperty( QgsSymbolLayer::Property::Offset, parseInterpolateByZoom( jsonLineOffset.toMap(), context, context.pixelSizeConversionFactor() * -1, &lineOffset ) );
610 break;
611
612 case QMetaType::Type::QVariantList:
613 case QMetaType::Type::QStringList:
614 ddProperties.setProperty( QgsSymbolLayer::Property::Offset, parseValueList( jsonLineOffset.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * -1, 255, nullptr, &lineOffset ) );
615 break;
616
617 default:
618 context.pushWarning( QObject::tr( "%1: Skipping unsupported line-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineOffset.userType() ) ) ) );
619 break;
620 }
621 }
622
623 double lineOpacity = -1.0;
624 QgsProperty lineOpacityProperty;
625 if ( jsonPaint.contains( QStringLiteral( "line-opacity" ) ) )
626 {
627 const QVariant jsonLineOpacity = jsonPaint.value( QStringLiteral( "line-opacity" ) );
628 switch ( jsonLineOpacity.userType() )
629 {
630 case QMetaType::Type::Int:
631 case QMetaType::Type::LongLong:
632 case QMetaType::Type::Double:
633 lineOpacity = jsonLineOpacity.toDouble();
634 break;
635
636 case QMetaType::Type::QVariantMap:
638 {
639 double defaultValue = 1.0;
640 lineOpacityProperty = parseInterpolateByZoom( jsonLineOpacity.toMap(), context, 100, &defaultValue );
641 }
642 else
643 {
644 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateOpacityByZoom( jsonLineOpacity.toMap(), lineColor.isValid() ? lineColor.alpha() : 255, &context ) );
645 }
646 break;
647
648 case QMetaType::Type::QVariantList:
649 case QMetaType::Type::QStringList:
651 {
652 double defaultValue = 1.0;
653 QColor invalidColor;
654 lineOpacityProperty = parseValueList( jsonLineOpacity.toList(), PropertyType::Numeric, context, 100, 255, &invalidColor, &defaultValue );
655 }
656 else
657 {
658 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonLineOpacity.toList(), PropertyType::Opacity, context, 1, lineColor.isValid() ? lineColor.alpha() : 255 ) );
659 }
660 break;
661
662 default:
663 context.pushWarning( QObject::tr( "%1: Skipping unsupported line-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineOpacity.userType() ) ) ) );
664 break;
665 }
666 }
667
668 QVector< double > dashVector;
669 if ( jsonPaint.contains( QStringLiteral( "line-dasharray" ) ) )
670 {
671 const QVariant jsonLineDashArray = jsonPaint.value( QStringLiteral( "line-dasharray" ) );
672 switch ( jsonLineDashArray.userType() )
673 {
674 case QMetaType::Type::QVariantMap:
675 {
676 QString arrayExpression;
677 if ( !lineWidthProperty.asExpression().isEmpty() )
678 {
679 arrayExpression = QStringLiteral( "array_to_string(array_foreach(%1,@element * (%2)), ';')" ) // skip-keyword-check
680 .arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, 1 ),
681 lineWidthProperty.asExpression() );
682 }
683 else
684 {
685 arrayExpression = QStringLiteral( "array_to_string(%1, ';')" ).arg( parseArrayStops( jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList(), context, lineWidth ) );
686 }
688
689 const QVariantList dashSource = jsonLineDashArray.toMap().value( QStringLiteral( "stops" ) ).toList().first().toList().value( 1 ).toList();
690 for ( const QVariant &v : dashSource )
691 {
692 dashVector << v.toDouble() * lineWidth;
693 }
694 break;
695 }
696
697 case QMetaType::Type::QVariantList:
698 case QMetaType::Type::QStringList:
699 {
700 const QVariantList dashSource = jsonLineDashArray.toList();
701
702 if ( dashSource.at( 0 ).userType() == QMetaType::Type::QString )
703 {
704 QgsProperty property = parseValueList( dashSource, PropertyType::NumericArray, context, 1, 255, nullptr, nullptr );
705 if ( !lineWidthProperty.asExpression().isEmpty() )
706 {
707 property = QgsProperty::fromExpression( QStringLiteral( "array_to_string(array_foreach(%1,@element * (%2)), ';')" ) // skip-keyword-check
708 .arg( property.asExpression(), lineWidthProperty.asExpression() ) );
709 }
710 else
711 {
712 property = QgsProperty::fromExpression( QStringLiteral( "array_to_string(%1, ';')" ).arg( property.asExpression() ) );
713 }
714 ddProperties.setProperty( QgsSymbolLayer::Property::CustomDash, property );
715 }
716 else
717 {
718 QVector< double > rawDashVectorSizes;
719 rawDashVectorSizes.reserve( dashSource.size() );
720 for ( const QVariant &v : dashSource )
721 {
722 rawDashVectorSizes << v.toDouble();
723 }
724
725 // handle non-compliant dash vector patterns
726 if ( rawDashVectorSizes.size() == 1 )
727 {
728 // match behavior of MapBox style rendering -- if a user makes a line dash array with one element, it's ignored
729 rawDashVectorSizes.clear();
730 }
731 else if ( rawDashVectorSizes.size() % 2 == 1 )
732 {
733 // odd number of dash pattern sizes -- this isn't permitted by Qt/QGIS, but isn't explicitly blocked by the MapBox specs
734 // MapBox seems to add the extra dash element to the first dash size
735 rawDashVectorSizes[0] = rawDashVectorSizes[0] + rawDashVectorSizes[rawDashVectorSizes.size() - 1];
736 rawDashVectorSizes.resize( rawDashVectorSizes.size() - 1 );
737 }
738
739 if ( !rawDashVectorSizes.isEmpty() && ( !lineWidthProperty.asExpression().isEmpty() ) )
740 {
741 QStringList dashArrayStringParts;
742 dashArrayStringParts.reserve( rawDashVectorSizes.size() );
743 for ( double v : std::as_const( rawDashVectorSizes ) )
744 {
745 dashArrayStringParts << qgsDoubleToString( v );
746 }
747
748 QString arrayExpression = QStringLiteral( "array_to_string(array_foreach(array(%1),@element * (%2)), ';')" ) // skip-keyword-check
749 .arg( dashArrayStringParts.join( ',' ),
750 lineWidthProperty.asExpression() );
752 }
753
754 // dash vector sizes for QGIS symbols must be multiplied by the target line width
755 for ( double v : std::as_const( rawDashVectorSizes ) )
756 {
757 dashVector << v *lineWidth;
758 }
759 }
760 break;
761 }
762
763 default:
764 context.pushWarning( QObject::tr( "%1: Skipping unsupported line-dasharray type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineDashArray.userType() ) ) ) );
765 break;
766 }
767 }
768
769 Qt::PenCapStyle penCapStyle = Qt::FlatCap;
770 Qt::PenJoinStyle penJoinStyle = Qt::MiterJoin;
771 if ( jsonLayer.contains( QStringLiteral( "layout" ) ) )
772 {
773 const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
774 if ( jsonLayout.contains( QStringLiteral( "line-cap" ) ) )
775 {
776 penCapStyle = parseCapStyle( jsonLayout.value( QStringLiteral( "line-cap" ) ).toString() );
777 }
778 if ( jsonLayout.contains( QStringLiteral( "line-join" ) ) )
779 {
780 penJoinStyle = parseJoinStyle( jsonLayout.value( QStringLiteral( "line-join" ) ).toString() );
781 }
782 }
783
784 std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsLineSymbol >() );
785 symbol->setOutputUnit( context.targetUnit() );
786
787 if ( !rasterLineSprite.isEmpty() )
788 {
789 QgsRasterLineSymbolLayer *lineSymbol = new QgsRasterLineSymbolLayer( rasterLineSprite );
790 lineSymbol->setOutputUnit( context.targetUnit() );
791 lineSymbol->setPenCapStyle( penCapStyle );
792 lineSymbol->setPenJoinStyle( penJoinStyle );
793 lineSymbol->setDataDefinedProperties( ddProperties );
794 lineSymbol->setOffset( lineOffset );
795 lineSymbol->setOffsetUnit( context.targetUnit() );
796
797 if ( lineOpacity != -1 )
798 {
799 symbol->setOpacity( lineOpacity );
800 }
801 if ( !lineOpacityProperty.asExpression().isEmpty() )
802 {
803 QgsPropertyCollection ddProperties;
804 ddProperties.setProperty( QgsSymbol::Property::Opacity, lineOpacityProperty );
805 symbol->setDataDefinedProperties( ddProperties );
806 }
807 if ( lineWidth != -1 )
808 {
809 lineSymbol->setWidth( lineWidth );
810 }
811 symbol->changeSymbolLayer( 0, lineSymbol );
812 }
813 else
814 {
815 QgsSimpleLineSymbolLayer *lineSymbol = dynamic_cast< QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) );
816 Q_ASSERT( lineSymbol ); // should not fail since QgsLineSymbol() constructor instantiates a QgsSimpleLineSymbolLayer
817
818 // set render units
819 lineSymbol->setOutputUnit( context.targetUnit() );
820 lineSymbol->setPenCapStyle( penCapStyle );
821 lineSymbol->setPenJoinStyle( penJoinStyle );
822 lineSymbol->setDataDefinedProperties( ddProperties );
823 lineSymbol->setOffset( lineOffset );
824 lineSymbol->setOffsetUnit( context.targetUnit() );
825
826 if ( lineOpacity != -1 )
827 {
828 symbol->setOpacity( lineOpacity );
829 }
830 if ( !lineOpacityProperty.asExpression().isEmpty() )
831 {
832 QgsPropertyCollection ddProperties;
833 ddProperties.setProperty( QgsSymbol::Property::Opacity, lineOpacityProperty );
834 symbol->setDataDefinedProperties( ddProperties );
835 }
836 if ( lineColor.isValid() )
837 {
838 lineSymbol->setColor( lineColor );
839 }
840 if ( lineWidth != -1 )
841 {
842 lineSymbol->setWidth( lineWidth );
843 }
844 if ( !dashVector.empty() )
845 {
846 lineSymbol->setUseCustomDashPattern( true );
847 lineSymbol->setCustomDashVector( dashVector );
848 }
849 }
850
852 style.setSymbol( symbol.release() );
853 return true;
854}
855
857{
858 if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
859 {
860 context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
861 return false;
862 }
863
864 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
865 QgsPropertyCollection ddProperties;
866
867 // circle color
868 QColor circleFillColor;
869 if ( jsonPaint.contains( QStringLiteral( "circle-color" ) ) )
870 {
871 const QVariant jsonCircleColor = jsonPaint.value( QStringLiteral( "circle-color" ) );
872 switch ( jsonCircleColor.userType() )
873 {
874 case QMetaType::Type::QVariantMap:
875 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateColorByZoom( jsonCircleColor.toMap(), context, &circleFillColor ) );
876 break;
877
878 case QMetaType::Type::QVariantList:
879 case QMetaType::Type::QStringList:
880 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonCircleColor.toList(), PropertyType::Color, context, 1, 255, &circleFillColor ) );
881 break;
882
883 case QMetaType::Type::QString:
884 circleFillColor = parseColor( jsonCircleColor.toString(), context );
885 break;
886
887 default:
888 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleColor.userType() ) ) ) );
889 break;
890 }
891 }
892 else
893 {
894 // defaults to #000000
895 circleFillColor = QColor( 0, 0, 0 );
896 }
897
898 // circle radius
899 double circleDiameter = 10.0;
900 if ( jsonPaint.contains( QStringLiteral( "circle-radius" ) ) )
901 {
902 const QVariant jsonCircleRadius = jsonPaint.value( QStringLiteral( "circle-radius" ) );
903 switch ( jsonCircleRadius.userType() )
904 {
905 case QMetaType::Type::Int:
906 case QMetaType::Type::LongLong:
907 case QMetaType::Type::Double:
908 circleDiameter = jsonCircleRadius.toDouble() * context.pixelSizeConversionFactor() * 2;
909 break;
910
911 case QMetaType::Type::QVariantMap:
912 circleDiameter = -1;
913 ddProperties.setProperty( QgsSymbolLayer::Property::Width, parseInterpolateByZoom( jsonCircleRadius.toMap(), context, context.pixelSizeConversionFactor() * 2, &circleDiameter ) );
914 break;
915
916 case QMetaType::Type::QVariantList:
917 case QMetaType::Type::QStringList:
918 ddProperties.setProperty( QgsSymbolLayer::Property::Width, parseValueList( jsonCircleRadius.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * 2, 255, nullptr, &circleDiameter ) );
919 break;
920
921 default:
922 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-radius type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleRadius.userType() ) ) ) );
923 break;
924 }
925 }
926
927 double circleOpacity = -1.0;
928 if ( jsonPaint.contains( QStringLiteral( "circle-opacity" ) ) )
929 {
930 const QVariant jsonCircleOpacity = jsonPaint.value( QStringLiteral( "circle-opacity" ) );
931 switch ( jsonCircleOpacity.userType() )
932 {
933 case QMetaType::Type::Int:
934 case QMetaType::Type::LongLong:
935 case QMetaType::Type::Double:
936 circleOpacity = jsonCircleOpacity.toDouble();
937 break;
938
939 case QMetaType::Type::QVariantMap:
940 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateOpacityByZoom( jsonCircleOpacity.toMap(), circleFillColor.isValid() ? circleFillColor.alpha() : 255, &context ) );
941 break;
942
943 case QMetaType::Type::QVariantList:
944 case QMetaType::Type::QStringList:
945 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonCircleOpacity.toList(), PropertyType::Opacity, context, 1, circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) );
946 break;
947
948 default:
949 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleOpacity.userType() ) ) ) );
950 break;
951 }
952 }
953 if ( ( circleOpacity != -1 ) && circleFillColor.isValid() )
954 {
955 circleFillColor.setAlphaF( circleOpacity );
956 }
957
958 // circle stroke color
959 QColor circleStrokeColor;
960 if ( jsonPaint.contains( QStringLiteral( "circle-stroke-color" ) ) )
961 {
962 const QVariant jsonCircleStrokeColor = jsonPaint.value( QStringLiteral( "circle-stroke-color" ) );
963 switch ( jsonCircleStrokeColor.userType() )
964 {
965 case QMetaType::Type::QVariantMap:
966 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateColorByZoom( jsonCircleStrokeColor.toMap(), context, &circleStrokeColor ) );
967 break;
968
969 case QMetaType::Type::QVariantList:
970 case QMetaType::Type::QStringList:
971 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonCircleStrokeColor.toList(), PropertyType::Color, context, 1, 255, &circleStrokeColor ) );
972 break;
973
974 case QMetaType::Type::QString:
975 circleStrokeColor = parseColor( jsonCircleStrokeColor.toString(), context );
976 break;
977
978 default:
979 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleStrokeColor.userType() ) ) ) );
980 break;
981 }
982 }
983
984 // circle stroke width
985 double circleStrokeWidth = -1.0;
986 if ( jsonPaint.contains( QStringLiteral( "circle-stroke-width" ) ) )
987 {
988 const QVariant circleStrokeWidthJson = jsonPaint.value( QStringLiteral( "circle-stroke-width" ) );
989 switch ( circleStrokeWidthJson.userType() )
990 {
991 case QMetaType::Type::Int:
992 case QMetaType::Type::LongLong:
993 case QMetaType::Type::Double:
994 circleStrokeWidth = circleStrokeWidthJson.toDouble() * context.pixelSizeConversionFactor();
995 break;
996
997 case QMetaType::Type::QVariantMap:
998 circleStrokeWidth = -1.0;
999 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, parseInterpolateByZoom( circleStrokeWidthJson.toMap(), context, context.pixelSizeConversionFactor(), &circleStrokeWidth ) );
1000 break;
1001
1002 case QMetaType::Type::QVariantList:
1003 case QMetaType::Type::QStringList:
1004 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, parseValueList( circleStrokeWidthJson.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &circleStrokeWidth ) );
1005 break;
1006
1007 default:
1008 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( circleStrokeWidthJson.userType() ) ) ) );
1009 break;
1010 }
1011 }
1012
1013 double circleStrokeOpacity = -1.0;
1014 if ( jsonPaint.contains( QStringLiteral( "circle-stroke-opacity" ) ) )
1015 {
1016 const QVariant jsonCircleStrokeOpacity = jsonPaint.value( QStringLiteral( "circle-stroke-opacity" ) );
1017 switch ( jsonCircleStrokeOpacity.userType() )
1018 {
1019 case QMetaType::Type::Int:
1020 case QMetaType::Type::LongLong:
1021 case QMetaType::Type::Double:
1022 circleStrokeOpacity = jsonCircleStrokeOpacity.toDouble();
1023 break;
1024
1025 case QMetaType::Type::QVariantMap:
1026 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateOpacityByZoom( jsonCircleStrokeOpacity.toMap(), circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255, &context ) );
1027 break;
1028
1029 case QMetaType::Type::QVariantList:
1030 case QMetaType::Type::QStringList:
1031 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonCircleStrokeOpacity.toList(), PropertyType::Opacity, context, 1, circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) );
1032 break;
1033
1034 default:
1035 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-stroke-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleStrokeOpacity.userType() ) ) ) );
1036 break;
1037 }
1038 }
1039 if ( ( circleStrokeOpacity != -1 ) && circleStrokeColor.isValid() )
1040 {
1041 circleStrokeColor.setAlphaF( circleStrokeOpacity );
1042 }
1043
1044 // translate
1045 QPointF circleTranslate;
1046 if ( jsonPaint.contains( QStringLiteral( "circle-translate" ) ) )
1047 {
1048 const QVariant jsonCircleTranslate = jsonPaint.value( QStringLiteral( "circle-translate" ) );
1049 switch ( jsonCircleTranslate.userType() )
1050 {
1051
1052 case QMetaType::Type::QVariantMap:
1053 ddProperties.setProperty( QgsSymbolLayer::Property::Offset, parseInterpolatePointByZoom( jsonCircleTranslate.toMap(), context, context.pixelSizeConversionFactor(), &circleTranslate ) );
1054 break;
1055
1056 case QMetaType::Type::QVariantList:
1057 case QMetaType::Type::QStringList:
1058 circleTranslate = QPointF( jsonCircleTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(),
1059 jsonCircleTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
1060 break;
1061
1062 default:
1063 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleTranslate.userType() ) ) ) );
1064 break;
1065 }
1066 }
1067
1068 std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsMarkerSymbol >() );
1069 QgsSimpleMarkerSymbolLayer *markerSymbolLayer = dynamic_cast< QgsSimpleMarkerSymbolLayer * >( symbol->symbolLayer( 0 ) );
1070 Q_ASSERT( markerSymbolLayer );
1071
1072 // set render units
1073 symbol->setOutputUnit( context.targetUnit() );
1074 symbol->setDataDefinedProperties( ddProperties );
1075
1076 if ( !circleTranslate.isNull() )
1077 {
1078 markerSymbolLayer->setOffset( circleTranslate );
1079 markerSymbolLayer->setOffsetUnit( context.targetUnit() );
1080 }
1081
1082 if ( circleFillColor.isValid() )
1083 {
1084 markerSymbolLayer->setFillColor( circleFillColor );
1085 }
1086 if ( circleDiameter != -1 )
1087 {
1088 markerSymbolLayer->setSize( circleDiameter );
1089 markerSymbolLayer->setSizeUnit( context.targetUnit() );
1090 }
1091 if ( circleStrokeColor.isValid() )
1092 {
1093 markerSymbolLayer->setStrokeColor( circleStrokeColor );
1094 }
1095 if ( circleStrokeWidth != -1 )
1096 {
1097 markerSymbolLayer->setStrokeWidth( circleStrokeWidth );
1098 markerSymbolLayer->setStrokeWidthUnit( context.targetUnit() );
1099 }
1100
1102 style.setSymbol( symbol.release() );
1103 return true;
1104}
1105
1106void QgsMapBoxGlStyleConverter::parseSymbolLayer( const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &renderer, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context )
1107{
1108 hasLabeling = false;
1109 hasRenderer = false;
1110
1111 if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
1112 {
1113 context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
1114 return;
1115 }
1116 const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
1117 if ( !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1118 {
1119 hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
1120 return;
1121 }
1122
1123 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
1124
1125 QgsPropertyCollection ddLabelProperties;
1126
1127 double textSize = 16.0 * context.pixelSizeConversionFactor();
1128 QgsProperty textSizeProperty;
1129 if ( jsonLayout.contains( QStringLiteral( "text-size" ) ) )
1130 {
1131 const QVariant jsonTextSize = jsonLayout.value( QStringLiteral( "text-size" ) );
1132 switch ( jsonTextSize.userType() )
1133 {
1134 case QMetaType::Type::Int:
1135 case QMetaType::Type::LongLong:
1136 case QMetaType::Type::Double:
1137 textSize = jsonTextSize.toDouble() * context.pixelSizeConversionFactor();
1138 break;
1139
1140 case QMetaType::Type::QVariantMap:
1141 textSize = -1;
1142 textSizeProperty = parseInterpolateByZoom( jsonTextSize.toMap(), context, context.pixelSizeConversionFactor(), &textSize );
1143
1144 break;
1145
1146 case QMetaType::Type::QVariantList:
1147 case QMetaType::Type::QStringList:
1148 textSize = -1;
1149 textSizeProperty = parseValueList( jsonTextSize.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &textSize );
1150 break;
1151
1152 default:
1153 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextSize.userType() ) ) ) );
1154 break;
1155 }
1156
1157 if ( textSizeProperty )
1158 {
1159 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Size, textSizeProperty );
1160 }
1161 }
1162
1163 // a rough average of ems to character count conversion for a variety of fonts
1164 constexpr double EM_TO_CHARS = 2.0;
1165
1166 double textMaxWidth = -1;
1167 if ( jsonLayout.contains( QStringLiteral( "text-max-width" ) ) )
1168 {
1169 const QVariant jsonTextMaxWidth = jsonLayout.value( QStringLiteral( "text-max-width" ) );
1170 switch ( jsonTextMaxWidth.userType() )
1171 {
1172 case QMetaType::Type::Int:
1173 case QMetaType::Type::LongLong:
1174 case QMetaType::Type::Double:
1175 textMaxWidth = jsonTextMaxWidth.toDouble() * EM_TO_CHARS;
1176 break;
1177
1178 case QMetaType::Type::QVariantMap:
1179 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::AutoWrapLength, parseInterpolateByZoom( jsonTextMaxWidth.toMap(), context, EM_TO_CHARS, &textMaxWidth ) );
1180 break;
1181
1182 case QMetaType::Type::QVariantList:
1183 case QMetaType::Type::QStringList:
1184 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::AutoWrapLength, parseValueList( jsonTextMaxWidth.toList(), PropertyType::Numeric, context, EM_TO_CHARS, 255, nullptr, &textMaxWidth ) );
1185 break;
1186
1187 default:
1188 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-max-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextMaxWidth.userType() ) ) ) );
1189 break;
1190 }
1191 }
1192 else
1193 {
1194 // defaults to 10
1195 textMaxWidth = 10 * EM_TO_CHARS;
1196 }
1197
1198 double textLetterSpacing = -1;
1199 if ( jsonLayout.contains( QStringLiteral( "text-letter-spacing" ) ) )
1200 {
1201 const QVariant jsonTextLetterSpacing = jsonLayout.value( QStringLiteral( "text-letter-spacing" ) );
1202 switch ( jsonTextLetterSpacing.userType() )
1203 {
1204 case QMetaType::Type::Int:
1205 case QMetaType::Type::LongLong:
1206 case QMetaType::Type::Double:
1207 textLetterSpacing = jsonTextLetterSpacing.toDouble();
1208 break;
1209
1210 case QMetaType::Type::QVariantMap:
1211 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::FontLetterSpacing, parseInterpolateByZoom( jsonTextLetterSpacing.toMap(), context, 1, &textLetterSpacing ) );
1212 break;
1213
1214 case QMetaType::Type::QVariantList:
1215 case QMetaType::Type::QStringList:
1216 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::FontLetterSpacing, parseValueList( jsonTextLetterSpacing.toList(), PropertyType::Numeric, context, 1, 255, nullptr, &textLetterSpacing ) );
1217 break;
1218
1219 default:
1220 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-letter-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextLetterSpacing.userType() ) ) ) );
1221 break;
1222 }
1223 }
1224
1225 QFont textFont;
1226 bool foundFont = false;
1227 QString fontName;
1228 QString fontStyleName;
1229
1230 if ( jsonLayout.contains( QStringLiteral( "text-font" ) ) )
1231 {
1232 auto splitFontFamily = []( const QString & fontName, QString & family, QString & style ) -> bool
1233 {
1234 QString matchedFamily;
1235 const QStringList textFontParts = fontName.split( ' ' );
1236 for ( int i = textFontParts.size() - 1; i >= 1; --i )
1237 {
1238 const QString candidateFontFamily = textFontParts.mid( 0, i ).join( ' ' );
1239 const QString candidateFontStyle = textFontParts.mid( i ).join( ' ' );
1240
1241 const QString processedFontFamily = QgsApplication::fontManager()->processFontFamilyName( candidateFontFamily );
1242 if ( QgsFontUtils::fontFamilyHasStyle( processedFontFamily, candidateFontStyle ) )
1243 {
1244 family = processedFontFamily;
1245 style = candidateFontStyle;
1246 return true;
1247 }
1248 else if ( QgsApplication::fontManager()->tryToDownloadFontFamily( processedFontFamily, matchedFamily ) )
1249 {
1250 if ( processedFontFamily == matchedFamily )
1251 {
1252 family = processedFontFamily;
1253 style = candidateFontStyle;
1254 }
1255 else
1256 {
1257 family = matchedFamily;
1258 style = processedFontFamily;
1259 style.replace( matchedFamily, QString() );
1260 style = style.trimmed();
1261 if ( !style.isEmpty() && !candidateFontStyle.isEmpty() )
1262 {
1263 style += QStringLiteral( " %1" ).arg( candidateFontStyle );
1264 }
1265 }
1266 return true;
1267 }
1268 }
1269
1270 const QString processedFontFamily = QgsApplication::fontManager()->processFontFamilyName( fontName );
1271 if ( QFontDatabase().hasFamily( processedFontFamily ) )
1272 {
1273 // the json isn't following the spec correctly!!
1274 family = processedFontFamily;
1275 style.clear();
1276 return true;
1277 }
1278 else if ( QgsApplication::fontManager()->tryToDownloadFontFamily( processedFontFamily, matchedFamily ) )
1279 {
1280 family = matchedFamily;
1281 style.clear();
1282 return true;
1283 }
1284 return false;
1285 };
1286
1287 const QVariant jsonTextFont = jsonLayout.value( QStringLiteral( "text-font" ) );
1288 if ( jsonTextFont.userType() != QMetaType::Type::QVariantList && jsonTextFont.userType() != QMetaType::Type::QStringList && jsonTextFont.userType() != QMetaType::Type::QString
1289 && jsonTextFont.userType() != QMetaType::Type::QVariantMap )
1290 {
1291 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-font type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextFont.userType() ) ) ) );
1292 }
1293 else
1294 {
1295 switch ( jsonTextFont.userType() )
1296 {
1297 case QMetaType::Type::QVariantList:
1298 case QMetaType::Type::QStringList:
1299 fontName = jsonTextFont.toList().value( 0 ).toString();
1300 break;
1301
1302 case QMetaType::Type::QString:
1303 fontName = jsonTextFont.toString();
1304 break;
1305
1306 case QMetaType::Type::QVariantMap:
1307 {
1308 QString familyCaseString = QStringLiteral( "CASE " );
1309 QString styleCaseString = QStringLiteral( "CASE " );
1310 QString fontFamily;
1311 const QVariantList stops = jsonTextFont.toMap().value( QStringLiteral( "stops" ) ).toList();
1312
1313 bool error = false;
1314 for ( int i = 0; i < stops.length() - 1; ++i )
1315 {
1316 // bottom zoom and value
1317 const QVariant bz = stops.value( i ).toList().value( 0 );
1318 const QString bv = stops.value( i ).toList().value( 1 ).userType() == QMetaType::Type::QString ? stops.value( i ).toList().value( 1 ).toString() : stops.value( i ).toList().value( 1 ).toList().value( 0 ).toString();
1319 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
1320 {
1321 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1322 error = true;
1323 break;
1324 }
1325
1326 // top zoom
1327 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
1328 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
1329 {
1330 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1331 error = true;
1332 break;
1333 }
1334
1335 if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1336 {
1337 familyCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1338 "THEN %3 " ).arg( bz.toString(),
1339 tz.toString(),
1340 QgsExpression::quotedValue( fontFamily ) );
1341 styleCaseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1342 "THEN %3 " ).arg( bz.toString(),
1343 tz.toString(),
1344 QgsExpression::quotedValue( fontStyleName ) );
1345 }
1346 else
1347 {
1348 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1349 }
1350 }
1351 if ( error )
1352 break;
1353
1354 const QString bv = stops.constLast().toList().value( 1 ).userType() == QMetaType::Type::QString ? stops.constLast().toList().value( 1 ).toString() : stops.constLast().toList().value( 1 ).toList().value( 0 ).toString();
1355 if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1356 {
1357 familyCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontFamily ) );
1358 styleCaseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( fontStyleName ) );
1359 }
1360 else
1361 {
1362 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1363 }
1364
1365 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Family, QgsProperty::fromExpression( familyCaseString ) );
1367
1368 foundFont = true;
1369 fontName = fontFamily;
1370
1371 break;
1372 }
1373
1374 default:
1375 break;
1376 }
1377
1378 QString fontFamily;
1379 if ( splitFontFamily( fontName, fontFamily, fontStyleName ) )
1380 {
1381 textFont = QgsFontUtils::createFont( fontFamily );
1382 if ( !fontStyleName.isEmpty() )
1383 textFont.setStyleName( fontStyleName );
1384 foundFont = true;
1385 }
1386 }
1387 }
1388 else
1389 {
1390 // Defaults to ["Open Sans Regular","Arial Unicode MS Regular"].
1391 if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Open Sans" ), QStringLiteral( "Regular" ) ) )
1392 {
1393 fontName = QStringLiteral( "Open Sans" );
1394 textFont = QgsFontUtils::createFont( fontName );
1395 textFont.setStyleName( QStringLiteral( "Regular" ) );
1396 fontStyleName = QStringLiteral( "Regular" );
1397 foundFont = true;
1398 }
1399 else if ( QgsFontUtils::fontFamilyHasStyle( QStringLiteral( "Arial Unicode MS" ), QStringLiteral( "Regular" ) ) )
1400 {
1401 fontName = QStringLiteral( "Arial Unicode MS" );
1402 textFont = QgsFontUtils::createFont( fontName );
1403 textFont.setStyleName( QStringLiteral( "Regular" ) );
1404 fontStyleName = QStringLiteral( "Regular" );
1405 foundFont = true;
1406 }
1407 else
1408 {
1409 fontName = QStringLiteral( "Open Sans, Arial Unicode MS" );
1410 }
1411 }
1412 if ( !foundFont && !fontName.isEmpty() )
1413 {
1414 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), fontName ) );
1415 }
1416
1417 // text color
1418 QColor textColor;
1419 if ( jsonPaint.contains( QStringLiteral( "text-color" ) ) )
1420 {
1421 const QVariant jsonTextColor = jsonPaint.value( QStringLiteral( "text-color" ) );
1422 switch ( jsonTextColor.userType() )
1423 {
1424 case QMetaType::Type::QVariantMap:
1425 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Color, parseInterpolateColorByZoom( jsonTextColor.toMap(), context, &textColor ) );
1426 break;
1427
1428 case QMetaType::Type::QVariantList:
1429 case QMetaType::Type::QStringList:
1430 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Color, parseValueList( jsonTextColor.toList(), PropertyType::Color, context, 1, 255, &textColor ) );
1431 break;
1432
1433 case QMetaType::Type::QString:
1434 textColor = parseColor( jsonTextColor.toString(), context );
1435 break;
1436
1437 default:
1438 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextColor.userType() ) ) ) );
1439 break;
1440 }
1441 }
1442 else
1443 {
1444 // defaults to #000000
1445 textColor = QColor( 0, 0, 0 );
1446 }
1447
1448 // buffer color
1449 QColor bufferColor( 0, 0, 0, 0 );
1450 if ( jsonPaint.contains( QStringLiteral( "text-halo-color" ) ) )
1451 {
1452 const QVariant jsonBufferColor = jsonPaint.value( QStringLiteral( "text-halo-color" ) );
1453 switch ( jsonBufferColor.userType() )
1454 {
1455 case QMetaType::Type::QVariantMap:
1456 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferColor, parseInterpolateColorByZoom( jsonBufferColor.toMap(), context, &bufferColor ) );
1457 break;
1458
1459 case QMetaType::Type::QVariantList:
1460 case QMetaType::Type::QStringList:
1461 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferColor, parseValueList( jsonBufferColor.toList(), PropertyType::Color, context, 1, 255, &bufferColor ) );
1462 break;
1463
1464 case QMetaType::Type::QString:
1465 bufferColor = parseColor( jsonBufferColor.toString(), context );
1466 break;
1467
1468 default:
1469 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonBufferColor.userType() ) ) ) );
1470 break;
1471 }
1472 }
1473
1474 double bufferSize = 0.0;
1475 // the pixel based text buffers appear larger when rendered on the web - so automatically scale
1476 // them up when converting to a QGIS style
1477 // (this number is based on trial-and-error comparisons only!)
1478 constexpr double BUFFER_SIZE_SCALE = 2.0;
1479 if ( jsonPaint.contains( QStringLiteral( "text-halo-width" ) ) )
1480 {
1481 const QVariant jsonHaloWidth = jsonPaint.value( QStringLiteral( "text-halo-width" ) );
1482 QString bufferSizeDataDefined;
1483 switch ( jsonHaloWidth.userType() )
1484 {
1485 case QMetaType::Type::Int:
1486 case QMetaType::Type::LongLong:
1487 case QMetaType::Type::Double:
1488 bufferSize = jsonHaloWidth.toDouble() * context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE;
1489 break;
1490
1491 case QMetaType::Type::QVariantMap:
1492 bufferSize = 1;
1493 bufferSizeDataDefined = parseInterpolateByZoom( jsonHaloWidth.toMap(), context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, &bufferSize ).asExpression();
1494 break;
1495
1496 case QMetaType::Type::QVariantList:
1497 case QMetaType::Type::QStringList:
1498 bufferSize = 1;
1499 bufferSizeDataDefined = parseValueList( jsonHaloWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, 255, nullptr, &bufferSize ).asExpression();
1500 break;
1501
1502 default:
1503 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonHaloWidth.userType() ) ) ) );
1504 break;
1505 }
1506
1507 // from the specs halo should not be larger than 1/4 of the text-size
1508 // https://docs.mapbox.com/style-spec/reference/layers/#paint-symbol-text-halo-width
1509 if ( bufferSize > 0 )
1510 {
1511 if ( textSize > 0 && bufferSizeDataDefined.isEmpty() )
1512 {
1513 bufferSize = std::min( bufferSize, textSize * BUFFER_SIZE_SCALE / 4 );
1514 }
1515 else if ( textSize > 0 && !bufferSizeDataDefined.isEmpty() )
1516 {
1517 bufferSizeDataDefined = QStringLiteral( "min(%1/4, %2)" ).arg( textSize * BUFFER_SIZE_SCALE ).arg( bufferSizeDataDefined );
1518 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1519 }
1520 else if ( !bufferSizeDataDefined.isEmpty() )
1521 {
1522 bufferSizeDataDefined = QStringLiteral( "min(%1*%2/4, %3)" )
1523 .arg( textSizeProperty.asExpression() )
1524 .arg( BUFFER_SIZE_SCALE )
1525 .arg( bufferSizeDataDefined );
1526 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1527 }
1528 else if ( bufferSizeDataDefined.isEmpty() )
1529 {
1530 bufferSizeDataDefined = QStringLiteral( "min(%1*%2/4, %3)" )
1531 .arg( textSizeProperty.asExpression() )
1532 .arg( BUFFER_SIZE_SCALE )
1533 .arg( bufferSize );
1534 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1535 }
1536 }
1537 }
1538
1539 double haloBlurSize = 0;
1540 if ( jsonPaint.contains( QStringLiteral( "text-halo-blur" ) ) )
1541 {
1542 const QVariant jsonTextHaloBlur = jsonPaint.value( QStringLiteral( "text-halo-blur" ) );
1543 switch ( jsonTextHaloBlur.userType() )
1544 {
1545 case QMetaType::Type::Int:
1546 case QMetaType::Type::LongLong:
1547 case QMetaType::Type::Double:
1548 {
1549 haloBlurSize = jsonTextHaloBlur.toDouble() * context.pixelSizeConversionFactor();
1550 break;
1551 }
1552
1553 default:
1554 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-blur type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextHaloBlur.userType() ) ) ) );
1555 break;
1556 }
1557 }
1558
1559 QgsTextFormat format;
1560 format.setSizeUnit( context.targetUnit() );
1561 if ( textColor.isValid() )
1562 format.setColor( textColor );
1563 if ( textSize >= 0 )
1564 format.setSize( textSize );
1565 if ( foundFont )
1566 {
1567 format.setFont( textFont );
1568 if ( !fontStyleName.isEmpty() )
1569 format.setNamedStyle( fontStyleName );
1570 }
1571 if ( textLetterSpacing > 0 )
1572 {
1573 QFont f = format.font();
1574 f.setLetterSpacing( QFont::AbsoluteSpacing, textLetterSpacing );
1575 format.setFont( f );
1576 }
1577
1578 if ( bufferSize > 0 )
1579 {
1580 // Color and opacity are separate components in QGIS
1581 const double opacity = bufferColor.alphaF();
1582 bufferColor.setAlphaF( 1.0 );
1583
1584 format.buffer().setEnabled( true );
1585 format.buffer().setSize( bufferSize );
1586 format.buffer().setSizeUnit( context.targetUnit() );
1587 format.buffer().setColor( bufferColor );
1588 format.buffer().setOpacity( opacity );
1589
1590 if ( haloBlurSize > 0 )
1591 {
1592 QgsEffectStack *stack = new QgsEffectStack();
1593 QgsBlurEffect *blur = new QgsBlurEffect() ;
1594 blur->setEnabled( true );
1595 blur->setBlurUnit( context.targetUnit() );
1596 blur->setBlurLevel( haloBlurSize );
1598 stack->appendEffect( blur );
1599 stack->setEnabled( true );
1600 format.buffer().setPaintEffect( stack );
1601 }
1602 }
1603
1604 QgsPalLayerSettings labelSettings;
1605
1606 if ( textMaxWidth > 0 )
1607 {
1608 labelSettings.autoWrapLength = textMaxWidth;
1609 }
1610
1611 // convert field name
1612 if ( jsonLayout.contains( QStringLiteral( "text-field" ) ) )
1613 {
1614 const QVariant jsonTextField = jsonLayout.value( QStringLiteral( "text-field" ) );
1615 switch ( jsonTextField.userType() )
1616 {
1617 case QMetaType::Type::QString:
1618 {
1619 labelSettings.fieldName = processLabelField( jsonTextField.toString(), labelSettings.isExpression );
1620 break;
1621 }
1622
1623 case QMetaType::Type::QVariantList:
1624 case QMetaType::Type::QStringList:
1625 {
1626 const QVariantList textFieldList = jsonTextField.toList();
1627 /*
1628 * e.g.
1629 * "text-field": ["format",
1630 * "foo", { "font-scale": 1.2 },
1631 * "bar", { "font-scale": 0.8 }
1632 * ]
1633 */
1634 if ( textFieldList.size() > 2 && textFieldList.at( 0 ).toString() == QLatin1String( "format" ) )
1635 {
1636 QStringList parts;
1637 for ( int i = 1; i < textFieldList.size(); ++i )
1638 {
1639 bool isExpression = false;
1640 const QString part = processLabelField( textFieldList.at( i ).toString(), isExpression );
1641 if ( !isExpression )
1642 parts << QgsExpression::quotedColumnRef( part );
1643 else
1644 parts << part;
1645 // TODO -- we could also translate font color, underline, overline, strikethrough to HTML tags!
1646 i += 1;
1647 }
1648 labelSettings.fieldName = QStringLiteral( "concat(%1)" ).arg( parts.join( ',' ) );
1649 labelSettings.isExpression = true;
1650 }
1651 else
1652 {
1653 /*
1654 * e.g.
1655 * "text-field": ["to-string", ["get", "name"]]
1656 */
1657 labelSettings.fieldName = parseExpression( textFieldList, context );
1658 labelSettings.isExpression = true;
1659 }
1660 break;
1661 }
1662
1663 case QMetaType::Type::QVariantMap:
1664 {
1665 const QVariantList stops = jsonTextField.toMap().value( QStringLiteral( "stops" ) ).toList();
1666 if ( !stops.empty() )
1667 {
1668 labelSettings.fieldName = parseLabelStops( stops, context );
1669 labelSettings.isExpression = true;
1670 }
1671 else
1672 {
1673 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field dictionary" ).arg( context.layerId() ) );
1674 }
1675 break;
1676 }
1677
1678 default:
1679 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextField.userType() ) ) ) );
1680 break;
1681 }
1682 }
1683
1684 if ( jsonLayout.contains( QStringLiteral( "text-rotate" ) ) )
1685 {
1686 const QVariant jsonTextRotate = jsonLayout.value( QStringLiteral( "text-rotate" ) );
1687 switch ( jsonTextRotate.userType() )
1688 {
1689 case QMetaType::Type::Double:
1690 case QMetaType::Type::Int:
1691 {
1692 labelSettings.angleOffset = jsonTextRotate.toDouble();
1693 break;
1694 }
1695
1696 case QMetaType::Type::QVariantList:
1697 case QMetaType::Type::QStringList:
1698 {
1699 const QgsProperty property = parseValueList( jsonTextRotate.toList(), PropertyType::Numeric, context );
1700 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelRotation, property );
1701 break;
1702 }
1703
1704 default:
1705 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextRotate.userType() ) ) ) );
1706 break;
1707 }
1708 }
1709
1710 if ( jsonLayout.contains( QStringLiteral( "text-transform" ) ) )
1711 {
1712 const QString textTransform = jsonLayout.value( QStringLiteral( "text-transform" ) ).toString();
1713 if ( textTransform == QLatin1String( "uppercase" ) )
1714 {
1715 labelSettings.fieldName = QStringLiteral( "upper(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1716 }
1717 else if ( textTransform == QLatin1String( "lowercase" ) )
1718 {
1719 labelSettings.fieldName = QStringLiteral( "lower(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1720 }
1721 labelSettings.isExpression = true;
1722 }
1723
1726 if ( jsonLayout.contains( QStringLiteral( "symbol-placement" ) ) )
1727 {
1728 const QString symbolPlacement = jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString();
1729 if ( symbolPlacement == QLatin1String( "line" ) )
1730 {
1733 geometryType = Qgis::GeometryType::Line;
1734
1735 if ( jsonLayout.contains( QStringLiteral( "text-rotation-alignment" ) ) )
1736 {
1737 const QString textRotationAlignment = jsonLayout.value( QStringLiteral( "text-rotation-alignment" ) ).toString();
1738 if ( textRotationAlignment == QLatin1String( "viewport" ) )
1739 {
1741 }
1742 }
1743
1744 if ( labelSettings.placement == Qgis::LabelPlacement::Curved )
1745 {
1746 QPointF textOffset;
1747 QgsProperty textOffsetProperty;
1748 if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1749 {
1750 const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1751
1752 // units are ems!
1753 switch ( jsonTextOffset.userType() )
1754 {
1755 case QMetaType::Type::QVariantMap:
1756 textOffsetProperty = parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, !textSizeProperty ? textSize : 1.0, &textOffset );
1757 if ( !textSizeProperty )
1758 {
1759 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelDistance, QStringLiteral( "abs(array_get(%1,1))-%2" ).arg( textOffsetProperty ).arg( textSize ) );
1760 }
1761 else
1762 {
1763 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelDistance, QStringLiteral( "with_variable('text_size',%2,abs(array_get(%1,1))*@text_size-@text_size)" ).arg( textOffsetProperty.asExpression(), textSizeProperty.asExpression() ) );
1764 }
1765 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LinePlacementOptions, QStringLiteral( "if(array_get(%1,1)>0,'BL','AL')" ).arg( textOffsetProperty ) );
1766 break;
1767
1768 case QMetaType::Type::QVariantList:
1769 case QMetaType::Type::QStringList:
1770 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1771 jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1772 break;
1773
1774 default:
1775 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) ) );
1776 break;
1777 }
1778
1779 if ( !textOffset.isNull() )
1780 {
1781 labelSettings.distUnits = context.targetUnit();
1782 labelSettings.dist = std::abs( textOffset.y() ) - textSize;
1784 if ( textSizeProperty && !textOffsetProperty )
1785 {
1786 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelDistance, QStringLiteral( "with_variable('text_size',%2,%1*@text_size-@text_size)" ).arg( std::abs( textOffset.y() / textSize ) ).arg( textSizeProperty.asExpression() ) );
1787 }
1788 }
1789 }
1790
1791 if ( textOffset.isNull() )
1792 {
1794 }
1795 }
1796 }
1797 }
1798
1799 if ( jsonLayout.contains( QStringLiteral( "text-justify" ) ) )
1800 {
1801 const QVariant jsonTextJustify = jsonLayout.value( QStringLiteral( "text-justify" ) );
1802
1803 // default is center
1804 QString textAlign = QStringLiteral( "center" );
1805
1806 const QVariantMap conversionMap
1807 {
1808 { QStringLiteral( "left" ), QStringLiteral( "left" ) },
1809 { QStringLiteral( "center" ), QStringLiteral( "center" ) },
1810 { QStringLiteral( "right" ), QStringLiteral( "right" ) },
1811 { QStringLiteral( "auto" ), QStringLiteral( "follow" ) }
1812 };
1813
1814 switch ( jsonTextJustify.userType() )
1815 {
1816 case QMetaType::Type::QString:
1817 textAlign = jsonTextJustify.toString();
1818 break;
1819
1820 case QMetaType::Type::QVariantList:
1821 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextJustify.toList(), context, conversionMap, &textAlign ) ) );
1822 break;
1823
1824 case QMetaType::Type::QVariantMap:
1825 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextJustify.toMap(), context, conversionMap, &textAlign ) );
1826 break;
1827
1828 default:
1829 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-justify type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextJustify.userType() ) ) ) );
1830 break;
1831 }
1832
1833 if ( textAlign == QLatin1String( "left" ) )
1835 else if ( textAlign == QLatin1String( "right" ) )
1837 else if ( textAlign == QLatin1String( "center" ) )
1839 else if ( textAlign == QLatin1String( "follow" ) )
1841 }
1842 else
1843 {
1845 }
1846
1847 if ( labelSettings.placement == Qgis::LabelPlacement::OverPoint )
1848 {
1849 if ( jsonLayout.contains( QStringLiteral( "text-anchor" ) ) )
1850 {
1851 const QVariant jsonTextAnchor = jsonLayout.value( QStringLiteral( "text-anchor" ) );
1852 QString textAnchor;
1853
1854 const QVariantMap conversionMap
1855 {
1856 { QStringLiteral( "center" ), 4 },
1857 { QStringLiteral( "left" ), 5 },
1858 { QStringLiteral( "right" ), 3 },
1859 { QStringLiteral( "top" ), 7 },
1860 { QStringLiteral( "bottom" ), 1 },
1861 { QStringLiteral( "top-left" ), 8 },
1862 { QStringLiteral( "top-right" ), 6 },
1863 { QStringLiteral( "bottom-left" ), 2 },
1864 { QStringLiteral( "bottom-right" ), 0 },
1865 };
1866
1867 switch ( jsonTextAnchor.userType() )
1868 {
1869 case QMetaType::Type::QString:
1870 textAnchor = jsonTextAnchor.toString();
1871 break;
1872
1873 case QMetaType::Type::QVariantList:
1874 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextAnchor.toList(), context, conversionMap, &textAnchor ) ) );
1875 break;
1876
1877 case QMetaType::Type::QVariantMap:
1878 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextAnchor.toMap(), context, conversionMap, &textAnchor ) );
1879 break;
1880
1881 default:
1882 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-anchor type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextAnchor.userType() ) ) ) );
1883 break;
1884 }
1885
1886 if ( textAnchor == QLatin1String( "center" ) )
1888 else if ( textAnchor == QLatin1String( "left" ) )
1890 else if ( textAnchor == QLatin1String( "right" ) )
1892 else if ( textAnchor == QLatin1String( "top" ) )
1894 else if ( textAnchor == QLatin1String( "bottom" ) )
1896 else if ( textAnchor == QLatin1String( "top-left" ) )
1898 else if ( textAnchor == QLatin1String( "top-right" ) )
1900 else if ( textAnchor == QLatin1String( "bottom-left" ) )
1902 else if ( textAnchor == QLatin1String( "bottom-right" ) )
1904 }
1905
1906 QPointF textOffset;
1907 if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1908 {
1909 const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1910
1911 // units are ems!
1912 switch ( jsonTextOffset.userType() )
1913 {
1914 case QMetaType::Type::QVariantMap:
1915 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetXY, parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, textSize, &textOffset ) );
1916 break;
1917
1918 case QMetaType::Type::QVariantList:
1919 case QMetaType::Type::QStringList:
1920 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1921 jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1922 break;
1923
1924 default:
1925 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) ) );
1926 break;
1927 }
1928
1929 if ( !textOffset.isNull() )
1930 {
1931 labelSettings.offsetUnits = context.targetUnit();
1932 labelSettings.xOffset = textOffset.x();
1933 labelSettings.yOffset = textOffset.y();
1934 }
1935 }
1936 }
1937
1938 if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) &&
1939 ( labelSettings.placement == Qgis::LabelPlacement::Horizontal || labelSettings.placement == Qgis::LabelPlacement::Curved ) )
1940 {
1941 QSize spriteSize;
1942 QString spriteProperty, spriteSizeProperty;
1943 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1944 if ( !sprite.isEmpty() )
1945 {
1946 double size = 1.0;
1947 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1948 {
1949 QgsProperty property;
1950 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1951 switch ( jsonIconSize.userType() )
1952 {
1953 case QMetaType::Type::Int:
1954 case QMetaType::Type::LongLong:
1955 case QMetaType::Type::Double:
1956 {
1957 size = jsonIconSize.toDouble();
1958 if ( !spriteSizeProperty.isEmpty() )
1959 {
1961 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
1962 }
1963 break;
1964 }
1965
1966 case QMetaType::Type::QVariantMap:
1967 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
1968 break;
1969
1970 case QMetaType::Type::QVariantList:
1971 case QMetaType::Type::QStringList:
1972 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
1973 break;
1974 default:
1975 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
1976 break;
1977 }
1978
1979 if ( !property.expressionString().isEmpty() )
1980 {
1981 if ( !spriteSizeProperty.isEmpty() )
1982 {
1984 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
1985 }
1986 else
1987 {
1989 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
1990 }
1991 }
1992 }
1993
1995 markerLayer->setPath( sprite );
1996 markerLayer->setSize( spriteSize.width() );
1997 markerLayer->setSizeUnit( context.targetUnit() );
1998
1999 if ( !spriteProperty.isEmpty() )
2000 {
2001 QgsPropertyCollection markerDdProperties;
2002 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2003 markerLayer->setDataDefinedProperties( markerDdProperties );
2004 }
2005
2006 QgsTextBackgroundSettings backgroundSettings;
2007 backgroundSettings.setEnabled( true );
2009 backgroundSettings.setSize( spriteSize * size );
2010 backgroundSettings.setSizeUnit( context.targetUnit() );
2012 backgroundSettings.setMarkerSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2013 format.setBackground( backgroundSettings );
2014 }
2015 }
2016
2017 if ( textSize >= 0 )
2018 {
2019 // TODO -- this probably needs revisiting -- it was copied from the MapTiler code, but may be wrong...
2020 labelSettings.priority = std::min( textSize / ( context.pixelSizeConversionFactor() * 3 ), 10.0 );
2021 }
2022
2023 labelSettings.setFormat( format );
2024
2025 // use a low obstacle weight for layers by default -- we'd rather have more labels for these layers, even if placement isn't ideal
2026 labelSettings.obstacleSettings().setFactor( 0.1 );
2027
2028 labelSettings.setDataDefinedProperties( ddLabelProperties );
2029
2030 labelingStyle.setGeometryType( geometryType );
2031 labelingStyle.setLabelSettings( labelSettings );
2032
2033 hasLabeling = true;
2034
2035 hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
2036}
2037
2039{
2040 if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
2041 {
2042 context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
2043 return false;
2044 }
2045 const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
2046
2047 if ( jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString() == QLatin1String( "line" ) && !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
2048 {
2049 QgsPropertyCollection ddProperties;
2050
2051 double spacing = -1.0;
2052 if ( jsonLayout.contains( QStringLiteral( "symbol-spacing" ) ) )
2053 {
2054 const QVariant jsonSpacing = jsonLayout.value( QStringLiteral( "symbol-spacing" ) );
2055 switch ( jsonSpacing.userType() )
2056 {
2057 case QMetaType::Type::Int:
2058 case QMetaType::Type::LongLong:
2059 case QMetaType::Type::Double:
2060 spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
2061 break;
2062
2063 case QMetaType::Type::QVariantMap:
2064 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing ) );
2065 break;
2066
2067 case QMetaType::Type::QVariantList:
2068 case QMetaType::Type::QStringList:
2069 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing ) );
2070 break;
2071
2072 default:
2073 context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonSpacing.userType() ) ) ) );
2074 break;
2075 }
2076 }
2077 else
2078 {
2079 // defaults to 250
2080 spacing = 250 * context.pixelSizeConversionFactor();
2081 }
2082
2083 bool rotateMarkers = true;
2084 if ( jsonLayout.contains( QStringLiteral( "icon-rotation-alignment" ) ) )
2085 {
2086 const QString alignment = jsonLayout.value( QStringLiteral( "icon-rotation-alignment" ) ).toString();
2087 if ( alignment == QLatin1String( "map" ) || alignment == QLatin1String( "auto" ) )
2088 {
2089 rotateMarkers = true;
2090 }
2091 else if ( alignment == QLatin1String( "viewport" ) )
2092 {
2093 rotateMarkers = false;
2094 }
2095 }
2096
2097 QgsPropertyCollection markerDdProperties;
2098 double rotation = 0.0;
2099 if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
2100 {
2101 const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
2102 switch ( jsonIconRotate.userType() )
2103 {
2104 case QMetaType::Type::Int:
2105 case QMetaType::Type::LongLong:
2106 case QMetaType::Type::Double:
2107 rotation = jsonIconRotate.toDouble();
2108 break;
2109
2110 case QMetaType::Type::QVariantMap:
2111 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2112 break;
2113
2114 case QMetaType::Type::QVariantList:
2115 case QMetaType::Type::QStringList:
2116 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2117 break;
2118
2119 default:
2120 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) ) );
2121 break;
2122 }
2123 }
2124
2125 QgsMarkerLineSymbolLayer *lineSymbol = new QgsMarkerLineSymbolLayer( rotateMarkers, spacing > 0 ? spacing : 1 );
2126 lineSymbol->setOutputUnit( context.targetUnit() );
2127 lineSymbol->setDataDefinedProperties( ddProperties );
2128 if ( spacing < 1 )
2129 {
2130 // if spacing isn't specified, it's a central point marker only
2132 }
2133
2135 QSize spriteSize;
2136 QString spriteProperty, spriteSizeProperty;
2137 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
2138 if ( !sprite.isNull() )
2139 {
2140 markerLayer->setPath( sprite );
2141 markerLayer->setSize( spriteSize.width() );
2142 markerLayer->setSizeUnit( context.targetUnit() );
2143
2144 if ( !spriteProperty.isEmpty() )
2145 {
2146 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2147 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2148 }
2149 }
2150
2151 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
2152 {
2153 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
2154 double size = 1.0;
2155 QgsProperty property;
2156 switch ( jsonIconSize.userType() )
2157 {
2158 case QMetaType::Type::Int:
2159 case QMetaType::Type::LongLong:
2160 case QMetaType::Type::Double:
2161 {
2162 size = jsonIconSize.toDouble();
2163 if ( !spriteSizeProperty.isEmpty() )
2164 {
2165 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2166 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
2167 }
2168 break;
2169 }
2170
2171 case QMetaType::Type::QVariantMap:
2172 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2173 break;
2174
2175 case QMetaType::Type::QVariantList:
2176 case QMetaType::Type::QStringList:
2177 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2178 break;
2179 default:
2180 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
2181 break;
2182 }
2183 markerLayer->setSize( size * spriteSize.width() );
2184 if ( !property.expressionString().isEmpty() )
2185 {
2186 if ( !spriteSizeProperty.isEmpty() )
2187 {
2188 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2189 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2190 }
2191 else
2192 {
2193 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2194 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2195 }
2196 }
2197 }
2198
2199 markerLayer->setDataDefinedProperties( markerDdProperties );
2200 markerLayer->setAngle( rotation );
2201 lineSymbol->setSubSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2202
2203 std::unique_ptr< QgsSymbol > symbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << lineSymbol );
2204
2205 // set render units
2206 symbol->setOutputUnit( context.targetUnit() );
2207 lineSymbol->setOutputUnit( context.targetUnit() );
2208
2210 rendererStyle.setSymbol( symbol.release() );
2211 return true;
2212 }
2213 else if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) )
2214 {
2215 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
2216
2217 QSize spriteSize;
2218 QString spriteProperty, spriteSizeProperty;
2219 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
2220 if ( !sprite.isEmpty() || !spriteProperty.isEmpty() )
2221 {
2223 rasterMarker->setPath( sprite );
2224 rasterMarker->setSize( spriteSize.width() );
2225 rasterMarker->setSizeUnit( context.targetUnit() );
2226
2227 QgsPropertyCollection markerDdProperties;
2228 if ( !spriteProperty.isEmpty() )
2229 {
2230 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2231 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2232 }
2233
2234 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
2235 {
2236 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
2237 double size = 1.0;
2238 QgsProperty property;
2239 switch ( jsonIconSize.userType() )
2240 {
2241 case QMetaType::Type::Int:
2242 case QMetaType::Type::LongLong:
2243 case QMetaType::Type::Double:
2244 {
2245 size = jsonIconSize.toDouble();
2246 if ( !spriteSizeProperty.isEmpty() )
2247 {
2248 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2249 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
2250 }
2251 break;
2252 }
2253
2254 case QMetaType::Type::QVariantMap:
2255 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2256 break;
2257
2258 case QMetaType::Type::QVariantList:
2259 case QMetaType::Type::QStringList:
2260 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2261 break;
2262 default:
2263 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
2264 break;
2265 }
2266 rasterMarker->setSize( size * spriteSize.width() );
2267 if ( !property.expressionString().isEmpty() )
2268 {
2269 if ( !spriteSizeProperty.isEmpty() )
2270 {
2271 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2272 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2273 }
2274 else
2275 {
2276 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2277 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2278 }
2279 }
2280 }
2281
2282 double rotation = 0.0;
2283 if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
2284 {
2285 const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
2286 switch ( jsonIconRotate.userType() )
2287 {
2288 case QMetaType::Type::Int:
2289 case QMetaType::Type::LongLong:
2290 case QMetaType::Type::Double:
2291 rotation = jsonIconRotate.toDouble();
2292 break;
2293
2294 case QMetaType::Type::QVariantMap:
2295 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2296 break;
2297
2298 case QMetaType::Type::QVariantList:
2299 case QMetaType::Type::QStringList:
2300 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2301 break;
2302
2303 default:
2304 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) ) );
2305 break;
2306 }
2307 }
2308
2309 double iconOpacity = -1.0;
2310 if ( jsonPaint.contains( QStringLiteral( "icon-opacity" ) ) )
2311 {
2312 const QVariant jsonIconOpacity = jsonPaint.value( QStringLiteral( "icon-opacity" ) );
2313 switch ( jsonIconOpacity.userType() )
2314 {
2315 case QMetaType::Type::Int:
2316 case QMetaType::Type::LongLong:
2317 case QMetaType::Type::Double:
2318 iconOpacity = jsonIconOpacity.toDouble();
2319 break;
2320
2321 case QMetaType::Type::QVariantMap:
2322 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseInterpolateByZoom( jsonIconOpacity.toMap(), context, 100, &iconOpacity ) );
2323 break;
2324
2325 case QMetaType::Type::QVariantList:
2326 case QMetaType::Type::QStringList:
2327 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseValueList( jsonIconOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &iconOpacity ) );
2328 break;
2329
2330 default:
2331 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconOpacity.userType() ) ) ) );
2332 break;
2333 }
2334 }
2335
2336 rasterMarker->setDataDefinedProperties( markerDdProperties );
2337 rasterMarker->setAngle( rotation );
2338 if ( iconOpacity >= 0 )
2339 rasterMarker->setOpacity( iconOpacity );
2340
2341 QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << rasterMarker );
2342 rendererStyle.setSymbol( markerSymbol );
2344 return true;
2345 }
2346 }
2347
2348 return false;
2349}
2350
2352{
2353 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2354 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2355 if ( stops.empty() )
2356 return QgsProperty();
2357
2358 QString caseString = QStringLiteral( "CASE " );
2359 const QString colorComponent( "color_part(%1,'%2')" );
2360
2361 for ( int i = 0; i < stops.length() - 1; ++i )
2362 {
2363 // step bottom zoom
2364 const QString bz = stops.at( i ).toList().value( 0 ).toString();
2365 // step top zoom
2366 const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
2367
2368 const QVariant bcVariant = stops.at( i ).toList().value( 1 );
2369 const QVariant tcVariant = stops.at( i + 1 ).toList().value( 1 );
2370
2371 const QColor bottomColor = parseColor( bcVariant.toString(), context );
2372 const QColor topColor = parseColor( tcVariant.toString(), context );
2373
2374 if ( i == 0 && bottomColor.isValid() )
2375 {
2376 int bcHue;
2377 int bcSat;
2378 int bcLight;
2379 int bcAlpha;
2380 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2381 caseString += QStringLiteral( "WHEN @vector_tile_zoom < %1 THEN color_hsla(%2, %3, %4, %5) " )
2382 .arg( bz ).arg( bcHue ).arg( bcSat ).arg( bcLight ).arg( bcAlpha );
2383 }
2384
2385 if ( bottomColor.isValid() && topColor.isValid() )
2386 {
2387 int bcHue;
2388 int bcSat;
2389 int bcLight;
2390 int bcAlpha;
2391 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2392 int tcHue;
2393 int tcSat;
2394 int tcLight;
2395 int tcAlpha;
2396 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2397 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2398 "%3, %4, %5, %6) " ).arg( bz, tz,
2399 interpolateExpression( bz.toDouble(), tz.toDouble(), bcHue, tcHue, base, 1, &context ),
2400 interpolateExpression( bz.toDouble(), tz.toDouble(), bcSat, tcSat, base, 1, &context ),
2401 interpolateExpression( bz.toDouble(), tz.toDouble(), bcLight, tcLight, base, 1, &context ),
2402 interpolateExpression( bz.toDouble(), tz.toDouble(), bcAlpha, tcAlpha, base, 1, &context ) );
2403 }
2404 else
2405 {
2406 const QString bottomColorExpr = parseColorExpression( bcVariant, context );
2407 const QString topColorExpr = parseColorExpression( tcVariant, context );
2408
2409 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2410 "%3, %4, %5, %6) " ).arg( bz, tz,
2411 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_hue" ), colorComponent.arg( topColorExpr ).arg( "hsl_hue" ), base, 1, &context ),
2412 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_saturation" ), colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ), base, 1, &context ),
2413 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "lightness" ), colorComponent.arg( topColorExpr ).arg( "lightness" ), base, 1, &context ),
2414 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "alpha" ), colorComponent.arg( topColorExpr ).arg( "alpha" ), base, 1, &context ) );
2415 }
2416 }
2417
2418 // top color
2419 const QString tz = stops.last().toList().value( 0 ).toString();
2420 const QVariant tcVariant = stops.last().toList().value( 1 );
2421 QColor topColor;
2422 if ( tcVariant.userType() == QMetaType::Type::QString )
2423 {
2424 topColor = parseColor( tcVariant, context );
2425 if ( topColor.isValid() )
2426 {
2427 int tcHue;
2428 int tcSat;
2429 int tcLight;
2430 int tcAlpha;
2431 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2432 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2433 "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz ).arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );
2434 }
2435 }
2436 else if ( tcVariant.userType() == QMetaType::QVariantList )
2437 {
2438 const QString topColorExpr = parseColorExpression( tcVariant, context );
2439
2440 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2441 "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
2442 .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" ) );
2443 }
2444
2445 if ( !stops.empty() && defaultColor )
2446 *defaultColor = parseColor( stops.value( 0 ).toList().value( 1 ).toString(), context );
2447
2448 return QgsProperty::fromExpression( caseString );
2449}
2450
2451QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, double *defaultNumber )
2452{
2453 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2454 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2455 if ( stops.empty() )
2456 return QgsProperty();
2457
2458 QString scaleExpression;
2459 if ( stops.size() <= 2 )
2460 {
2461 scaleExpression = interpolateExpression(
2462 stops.value( 0 ).toList().value( 0 ).toDouble(), // zoomMin
2463 stops.last().toList().value( 0 ).toDouble(), // zoomMax
2464 stops.value( 0 ).toList().value( 1 ), // valueMin
2465 stops.last().toList().value( 1 ), // valueMax
2466 base, multiplier, &context );
2467 }
2468 else
2469 {
2470 scaleExpression = parseStops( base, stops, multiplier, context );
2471 }
2472
2473 if ( !stops.empty() && defaultNumber )
2474 *defaultNumber = stops.value( 0 ).toList().value( 1 ).toDouble() * multiplier;
2475
2476 return QgsProperty::fromExpression( scaleExpression );
2477}
2478
2480{
2482 if ( contextPtr )
2483 {
2484 context = *contextPtr;
2485 }
2486 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2487 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2488 if ( stops.empty() )
2489 return QgsProperty();
2490
2491 QString scaleExpression;
2492 if ( stops.length() <= 2 )
2493 {
2494 const QVariant bv = stops.value( 0 ).toList().value( 1 );
2495 const QVariant tv = stops.last().toList().value( 1 );
2496 double bottom = 0.0;
2497 double top = 0.0;
2498 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2499 scaleExpression = QStringLiteral( "set_color_part(@symbol_color, 'alpha', %1)" )
2501 stops.value( 0 ).toList().value( 0 ).toDouble(),
2502 stops.last().toList().value( 0 ).toDouble(),
2503 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2504 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ), base, 1, &context ) );
2505 }
2506 else
2507 {
2508 scaleExpression = parseOpacityStops( base, stops, maxOpacity, context );
2509 }
2510 return QgsProperty::fromExpression( scaleExpression );
2511}
2512
2513QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context )
2514{
2515 QString caseString = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
2516 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
2517 .arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );
2518
2519 for ( int i = 0; i < stops.size() - 1; ++i )
2520 {
2521 const QVariant bv = stops.value( i ).toList().value( 1 );
2522 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2523 double bottom = 0.0;
2524 double top = 0.0;
2525 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2526
2527 caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
2528 "THEN set_color_part(@symbol_color, 'alpha', %3)" )
2529 .arg( stops.value( i ).toList().value( 0 ).toString(),
2530 stops.value( i + 1 ).toList().value( 0 ).toString(),
2532 stops.value( i ).toList().value( 0 ).toDouble(),
2533 stops.value( i + 1 ).toList().value( 0 ).toDouble(),
2534 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2535 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2536 base, 1, &context ) );
2537 }
2538
2539
2540 bool numeric = false;
2541 const QVariant vv = stops.last().toList().value( 1 );
2542 double dv = vv.toDouble( &numeric );
2543
2544 caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
2545 "THEN set_color_part(@symbol_color, 'alpha', %2) END" ).arg(
2546 stops.last().toList().value( 0 ).toString(),
2547 numeric ? QString::number( dv * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( vv, context ) ).arg( maxOpacity )
2548 );
2549 return caseString;
2550}
2551
2552QgsProperty QgsMapBoxGlStyleConverter::parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, QPointF *defaultPoint )
2553{
2554 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2555 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2556 if ( stops.empty() )
2557 return QgsProperty();
2558
2559 QString scaleExpression;
2560 if ( stops.size() <= 2 )
2561 {
2562 scaleExpression = QStringLiteral( "array(%1,%2)" ).arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2563 stops.last().toList().value( 0 ).toDouble(),
2564 stops.value( 0 ).toList().value( 1 ).toList().value( 0 ),
2565 stops.last().toList().value( 1 ).toList().value( 0 ), base, multiplier, &context ),
2566 interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2567 stops.last().toList().value( 0 ).toDouble(),
2568 stops.value( 0 ).toList().value( 1 ).toList().value( 1 ),
2569 stops.last().toList().value( 1 ).toList().value( 1 ), base, multiplier, &context )
2570 );
2571 }
2572 else
2573 {
2574 scaleExpression = parsePointStops( base, stops, context, multiplier );
2575 }
2576
2577 if ( !stops.empty() && defaultPoint )
2578 *defaultPoint = QPointF( stops.value( 0 ).toList().value( 1 ).toList().value( 0 ).toDouble() * multiplier,
2579 stops.value( 0 ).toList().value( 1 ).toList().value( 1 ).toDouble() * multiplier );
2580
2581 return QgsProperty::fromExpression( scaleExpression );
2582}
2583
2585 const QVariantMap &conversionMap, QString *defaultString )
2586{
2587 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2588 if ( stops.empty() )
2589 return QgsProperty();
2590
2591 const QString scaleExpression = parseStringStops( stops, context, conversionMap, defaultString );
2592
2593 return QgsProperty::fromExpression( scaleExpression );
2594}
2595
2596QString QgsMapBoxGlStyleConverter::parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier )
2597{
2598 QString caseString = QStringLiteral( "CASE " );
2599
2600 for ( int i = 0; i < stops.length() - 1; ++i )
2601 {
2602 // bottom zoom and value
2603 const QVariant bz = stops.value( i ).toList().value( 0 );
2604 const QVariant bv = stops.value( i ).toList().value( 1 );
2605 if ( bv.userType() != QMetaType::Type::QVariantList && bv.userType() != QMetaType::Type::QStringList )
2606 {
2607 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( bz.userType() ) ) ) );
2608 return QString();
2609 }
2610
2611 // top zoom and value
2612 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2613 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2614 if ( tv.userType() != QMetaType::Type::QVariantList && tv.userType() != QMetaType::Type::QStringList )
2615 {
2616 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( tz.userType() ) ) ) );
2617 return QString();
2618 }
2619
2620 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2621 "THEN array(%3,%4)" ).arg( bz.toString(),
2622 tz.toString(),
2623 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ), tv.toList().value( 0 ), base, multiplier, &context ),
2624 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ), tv.toList().value( 1 ), base, multiplier, &context ) );
2625 }
2626 caseString += QLatin1String( "END" );
2627 return caseString;
2628}
2629
2630QString QgsMapBoxGlStyleConverter::parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &, double multiplier )
2631{
2632 if ( stops.length() < 2 )
2633 return QString();
2634
2635 QString caseString = QStringLiteral( "CASE" );
2636
2637 for ( int i = 0; i < stops.length(); ++i )
2638 {
2639 caseString += QLatin1String( " WHEN " );
2640 QStringList conditions;
2641 if ( i > 0 )
2642 {
2643 const QVariant bottomZoom = stops.value( i ).toList().value( 0 );
2644 conditions << QStringLiteral( "@vector_tile_zoom > %1" ).arg( bottomZoom.toString() );
2645 }
2646 if ( i < stops.length() - 1 )
2647 {
2648 const QVariant topZoom = stops.value( i + 1 ).toList().value( 0 );
2649 conditions << QStringLiteral( "@vector_tile_zoom <= %1" ).arg( topZoom.toString() );
2650 }
2651
2652 const QVariantList values = stops.value( i ).toList().value( 1 ).toList();
2653 QStringList valuesFixed;
2654 bool ok = false;
2655 for ( const QVariant &value : values )
2656 {
2657 const double number = value.toDouble( &ok );
2658 if ( ok )
2659 valuesFixed << QString::number( number * multiplier );
2660 }
2661
2662 // top zoom and value
2663 caseString += QStringLiteral( "%1 THEN array(%3)" ).arg(
2664 conditions.join( QLatin1String( " AND " ) ),
2665 valuesFixed.join( ',' )
2666 );
2667 }
2668 caseString += QLatin1String( " END" );
2669 return caseString;
2670}
2671
2672QString QgsMapBoxGlStyleConverter::parseStops( double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context )
2673{
2674 QString caseString = QStringLiteral( "CASE " );
2675
2676 for ( int i = 0; i < stops.length() - 1; ++i )
2677 {
2678 // bottom zoom and value
2679 const QVariant bz = stops.value( i ).toList().value( 0 );
2680 const QVariant bv = stops.value( i ).toList().value( 1 );
2681 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2682 {
2683 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2684 return QString();
2685 }
2686
2687 // top zoom and value
2688 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2689 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2690 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2691 {
2692 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2693 return QString();
2694 }
2695
2696 const QString lowerComparator = i == 0 ? QStringLiteral( ">=" ) : QStringLiteral( ">" );
2697
2698 caseString += QStringLiteral( "WHEN @vector_tile_zoom %1 %2 AND @vector_tile_zoom <= %3 "
2699 "THEN %4 " ).arg( lowerComparator,
2700 bz.toString(),
2701 tz.toString(),
2702 interpolateExpression( bz.toDouble(), tz.toDouble(), bv, tv, base, multiplier, &context ) );
2703 }
2704
2705 const QVariant z = stops.last().toList().value( 0 );
2706 const QVariant v = stops.last().toList().value( 1 );
2707 QString vStr = v.toString();
2708 if ( ( QMetaType::Type )v.userType() == QMetaType::QVariantList )
2709 {
2710 vStr = parseExpression( v.toList(), context );
2711 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2712 "THEN ( ( %2 ) * %3 ) END" ).arg( z.toString() ).arg( vStr ).arg( multiplier );
2713 }
2714 else
2715 {
2716 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2717 "THEN %2 END" ).arg( z.toString() ).arg( v.toDouble() * multiplier );
2718 }
2719
2720 return caseString;
2721}
2722
2723QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
2724{
2725 QString caseString = QStringLiteral( "CASE " );
2726
2727 for ( int i = 0; i < stops.length() - 1; ++i )
2728 {
2729 // bottom zoom and value
2730 const QVariant bz = stops.value( i ).toList().value( 0 );
2731 const QString bv = stops.value( i ).toList().value( 1 ).toString();
2732 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2733 {
2734 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2735 return QString();
2736 }
2737
2738 // top zoom
2739 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2740 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2741 {
2742 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2743 return QString();
2744 }
2745
2746 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2747 "THEN %3 " ).arg( bz.toString(),
2748 tz.toString(),
2749 QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
2750 }
2751 caseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( conversionMap.value( stops.constLast().toList().value( 1 ).toString(),
2752 stops.constLast().toList().value( 1 ) ) ) );
2753 if ( defaultString )
2754 *defaultString = stops.constLast().toList().value( 1 ).toString();
2755 return caseString;
2756}
2757
2759{
2760 QString caseString = QStringLiteral( "CASE " );
2761
2762 bool isExpression = false;
2763 for ( int i = 0; i < stops.length() - 1; ++i )
2764 {
2765 // bottom zoom and value
2766 const QVariant bz = stops.value( i ).toList().value( 0 );
2767 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2768 {
2769 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2770 return QString();
2771 }
2772
2773 // top zoom
2774 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2775 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2776 {
2777 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2778 return QString();
2779 }
2780
2781 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
2782 if ( fieldPart.isEmpty() )
2783 fieldPart = QStringLiteral( "''" );
2784 else if ( !isExpression )
2785 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
2786
2787 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom < %2 "
2788 "THEN %3 " ).arg( bz.toString(),
2789 tz.toString(),
2790 fieldPart ) ;
2791 }
2792
2793 {
2794 const QVariant bz = stops.constLast().toList().value( 0 );
2795 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2796 {
2797 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2798 return QString();
2799 }
2800
2801 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
2802 if ( fieldPart.isEmpty() )
2803 fieldPart = QStringLiteral( "''" );
2804 else if ( !isExpression )
2805 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
2806
2807 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 "
2808 "THEN %3 " ).arg( bz.toString(),
2809 fieldPart ) ;
2810 }
2811
2812 QString defaultPart = processLabelField( stops.constFirst().toList().value( 1 ).toString(), isExpression );
2813 if ( defaultPart.isEmpty() )
2814 defaultPart = QStringLiteral( "''" );
2815 else if ( !isExpression )
2816 defaultPart = QgsExpression::quotedColumnRef( defaultPart );
2817 caseString += QStringLiteral( "ELSE %1 END" ).arg( defaultPart );
2818
2819 return caseString;
2820}
2821
2822QgsProperty QgsMapBoxGlStyleConverter::parseValueList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2823{
2824 const QString method = json.value( 0 ).toString();
2825 if ( method == QLatin1String( "interpolate" ) )
2826 {
2827 return parseInterpolateListByZoom( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2828 }
2829 else if ( method == QLatin1String( "match" ) )
2830 {
2831 return parseMatchList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2832 }
2833 else if ( method == QLatin1String( "step" ) )
2834 {
2835 return parseStepList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2836 }
2837 else
2838 {
2839 return QgsProperty::fromExpression( parseExpression( json, context ) );
2840 }
2841}
2842
2843QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2844{
2845 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
2846 if ( attribute.isEmpty() )
2847 {
2848 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
2849 return QgsProperty();
2850 }
2851
2852 QString caseString = QStringLiteral( "CASE " );
2853
2854 for ( int i = 2; i < json.length() - 1; i += 2 )
2855 {
2856 QVariantList keys;
2857 QVariant variantKeys = json.value( i );
2858 if ( variantKeys.userType() == QMetaType::Type::QVariantList || variantKeys.userType() == QMetaType::Type::QStringList )
2859 keys = variantKeys.toList();
2860 else
2861 keys = {variantKeys};
2862
2863 QStringList matchString;
2864 for ( const QVariant &key : keys )
2865 {
2866 matchString << QgsExpression::quotedValue( key );
2867 }
2868
2869 const QVariant value = json.value( i + 1 );
2870
2871 QString valueString;
2872 switch ( type )
2873 {
2875 {
2876 if ( value.userType() == QMetaType::Type::QVariantList || value.userType() == QMetaType::Type::QStringList )
2877 {
2878 valueString = parseMatchList( value.toList(), PropertyType::Color, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
2879 }
2880 else
2881 {
2882 const QColor color = parseColor( value, context );
2883 valueString = QgsExpression::quotedString( color.name() );
2884 }
2885 break;
2886 }
2887
2889 {
2890 const double v = value.toDouble() * multiplier;
2891 valueString = QString::number( v );
2892 break;
2893 }
2894
2896 {
2897 const double v = value.toDouble() * maxOpacity;
2898 valueString = QString::number( v );
2899 break;
2900 }
2901
2903 {
2904 valueString = QStringLiteral( "array(%1,%2)" ).arg( value.toList().value( 0 ).toDouble() * multiplier,
2905 value.toList().value( 0 ).toDouble() * multiplier );
2906 break;
2907 }
2908
2910 {
2911 if ( value.toList().count() == 2 && value.toList().first().toString() == QLatin1String( "literal" ) )
2912 {
2913 valueString = QStringLiteral( "array(%1)" ).arg( value.toList().at( 1 ).toStringList().join( ',' ) );
2914 }
2915 else
2916 {
2917 valueString = QStringLiteral( "array(%1)" ).arg( value.toStringList().join( ',' ) );
2918 }
2919 break;
2920 }
2921 }
2922
2923 if ( matchString.count() == 1 )
2924 {
2925 caseString += QStringLiteral( "WHEN %1 IS %2 THEN %3 " ).arg( attribute, matchString.at( 0 ), valueString );
2926 }
2927 else
2928 {
2929 caseString += QStringLiteral( "WHEN %1 IN (%2) THEN %3 " ).arg( attribute, matchString.join( ',' ), valueString );
2930 }
2931 }
2932
2933 QVariant lastValue = json.constLast();
2934 QString elseValue;
2935
2936 switch ( lastValue.userType() )
2937 {
2938 case QMetaType::Type::QVariantList:
2939 case QMetaType::Type::QStringList:
2940 elseValue = parseValueList( lastValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
2941 break;
2942
2943 default:
2944 {
2945 switch ( type )
2946 {
2948 {
2949 const QColor color = parseColor( lastValue, context );
2950 if ( defaultColor )
2951 *defaultColor = color;
2952
2953 elseValue = QgsExpression::quotedString( color.name() );
2954 break;
2955 }
2956
2958 {
2959 const double v = json.constLast().toDouble() * multiplier;
2960 if ( defaultNumber )
2961 *defaultNumber = v;
2962 elseValue = QString::number( v );
2963 break;
2964 }
2965
2967 {
2968 const double v = json.constLast().toDouble() * maxOpacity;
2969 if ( defaultNumber )
2970 *defaultNumber = v;
2971 elseValue = QString::number( v );
2972 break;
2973 }
2974
2976 {
2977 elseValue = QStringLiteral( "array(%1,%2)" )
2978 .arg( json.constLast().toList().value( 0 ).toDouble() * multiplier )
2979 .arg( json.constLast().toList().value( 0 ).toDouble() * multiplier );
2980 break;
2981 }
2982
2984 {
2985 if ( json.constLast().toList().count() == 2 && json.constLast().toList().first().toString() == QLatin1String( "literal" ) )
2986 {
2987 elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toList().at( 1 ).toStringList().join( ',' ) );
2988 }
2989 else
2990 {
2991 elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toStringList().join( ',' ) );
2992 }
2993 break;
2994 }
2995
2996 }
2997 break;
2998 }
2999 }
3000
3001 caseString += QStringLiteral( "ELSE %1 END" ).arg( elseValue );
3002 return QgsProperty::fromExpression( caseString );
3003}
3004
3005QgsProperty QgsMapBoxGlStyleConverter::parseStepList( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
3006{
3007 const QString expression = parseExpression( json.value( 1 ).toList(), context );
3008 if ( expression.isEmpty() )
3009 {
3010 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3011 return QgsProperty();
3012 }
3013
3014 QString caseString = QStringLiteral( "CASE " );
3015
3016
3017 for ( int i = json.length() - 2; i > 0; i -= 2 )
3018 {
3019 const QVariant stepValue = json.value( i + 1 );
3020
3021 QString valueString;
3022 if ( stepValue.canConvert<QVariantList>()
3023 && ( stepValue.toList().count() != 2 || type != PropertyType::Point )
3024 && type != PropertyType::NumericArray )
3025 {
3026 valueString = parseValueList( stepValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).expressionString();
3027 }
3028 else
3029 {
3030 switch ( type )
3031 {
3033 {
3034 const QColor color = parseColor( stepValue, context );
3035 valueString = QgsExpression::quotedString( color.name() );
3036 break;
3037 }
3038
3040 {
3041 const double v = stepValue.toDouble() * multiplier;
3042 valueString = QString::number( v );
3043 break;
3044 }
3045
3047 {
3048 const double v = stepValue.toDouble() * maxOpacity;
3049 valueString = QString::number( v );
3050 break;
3051 }
3052
3054 {
3055 valueString = QStringLiteral( "array(%1,%2)" ).arg(
3056 stepValue.toList().value( 0 ).toDouble() * multiplier ).arg(
3057 stepValue.toList().value( 0 ).toDouble() * multiplier
3058 );
3059 break;
3060 }
3061
3063 {
3064 if ( stepValue.toList().count() == 2 && stepValue.toList().first().toString() == QLatin1String( "literal" ) )
3065 {
3066 valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toList().at( 1 ).toStringList().join( ',' ) );
3067 }
3068 else
3069 {
3070 valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toStringList().join( ',' ) );
3071 }
3072 break;
3073 }
3074 }
3075 }
3076
3077 if ( i > 1 )
3078 {
3079 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
3080 caseString += QStringLiteral( " WHEN %1 >= %2 THEN (%3) " ).arg( expression, stepKey, valueString );
3081 }
3082 else
3083 {
3084 caseString += QStringLiteral( "ELSE (%1) END" ).arg( valueString );
3085 }
3086 }
3087 return QgsProperty::fromExpression( caseString );
3088}
3089
3090QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateListByZoom( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
3091{
3092 if ( json.value( 0 ).toString() != QLatin1String( "interpolate" ) )
3093 {
3094 context.pushWarning( QObject::tr( "%1: Could not interpret value list" ).arg( context.layerId() ) );
3095 return QgsProperty();
3096 }
3097
3098 double base = 1;
3099 const QString technique = json.value( 1 ).toList().value( 0 ).toString();
3100 if ( technique == QLatin1String( "linear" ) )
3101 base = 1;
3102 else if ( technique == QLatin1String( "exponential" ) )
3103 base = json.value( 1 ).toList(). value( 1 ).toDouble();
3104 else if ( technique == QLatin1String( "cubic-bezier" ) )
3105 {
3106 context.pushWarning( QObject::tr( "%1: Cubic-bezier interpolation is not supported, linear used instead." ).arg( context.layerId() ) );
3107 base = 1;
3108 }
3109 else
3110 {
3111 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation method %2" ).arg( context.layerId(), technique ) );
3112 return QgsProperty();
3113 }
3114
3115 if ( json.value( 2 ).toList().value( 0 ).toString() != QLatin1String( "zoom" ) )
3116 {
3117 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation input %2" ).arg( context.layerId(), json.value( 2 ).toString() ) );
3118 return QgsProperty();
3119 }
3120
3121 // Convert stops into list of lists
3122 QVariantList stops;
3123 for ( int i = 3; i < json.length(); i += 2 )
3124 {
3125 stops.push_back( QVariantList() << json.value( i ).toString() << json.value( i + 1 ) );
3126 }
3127
3128 QVariantMap props;
3129 props.insert( QStringLiteral( "stops" ), stops );
3130 props.insert( QStringLiteral( "base" ), base );
3131 switch ( type )
3132 {
3134 return parseInterpolateColorByZoom( props, context, defaultColor );
3135
3137 return parseInterpolateByZoom( props, context, multiplier, defaultNumber );
3138
3140 return parseInterpolateOpacityByZoom( props, maxOpacity, &context );
3141
3143 return parseInterpolatePointByZoom( props, context, multiplier );
3144
3146 context.pushWarning( QObject::tr( "%1: Skipping unsupported numeric array in interpolate" ).arg( context.layerId() ) );
3147 return QgsProperty();
3148
3149 }
3150 return QgsProperty();
3151}
3152
3154{
3155 if ( ( QMetaType::Type )colorExpression.userType() == QMetaType::QVariantList )
3156 {
3157 return parseExpression( colorExpression.toList(), context, true );
3158 }
3159 return parseValue( colorExpression, context, true );
3160}
3161
3163{
3164 if ( color.userType() != QMetaType::Type::QString )
3165 {
3166 context.pushWarning( QObject::tr( "%1: Could not parse non-string color %2, skipping" ).arg( context.layerId(), color.toString() ) );
3167 return QColor();
3168 }
3169
3170 return QgsSymbolLayerUtils::parseColor( color.toString() );
3171}
3172
3173void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
3174{
3175 hue = std::max( 0, color.hslHue() );
3176 saturation = color.hslSaturation() / 255.0 * 100;
3177 lightness = color.lightness() / 255.0 * 100;
3178 alpha = color.alpha();
3179}
3180
3181QString QgsMapBoxGlStyleConverter::interpolateExpression( double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier, QgsMapBoxGlStyleConversionContext *contextPtr )
3182{
3184 if ( contextPtr )
3185 {
3186 context = *contextPtr;
3187 }
3188
3189 // special case where min = max !
3190 if ( valueMin.canConvert( QMetaType::Double ) && valueMax.canConvert( QMetaType::Double ) )
3191 {
3192 bool minDoubleOk = true;
3193 const double min = valueMin.toDouble( &minDoubleOk );
3194 bool maxDoubleOk = true;
3195 const double max = valueMax.toDouble( &maxDoubleOk );
3196 if ( minDoubleOk && maxDoubleOk && qgsDoubleNear( min, max ) )
3197 {
3198 return QString::number( min * multiplier );
3199 }
3200 }
3201
3202 QString minValueExpr = valueMin.toString();
3203 QString maxValueExpr = valueMax.toString();
3204 if ( valueMin.userType() == QMetaType::Type::QVariantList )
3205 {
3206 minValueExpr = parseExpression( valueMin.toList(), context );
3207 }
3208 if ( valueMax.userType() == QMetaType::Type::QVariantList )
3209 {
3210 maxValueExpr = parseExpression( valueMax.toList(), context );
3211 }
3212
3213 QString expression;
3214 if ( minValueExpr == maxValueExpr )
3215 {
3216 expression = minValueExpr;
3217 }
3218 else
3219 {
3220 if ( base == 1 )
3221 {
3222 expression = QStringLiteral( "scale_linear(@vector_tile_zoom,%1,%2,%3,%4)" ).arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr );
3223 }
3224 else
3225 {
3226 expression = QStringLiteral( "scale_exponential(@vector_tile_zoom,%1,%2,%3,%4,%5)" ).arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr ).arg( base );
3227 }
3228 }
3229
3230 if ( multiplier != 1 )
3231 return QStringLiteral( "(%1) * %2" ).arg( expression ).arg( multiplier );
3232 else
3233 return expression;
3234}
3235
3236Qt::PenCapStyle QgsMapBoxGlStyleConverter::parseCapStyle( const QString &style )
3237{
3238 if ( style == QLatin1String( "round" ) )
3239 return Qt::RoundCap;
3240 else if ( style == QLatin1String( "square" ) )
3241 return Qt::SquareCap;
3242 else
3243 return Qt::FlatCap; // "butt" is default
3244}
3245
3246Qt::PenJoinStyle QgsMapBoxGlStyleConverter::parseJoinStyle( const QString &style )
3247{
3248 if ( style == QLatin1String( "bevel" ) )
3249 return Qt::BevelJoin;
3250 else if ( style == QLatin1String( "round" ) )
3251 return Qt::RoundJoin;
3252 else
3253 return Qt::MiterJoin; // "miter" is default
3254}
3255
3256QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3257{
3258 QString op = expression.value( 0 ).toString();
3259 if ( op == QLatin1String( "%" ) && expression.size() >= 3 )
3260 {
3261 return QStringLiteral( "%1 %2 %3" ).arg( parseValue( expression.value( 1 ), context ),
3262 op,
3263 parseValue( expression.value( 2 ), context ) );
3264 }
3265 else if ( op == QLatin1String( "to-number" ) )
3266 {
3267 return QStringLiteral( "to_real(%1)" ).arg( parseValue( expression.value( 1 ), context ) );
3268 }
3269 if ( op == QLatin1String( "literal" ) )
3270 {
3271 return expression.value( 1 ).toString();
3272 }
3273 else if ( op == QLatin1String( "all" )
3274 || op == QLatin1String( "any" )
3275 || op == QLatin1String( "none" ) )
3276 {
3277 QStringList parts;
3278 for ( int i = 1; i < expression.size(); ++i )
3279 {
3280 const QString part = parseValue( expression.at( i ), context );
3281 if ( part.isEmpty() )
3282 {
3283 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3284 return QString();
3285 }
3286 parts << part;
3287 }
3288
3289 if ( op == QLatin1String( "none" ) )
3290 return QStringLiteral( "NOT (%1)" ).arg( parts.join( QLatin1String( ") AND NOT (" ) ) );
3291
3292 QString operatorString;
3293 if ( op == QLatin1String( "all" ) )
3294 operatorString = QStringLiteral( ") AND (" );
3295 else if ( op == QLatin1String( "any" ) )
3296 operatorString = QStringLiteral( ") OR (" );
3297
3298 return QStringLiteral( "(%1)" ).arg( parts.join( operatorString ) );
3299 }
3300 else if ( op == '!' )
3301 {
3302 // ! inverts next expression's meaning
3303 QVariantList contraJsonExpr = expression.value( 1 ).toList();
3304 contraJsonExpr[0] = QString( op + contraJsonExpr[0].toString() );
3305 // ['!', ['has', 'level']] -> ['!has', 'level']
3306 return parseKey( contraJsonExpr, context );
3307 }
3308 else if ( op == QLatin1String( "==" )
3309 || op == QLatin1String( "!=" )
3310 || op == QLatin1String( ">=" )
3311 || op == '>'
3312 || op == QLatin1String( "<=" )
3313 || op == '<' )
3314 {
3315 // use IS and NOT IS instead of = and != because they can deal with NULL values
3316 if ( op == QLatin1String( "==" ) )
3317 op = QStringLiteral( "IS" );
3318 else if ( op == QLatin1String( "!=" ) )
3319 op = QStringLiteral( "IS NOT" );
3320 return QStringLiteral( "%1 %2 %3" ).arg( parseKey( expression.value( 1 ), context ),
3321 op, parseValue( expression.value( 2 ), context ) );
3322 }
3323 else if ( op == QLatin1String( "has" ) )
3324 {
3325 return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NOT NULL" );
3326 }
3327 else if ( op == QLatin1String( "!has" ) )
3328 {
3329 return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NULL" );
3330 }
3331 else if ( op == QLatin1String( "in" ) || op == QLatin1String( "!in" ) )
3332 {
3333 const QString key = parseKey( expression.value( 1 ), context );
3334 QStringList parts;
3335
3336 QVariantList values = expression.mid( 2 );
3337 if ( expression.size() == 3
3338 && expression.at( 2 ).userType() == QMetaType::Type::QVariantList && expression.at( 2 ).toList().count() > 1
3339 && expression.at( 2 ).toList().at( 0 ).toString() == QStringLiteral( "literal" ) )
3340 {
3341 values = expression.at( 2 ).toList().at( 1 ).toList();
3342 }
3343
3344 for ( const QVariant &value : std::as_const( values ) )
3345 {
3346 const QString part = parseValue( value, context );
3347 if ( part.isEmpty() )
3348 {
3349 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3350 return QString();
3351 }
3352 parts << part;
3353 }
3354 if ( op == QLatin1String( "in" ) )
3355 return QStringLiteral( "%1 IN (%2)" ).arg( key, parts.join( QLatin1String( ", " ) ) );
3356 else
3357 return QStringLiteral( "(%1 IS NULL OR %1 NOT IN (%2))" ).arg( key, parts.join( QLatin1String( ", " ) ) );
3358 }
3359 else if ( op == QLatin1String( "get" ) )
3360 {
3361 return parseKey( expression.value( 1 ), context );
3362 }
3363 else if ( op == QLatin1String( "match" ) )
3364 {
3365 const QString attribute = expression.value( 1 ).toList().value( 1 ).toString();
3366
3367 if ( expression.size() == 5
3368 && expression.at( 3 ).userType() == QMetaType::Type::Bool && expression.at( 3 ).toBool() == true
3369 && expression.at( 4 ).userType() == QMetaType::Type::Bool && expression.at( 4 ).toBool() == false )
3370 {
3371 // simple case, make a nice simple expression instead of a CASE statement
3372 if ( expression.at( 2 ).userType() == QMetaType::Type::QVariantList || expression.at( 2 ).userType() == QMetaType::Type::QStringList )
3373 {
3374 QStringList parts;
3375 for ( const QVariant &p : expression.at( 2 ).toList() )
3376 {
3377 parts << parseValue( p, context );
3378 }
3379
3380 if ( parts.size() > 1 )
3381 return QStringLiteral( "%1 IN (%2)" ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3382 else
3383 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ).toList().value( 0 ) );
3384 }
3385 else if ( expression.at( 2 ).userType() == QMetaType::Type::QString || expression.at( 2 ).userType() == QMetaType::Type::Int
3386 || expression.at( 2 ).userType() == QMetaType::Type::Double || expression.at( 2 ).userType() == QMetaType::Type::LongLong )
3387 {
3388 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ) );
3389 }
3390 else
3391 {
3392 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3393 return QString();
3394 }
3395 }
3396 else
3397 {
3398 QString caseString = QStringLiteral( "CASE " );
3399 for ( int i = 2; i < expression.size() - 2; i += 2 )
3400 {
3401 if ( expression.at( i ).userType() == QMetaType::Type::QVariantList || expression.at( i ).userType() == QMetaType::Type::QStringList )
3402 {
3403 QStringList parts;
3404 for ( const QVariant &p : expression.at( i ).toList() )
3405 {
3406 parts << QgsExpression::quotedValue( p );
3407 }
3408
3409 if ( parts.size() > 1 )
3410 caseString += QStringLiteral( "WHEN %1 IN (%2) " ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3411 else
3412 caseString += QStringLiteral( "WHEN %1 " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ).toList().value( 0 ) ) );
3413 }
3414 else if ( expression.at( i ).userType() == QMetaType::Type::QString || expression.at( i ).userType() == QMetaType::Type::Int
3415 || expression.at( i ).userType() == QMetaType::Type::Double || expression.at( i ).userType() == QMetaType::Type::LongLong )
3416 {
3417 caseString += QStringLiteral( "WHEN (%1) " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ) ) );
3418 }
3419
3420 caseString += QStringLiteral( "THEN %1 " ).arg( parseValue( expression.at( i + 1 ), context, colorExpected ) );
3421 }
3422 caseString += QStringLiteral( "ELSE %1 END" ).arg( parseValue( expression.last(), context, colorExpected ) );
3423 return caseString;
3424 }
3425 }
3426 else if ( op == QLatin1String( "to-string" ) )
3427 {
3428 return QStringLiteral( "to_string(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
3429 }
3430 else if ( op == QLatin1String( "case" ) )
3431 {
3432 QString caseString = QStringLiteral( "CASE" );
3433 for ( int i = 1; i < expression.size() - 2; i += 2 )
3434 {
3435 const QString condition = parseExpression( expression.value( i ).toList(), context );
3436 const QString value = parseValue( expression.value( i + 1 ), context );
3437 caseString += QStringLiteral( " WHEN (%1) THEN %2" ).arg( condition, value );
3438 }
3439 const QString value = parseValue( expression.constLast(), context );
3440 caseString += QStringLiteral( " ELSE %1 END" ).arg( value );
3441 return caseString;
3442 }
3443 else if ( op == QLatin1String( "zoom" ) && expression.count() == 1 )
3444 {
3445 return QStringLiteral( "@vector_tile_zoom" );
3446 }
3447 else if ( op == QLatin1String( "concat" ) )
3448 {
3449 QString concatString = QStringLiteral( "concat(" );
3450 for ( int i = 1; i < expression.size(); i++ )
3451 {
3452 if ( i > 1 )
3453 concatString += QLatin1String( ", " );
3454 concatString += parseValue( expression.value( i ), context );
3455 }
3456 concatString += QLatin1Char( ')' );
3457 return concatString;
3458 }
3459 else if ( op == QLatin1String( "length" ) )
3460 {
3461 return QStringLiteral( "length(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
3462 }
3463 else if ( op == QLatin1String( "step" ) )
3464 {
3465 const QString stepExpression = parseExpression( expression.value( 1 ).toList(), context );
3466 if ( stepExpression.isEmpty() )
3467 {
3468 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3469 return QString();
3470 }
3471
3472 QString caseString = QStringLiteral( "CASE " );
3473
3474 for ( int i = expression.length() - 2; i > 0; i -= 2 )
3475 {
3476 const QString stepValue = parseValue( expression.value( i + 1 ), context, colorExpected );
3477 if ( i > 1 )
3478 {
3479 const QString stepKey = QgsExpression::quotedValue( expression.value( i ) );
3480 caseString += QStringLiteral( " WHEN %1 >= %2 THEN (%3) " ).arg( stepExpression, stepKey, stepValue );
3481 }
3482 else
3483 {
3484 caseString += QStringLiteral( "ELSE (%1) END" ).arg( stepValue );
3485 }
3486 }
3487 return caseString;
3488 }
3489 else
3490 {
3491 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression \"%2\"" ).arg( context.layerId(), op ) );
3492 return QString();
3493 }
3494}
3495
3496QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
3497{
3498 if ( context.spriteImage().isNull() )
3499 {
3500 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3501 return QImage();
3502 }
3503
3504 const QVariantMap spriteDefinition = context.spriteDefinitions().value( name ).toMap();
3505 if ( spriteDefinition.size() == 0 )
3506 {
3507 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3508 return QImage();
3509 }
3510
3511 const QImage sprite = context.spriteImage().copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
3512 spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
3513 spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
3514 spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
3515 if ( sprite.isNull() )
3516 {
3517 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3518 return QImage();
3519 }
3520
3521 spriteSize = sprite.size() / spriteDefinition.value( QStringLiteral( "pixelRatio" ) ).toDouble() * context.pixelSizeConversionFactor();
3522 return sprite;
3523}
3524
3525QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64WithProperties( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty )
3526{
3527 QString spritePath;
3528
3529 auto prepareBase64 = []( const QImage & sprite )
3530 {
3531 QString path;
3532 if ( !sprite.isNull() )
3533 {
3534 QByteArray blob;
3535 QBuffer buffer( &blob );
3536 buffer.open( QIODevice::WriteOnly );
3537 sprite.save( &buffer, "PNG" );
3538 buffer.close();
3539 const QByteArray encoded = blob.toBase64();
3540 path = QString( encoded );
3541 path.prepend( QLatin1String( "base64:" ) );
3542 }
3543 return path;
3544 };
3545
3546 switch ( value.userType() )
3547 {
3548 case QMetaType::Type::QString:
3549 {
3550 QString spriteName = value.toString();
3551 const thread_local QRegularExpression fieldNameMatch( QStringLiteral( "{([^}]+)}" ) );
3552 QRegularExpressionMatch match = fieldNameMatch.match( spriteName );
3553 if ( match.hasMatch() )
3554 {
3555 const QString fieldName = match.captured( 1 );
3556 spriteProperty = QStringLiteral( "CASE" );
3557 spriteSizeProperty = QStringLiteral( "CASE" );
3558
3559 spriteName.replace( "(", QLatin1String( "\\(" ) );
3560 spriteName.replace( ")", QLatin1String( "\\)" ) );
3561 spriteName.replace( fieldNameMatch, QStringLiteral( "([^\\/\\\\]+)" ) );
3562 const QRegularExpression fieldValueMatch( spriteName );
3563 const QStringList spriteNames = context.spriteDefinitions().keys();
3564 for ( const QString &name : spriteNames )
3565 {
3566 match = fieldValueMatch.match( name );
3567 if ( match.hasMatch() )
3568 {
3569 QSize size;
3570 QString path;
3571 const QString fieldValue = match.captured( 1 );
3572 const QImage sprite = retrieveSprite( name, context, size );
3573 path = prepareBase64( sprite );
3574 if ( spritePath.isEmpty() && !path.isEmpty() )
3575 {
3576 spritePath = path;
3577 spriteSize = size;
3578 }
3579
3580 spriteProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN '%3'" )
3581 .arg( fieldName, fieldValue, path );
3582 spriteSizeProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN %3" )
3583 .arg( fieldName ).arg( fieldValue ).arg( size.width() );
3584 }
3585 }
3586
3587 spriteProperty += QLatin1String( " END" );
3588 spriteSizeProperty += QLatin1String( " END" );
3589 }
3590 else
3591 {
3592 spriteProperty.clear();
3593 spriteSizeProperty.clear();
3594 const QImage sprite = retrieveSprite( spriteName, context, spriteSize );
3595 spritePath = prepareBase64( sprite );
3596 }
3597 break;
3598 }
3599
3600 case QMetaType::Type::QVariantMap:
3601 {
3602 const QVariantList stops = value.toMap().value( QStringLiteral( "stops" ) ).toList();
3603 if ( stops.size() == 0 )
3604 break;
3605
3606 QString path;
3607 QSize size;
3608 QImage sprite;
3609
3610 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
3611 spritePath = prepareBase64( sprite );
3612
3613 spriteProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN '%2'" )
3614 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3615 .arg( spritePath );
3616 spriteSizeProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN %2" )
3617 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3618 .arg( spriteSize.width() );
3619
3620 for ( int i = 0; i < stops.size() - 1; ++i )
3621 {
3622 ;
3623 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
3624 path = prepareBase64( sprite );
3625
3626 spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3627 "THEN '%3'" )
3628 .arg( stops.value( i ).toList().value( 0 ).toString(),
3629 stops.value( i + 1 ).toList().value( 0 ).toString(),
3630 path );
3631 spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3632 "THEN %3" )
3633 .arg( stops.value( i ).toList().value( 0 ).toString(),
3634 stops.value( i + 1 ).toList().value( 0 ).toString() )
3635 .arg( size.width() );
3636 }
3637 sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
3638 path = prepareBase64( sprite );
3639
3640 spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3641 "THEN '%2' END" )
3642 .arg( stops.last().toList().value( 0 ).toString() )
3643 .arg( path );
3644 spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3645 "THEN %2 END" )
3646 .arg( stops.last().toList().value( 0 ).toString() )
3647 .arg( size.width() );
3648 break;
3649 }
3650
3651 case QMetaType::Type::QVariantList:
3652 {
3653 const QVariantList json = value.toList();
3654 const QString method = json.value( 0 ).toString();
3655
3656 if ( method == QLatin1String( "match" ) )
3657 {
3658 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
3659 if ( attribute.isEmpty() )
3660 {
3661 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
3662 break;
3663 }
3664
3665 spriteProperty = QStringLiteral( "CASE" );
3666 spriteSizeProperty = QStringLiteral( "CASE" );
3667
3668 for ( int i = 2; i < json.length() - 1; i += 2 )
3669 {
3670 const QVariant matchKey = json.value( i );
3671 const QVariant matchValue = json.value( i + 1 );
3672 QString matchString;
3673 switch ( matchKey.userType() )
3674 {
3675 case QMetaType::Type::QVariantList:
3676 case QMetaType::Type::QStringList:
3677 {
3678 const QVariantList keys = matchKey.toList();
3679 QStringList matchStringList;
3680 for ( const QVariant &key : keys )
3681 {
3682 matchStringList << QgsExpression::quotedValue( key );
3683 }
3684 matchString = matchStringList.join( ',' );
3685 break;
3686 }
3687
3688 case QMetaType::Type::Bool:
3689 case QMetaType::Type::QString:
3690 case QMetaType::Type::Int:
3691 case QMetaType::Type::LongLong:
3692 case QMetaType::Type::Double:
3693 {
3694 matchString = QgsExpression::quotedValue( matchKey );
3695 break;
3696 }
3697
3698 default:
3699 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
3700 break;
3701
3702 }
3703
3704 const QImage sprite = retrieveSprite( matchValue.toString(), context, spriteSize );
3705 spritePath = prepareBase64( sprite );
3706
3707 spriteProperty += QStringLiteral( " WHEN %1 IN (%2) "
3708 "THEN '%3'" ).arg( attribute,
3709 matchString,
3710 spritePath );
3711
3712 spriteSizeProperty += QStringLiteral( " WHEN %1 IN (%2) "
3713 "THEN %3" ).arg( attribute,
3714 matchString ).arg( spriteSize.width() );
3715 }
3716
3717 if ( !json.constLast().toString().isEmpty() )
3718 {
3719 const QImage sprite = retrieveSprite( json.constLast().toString(), context, spriteSize );
3720 spritePath = prepareBase64( sprite );
3721 }
3722 else
3723 {
3724 spritePath = QString();
3725 }
3726
3727 spriteProperty += QStringLiteral( " ELSE '%1' END" ).arg( spritePath );
3728 spriteSizeProperty += QStringLiteral( " ELSE %3 END" ).arg( spriteSize.width() );
3729 break;
3730 }
3731 else if ( method == QLatin1String( "step" ) )
3732 {
3733 const QString expression = parseExpression( json.value( 1 ).toList(), context );
3734 if ( expression.isEmpty() )
3735 {
3736 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3737 break;
3738 }
3739
3740 spriteProperty = QStringLiteral( "CASE" );
3741 spriteSizeProperty = QStringLiteral( "CASE" );
3742 for ( int i = json.length() - 2; i > 2; i -= 2 )
3743 {
3744 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
3745 const QString stepValue = json.value( i + 1 ).toString();
3746
3747 const QImage sprite = retrieveSprite( stepValue, context, spriteSize );
3748 spritePath = prepareBase64( sprite );
3749
3750 spriteProperty += QStringLiteral( " WHEN %1 >= %2 THEN '%3' " ).arg( expression, stepKey, spritePath );
3751 spriteSizeProperty += QStringLiteral( " WHEN %1 >= %2 THEN %3 " ).arg( expression ).arg( stepKey ).arg( spriteSize.width() );
3752 }
3753
3754 const QImage sprite = retrieveSprite( json.at( 2 ).toString(), context, spriteSize );
3755 spritePath = prepareBase64( sprite );
3756
3757 spriteProperty += QStringLiteral( "ELSE '%1' END" ).arg( spritePath );
3758 spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3759 break;
3760 }
3761 else if ( method == QLatin1String( "case" ) )
3762 {
3763 spriteProperty = QStringLiteral( "CASE" );
3764 spriteSizeProperty = QStringLiteral( "CASE" );
3765 for ( int i = 1; i < json.length() - 2; i += 2 )
3766 {
3767 const QString caseExpression = parseExpression( json.value( i ).toList(), context );
3768 const QString caseValue = json.value( i + 1 ).toString();
3769
3770 const QImage sprite = retrieveSprite( caseValue, context, spriteSize );
3771 spritePath = prepareBase64( sprite );
3772
3773 spriteProperty += QStringLiteral( " WHEN %1 THEN '%2' " ).arg( caseExpression, spritePath );
3774 spriteSizeProperty += QStringLiteral( " WHEN %1 THEN %2 " ).arg( caseExpression ).arg( spriteSize.width() );
3775 }
3776 const QImage sprite = retrieveSprite( json.last().toString(), context, spriteSize );
3777 spritePath = prepareBase64( sprite );
3778
3779 spriteProperty += QStringLiteral( "ELSE '%1' END" ).arg( spritePath );
3780 spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3781 break;
3782 }
3783 else
3784 {
3785 context.pushWarning( QObject::tr( "%1: Could not interpret sprite value list with method %2" ).arg( context.layerId(), method ) );
3786 break;
3787 }
3788 }
3789
3790 default:
3791 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
3792 break;
3793 }
3794
3795 return spritePath;
3796}
3797
3798QString QgsMapBoxGlStyleConverter::parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3799{
3800 QColor c;
3801 switch ( value.userType() )
3802 {
3803 case QMetaType::Type::QVariantList:
3804 case QMetaType::Type::QStringList:
3805 return parseExpression( value.toList(), context, colorExpected );
3806
3807 case QMetaType::Type::Bool:
3808 case QMetaType::Type::QString:
3809 if ( colorExpected )
3810 {
3811 QColor c = parseColor( value, context );
3812 if ( c.isValid() )
3813 {
3814 return parseValue( c, context );
3815 }
3816 }
3817 return QgsExpression::quotedValue( value );
3818
3819 case QMetaType::Type::Int:
3820 case QMetaType::Type::LongLong:
3821 case QMetaType::Type::Double:
3822 return value.toString();
3823
3824 case QMetaType::Type::QColor:
3825 c = value.value<QColor>();
3826 return QString( "color_rgba(%1,%2,%3,%4)" ).arg( c.red() ).arg( c.green() ).arg( c.blue() ).arg( c.alpha() );
3827
3828 default:
3829 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression part" ).arg( context.layerId() ) );
3830 break;
3831 }
3832 return QString();
3833}
3834
3835QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context )
3836{
3837 if ( value.toString() == QLatin1String( "$type" ) )
3838 {
3839 return QStringLiteral( "_geom_type" );
3840 }
3841 if ( value.toString() == QLatin1String( "level" ) )
3842 {
3843 return QStringLiteral( "level" );
3844 }
3845 else if ( ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() == 1 ) || value.userType() == QMetaType::Type::QStringList )
3846 {
3847 if ( value.toList().size() > 1 )
3848 return value.toList().at( 1 ).toString();
3849 else
3850 {
3851 QString valueString = value.toList().value( 0 ).toString();
3852 if ( valueString == QLatin1String( "geometry-type" ) )
3853 {
3854 return QStringLiteral( "_geom_type" );
3855 }
3856 return valueString;
3857 }
3858 }
3859 else if ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() > 1 )
3860 {
3861 return parseExpression( value.toList(), context );
3862 }
3863 return QgsExpression::quotedColumnRef( value.toString() );
3864}
3865
3866QString QgsMapBoxGlStyleConverter::processLabelField( const QString &string, bool &isExpression )
3867{
3868 // {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
3869 // but if single field is covered in {}, return it directly
3870 const thread_local QRegularExpression singleFieldRx( QStringLiteral( "^{([^}]+)}$" ) );
3871 const QRegularExpressionMatch match = singleFieldRx.match( string );
3872 if ( match.hasMatch() )
3873 {
3874 isExpression = false;
3875 return match.captured( 1 );
3876 }
3877
3878 const thread_local QRegularExpression multiFieldRx( QStringLiteral( "(?={[^}]+})" ) );
3879 const QStringList parts = string.split( multiFieldRx );
3880 if ( parts.size() > 1 )
3881 {
3882 isExpression = true;
3883
3884 QStringList res;
3885 for ( const QString &part : parts )
3886 {
3887 if ( part.isEmpty() )
3888 continue;
3889
3890 if ( !part.contains( '{' ) )
3891 {
3892 res << QgsExpression::quotedValue( part );
3893 continue;
3894 }
3895
3896 // part will start at a {field} reference
3897 const QStringList split = part.split( '}' );
3898 res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
3899 if ( !split.at( 1 ).isEmpty() )
3900 res << QgsExpression::quotedValue( split.at( 1 ) );
3901 }
3902 return QStringLiteral( "concat(%1)" ).arg( res.join( ',' ) );
3903 }
3904 else
3905 {
3906 isExpression = false;
3907 return string;
3908 }
3909}
3910
3912{
3913 return mRenderer ? mRenderer->clone() : nullptr;
3914}
3915
3917{
3918 return mLabeling ? mLabeling->clone() : nullptr;
3919}
3920
3921QList<QgsMapBoxGlStyleAbstractSource *> QgsMapBoxGlStyleConverter::sources()
3922{
3923 return mSources;
3924}
3925
3926QList<QgsMapBoxGlStyleRasterSubLayer> QgsMapBoxGlStyleConverter::rasterSubLayers() const
3927{
3928 return mRasterSubLayers;
3929}
3930
3932{
3933 QList<QgsMapLayer *> subLayers;
3934 for ( const QgsMapBoxGlStyleRasterSubLayer &subLayer : mRasterSubLayers )
3935 {
3936 const QString sourceName = subLayer.source();
3937 std::unique_ptr< QgsRasterLayer > rl;
3938 for ( const QgsMapBoxGlStyleAbstractSource *source : mSources )
3939 {
3940 if ( source->type() == Qgis::MapBoxGlStyleSourceType::Raster && source->name() == sourceName )
3941 {
3942 const QgsMapBoxGlStyleRasterSource *rasterSource = qgis::down_cast< const QgsMapBoxGlStyleRasterSource * >( source );
3943 rl.reset( rasterSource->toRasterLayer() );
3944 rl->pipe()->setDataDefinedProperties( subLayer.dataDefinedProperties() );
3945 break;
3946 }
3947 }
3948
3949 if ( rl )
3950 {
3951 subLayers.append( rl.release() );
3952 }
3953 }
3954 return subLayers;
3955}
3956
3957
3959{
3960 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
3961 if ( !context )
3962 {
3963 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
3964 context = tmpContext.get();
3965 }
3966
3967 auto typeFromString = [context]( const QString & string, const QString & name )->Qgis::MapBoxGlStyleSourceType
3968 {
3969 if ( string.compare( QLatin1String( "vector" ), Qt::CaseInsensitive ) == 0 )
3971 else if ( string.compare( QLatin1String( "raster" ), Qt::CaseInsensitive ) == 0 )
3973 else if ( string.compare( QLatin1String( "raster-dem" ), Qt::CaseInsensitive ) == 0 )
3975 else if ( string.compare( QLatin1String( "geojson" ), Qt::CaseInsensitive ) == 0 )
3977 else if ( string.compare( QLatin1String( "image" ), Qt::CaseInsensitive ) == 0 )
3979 else if ( string.compare( QLatin1String( "video" ), Qt::CaseInsensitive ) == 0 )
3981 context->pushWarning( QObject::tr( "Invalid source type \"%1\" for source \"%2\"" ).arg( string, name ) );
3983 };
3984
3985 for ( auto it = sources.begin(); it != sources.end(); ++it )
3986 {
3987 const QString name = it.key();
3988 const QVariantMap jsonSource = it.value().toMap();
3989 const QString typeString = jsonSource.value( QStringLiteral( "type" ) ).toString();
3990
3991 const Qgis::MapBoxGlStyleSourceType type = typeFromString( typeString, name );
3992
3993 switch ( type )
3994 {
3996 parseRasterSource( jsonSource, name, context );
3997 break;
4004 QgsDebugError( QStringLiteral( "Ignoring vector tile style source %1 (%2)" ).arg( name, qgsEnumValueToKey( type ) ) );
4005 continue;
4006 }
4007 }
4008}
4009
4010void QgsMapBoxGlStyleConverter::parseRasterSource( const QVariantMap &source, const QString &name, QgsMapBoxGlStyleConversionContext *context )
4011{
4012 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
4013 if ( !context )
4014 {
4015 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
4016 context = tmpContext.get();
4017 }
4018
4019 std::unique_ptr< QgsMapBoxGlStyleRasterSource > raster = std::make_unique< QgsMapBoxGlStyleRasterSource >( name );
4020 if ( raster->setFromJson( source, context ) )
4021 mSources.append( raster.release() );
4022}
4023
4024bool QgsMapBoxGlStyleConverter::numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top )
4025{
4026 if ( bottomVariant.canConvert( QMetaType::Double ) && topVariant.canConvert( QMetaType::Double ) )
4027 {
4028 bool bDoubleOk, tDoubleOk;
4029 bottom = bottomVariant.toDouble( &bDoubleOk );
4030 top = topVariant.toDouble( &tDoubleOk );
4031 return ( bDoubleOk && tDoubleOk );
4032 }
4033 return false;
4034}
4035
4036//
4037// QgsMapBoxGlStyleConversionContext
4038//
4040{
4041 QgsDebugError( warning );
4042 mWarnings << warning;
4043}
4044
4046{
4047 return mTargetUnit;
4048}
4049
4051{
4052 mTargetUnit = targetUnit;
4053}
4054
4056{
4057 return mSizeConversionFactor;
4058}
4059
4061{
4062 mSizeConversionFactor = sizeConversionFactor;
4063}
4064
4066{
4067 return mSpriteImage;
4068}
4069
4071{
4072 return mSpriteDefinitions;
4073}
4074
4075void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions )
4076{
4077 mSpriteImage = image;
4078 mSpriteDefinitions = definitions;
4079}
4080
4081void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions )
4082{
4083 setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap() );
4084}
4085
4087{
4088 return mLayerId;
4089}
4090
4092{
4093 mLayerId = value;
4094}
4095
4096//
4097// QgsMapBoxGlStyleAbstractSource
4098//
4100 : mName( name )
4101{
4102}
4103
4105{
4106 return mName;
4107}
4108
4110
4111//
4112// QgsMapBoxGlStyleRasterSource
4113//
4114
4120
4125
4127{
4128 mAttribution = json.value( QStringLiteral( "attribution" ) ).toString();
4129
4130 const QString scheme = json.value( QStringLiteral( "scheme" ), QStringLiteral( "xyz" ) ).toString();
4131 if ( scheme.compare( QLatin1String( "xyz" ) ) == 0 )
4132 {
4133 // xyz scheme is supported
4134 }
4135 else
4136 {
4137 context->pushWarning( QObject::tr( "%1 scheme is not supported for raster source %2" ).arg( scheme, name() ) );
4138 return false;
4139 }
4140
4141 mMinZoom = json.value( QStringLiteral( "minzoom" ), QStringLiteral( "0" ) ).toInt();
4142 mMaxZoom = json.value( QStringLiteral( "maxzoom" ), QStringLiteral( "22" ) ).toInt();
4143 mTileSize = json.value( QStringLiteral( "tileSize" ), QStringLiteral( "512" ) ).toInt();
4144
4145 const QVariantList tiles = json.value( QStringLiteral( "tiles" ) ).toList();
4146 for ( const QVariant &tile : tiles )
4147 {
4148 mTiles.append( tile.toString() );
4149 }
4150
4151 return true;
4152}
4153
4155{
4156 QVariantMap parts;
4157 parts.insert( QStringLiteral( "type" ), QStringLiteral( "xyz" ) );
4158 parts.insert( QStringLiteral( "url" ), mTiles.value( 0 ) );
4159
4160 if ( mTileSize == 256 )
4161 parts.insert( QStringLiteral( "tilePixelRation" ), QStringLiteral( "1" ) );
4162 else if ( mTileSize == 512 )
4163 parts.insert( QStringLiteral( "tilePixelRation" ), QStringLiteral( "2" ) );
4164
4165 parts.insert( QStringLiteral( "zmax" ), QString::number( mMaxZoom ) );
4166 parts.insert( QStringLiteral( "zmin" ), QString::number( mMinZoom ) );
4167
4168 std::unique_ptr< QgsRasterLayer > rl = std::make_unique< QgsRasterLayer >( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "wms" ), parts ), name(), QStringLiteral( "wms" ) );
4169 return rl.release();
4170}
4171
4172//
4173// QgsMapBoxGlStyleRasterSubLayer
4174//
4176 : mId( id )
4177 , mSource( source )
4178{
4179
4180}
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
@ OnLine
Labels can be placed directly over a line feature.
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
@ CentralPoint
Place symbols at the mid point of the line.
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
@ Curved
Arranges candidates following the curvature of a line feature. Applies to line layers only.
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature. Applies to polygon layers only...
GeometryType
The geometry types are used to group Qgis::WkbType in a coarse way.
Definition qgis.h:337
@ Polygon
Polygons.
@ FollowPlacement
Alignment follows placement of label, e.g., labels to the left of a feature will be drawn with right ...
RenderUnit
Rendering size units.
Definition qgis.h:4819
MapBoxGlStyleSourceType
Available MapBox GL style source types.
Definition qgis.h:4028
@ RasterDem
Raster DEM source.
@ Unknown
Other/unknown source type.
@ Viewport
Relative to the whole viewport/output device.
void setPenJoinStyle(Qt::PenJoinStyle style)
Sets the pen join style used to render the line (e.g.
void setPenCapStyle(Qt::PenCapStyle style)
Sets the pen cap style used to render the line (e.g.
static QgsFontManager * fontManager()
Returns the application font manager, which manages available fonts and font installation for the QGI...
A paint effect which blurs a source picture, using a number of different blur methods.
void setBlurUnit(const Qgis::RenderUnit unit)
Sets the units used for the blur level (radius).
@ StackBlur
Stack blur, a fast but low quality blur. Valid blur level values are between 0 - 16.
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)
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, QMetaType::Type fieldType=QMetaType::Type::UnknownType)
Create an expression allowing to evaluate if a field is equal to a value.
static QString quotedColumnRef(QString name)
Returns a quoted column reference (in double quotes)
QString processFontFamilyName(const QString &name) const
Processes a font family name, applying any matching fontFamilyReplacements() to the name.
static QFont createFont(const QString &family, int pointSize=-1, int weight=-1, bool italic=false)
Creates a font with the specified family.
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
void setPlacementFlags(Qgis::LabelLinePlacementFlags 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,...
void setQuadrant(Qgis::LabelQuadrantPosition quadrant)
Sets the quadrant in which to offset labels from the point.
virtual void setWidth(double width)
Sets the width of the line symbol layer.
void setOffset(double offset)
Sets the line's offset.
void setOffsetUnit(Qgis::RenderUnit unit)
Sets the unit for the line's offset.
Abstract base class for MapBox GL style sources.
QString name() const
Returns the source's name.
QgsMapBoxGlStyleAbstractSource(const QString &name)
Constructor for QgsMapBoxGlStyleAbstractSource.
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...
void setTargetUnit(Qgis::RenderUnit targetUnit)
Sets the target unit type.
void setPixelSizeConversionFactor(double sizeConversionFactor)
Sets the pixel size conversion factor, used to scale the original pixel sizes when converting styles.
Qgis::RenderUnit targetUnit() const
Returns the target unit type.
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.
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)
@ NumericArray
Numeric array for dash arrays or such.
QList< QgsMapBoxGlStyleAbstractSource * > sources()
Returns the list of converted sources.
QgsVectorTileLabeling * labeling() const
Returns a new instance of a vector tile labeling representing the converted style,...
QList< QgsMapBoxGlStyleRasterSubLayer > rasterSubLayers() const
Returns a list of raster sub layers contained in the 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 parseStepList(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 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.
QList< QgsMapLayer * > createSubLayers() const
Returns a list of new map layers corresponding to sublayers of the style, e.g.
@ Success
Conversion was successful.
@ NoLayerList
No layer list was found in JSON input.
QgsMapBoxGlStyleConverter()
Constructor for QgsMapBoxGlStyleConverter.
static QImage retrieveSprite(const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize)
Retrieves the sprite image with the specified name, taken from the specified context.
static QString parseLabelStops(const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context)
Parses a list of interpolation stops containing label values.
void parseLayers(const QVariantList &layers, QgsMapBoxGlStyleConversionContext *context=nullptr)
Parse list of layers from JSON.
static QgsProperty parseInterpolateColorByZoom(const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, QColor *defaultColor=nullptr)
Parses a color value which is interpolated by zoom range.
static QString retrieveSpriteAsBase64WithProperties(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 QgsProperty parseInterpolateOpacityByZoom(const QVariantMap &json, int maxOpacity, QgsMapBoxGlStyleConversionContext *contextPtr=0)
Interpolates opacity with either scale_linear() or scale_exp() (depending on base value).
void parseSources(const QVariantMap &sources, QgsMapBoxGlStyleConversionContext *context=nullptr)
Parse list of sources from JSON.
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.
void parseRasterSource(const QVariantMap &source, const QString &name, QgsMapBoxGlStyleConversionContext *context=nullptr)
Parse a raster source from JSON.
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/...
Encapsulates a MapBox GL style raster source.
Qgis::MapBoxGlStyleSourceType type() const override
Returns the source type.
QgsMapBoxGlStyleRasterSource(const QString &name)
Constructor for QgsMapBoxGlStyleRasterSource.
QgsRasterLayer * toRasterLayer() const
Returns a new raster layer representing the raster source, or nullptr if the source cannot be represe...
bool setFromJson(const QVariantMap &json, QgsMapBoxGlStyleConversionContext *context) override
Sets the source's state from a json map.
QStringList tiles() const
Returns the list of tile sources.
Encapsulates a MapBox GL style raster sub layer.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the layer's data defined properties.
QgsMapBoxGlStyleRasterSubLayer(const QString &id, const QString &source)
Constructor for QgsMapBoxGlStyleRasterSubLayer, with the given id and source.
Line symbol layer type which draws repeating marker symbols along a line feature.
void setOutputUnit(Qgis::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
bool setSubSymbol(QgsSymbol *symbol) override
Sets layer's subsymbol. takes ownership of the passed symbol.
virtual void setSize(double size)
Sets the symbol size.
void setOffsetUnit(Qgis::RenderUnit unit)
Sets the units for the symbol's offset.
void setAngle(double angle)
Sets the rotation angle for the marker.
void setOffset(QPointF offset)
Sets the marker's offset, which is the horizontal and vertical displacement which the rendered marker...
void setSizeUnit(Qgis::RenderUnit unit)
Sets the units for the symbol's size.
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.
const QgsLabelObstacleSettings & obstacleSettings() const
Returns the label obstacle settings.
void setFormat(const QgsTextFormat &format)
Sets the label text formatting settings, e.g., font settings, buffer settings, etc.
double xOffset
Horizontal offset of label.
Qgis::LabelPlacement placement
Label placement mode.
Qgis::LabelMultiLineAlignment multilineAlign
Horizontal alignment of multi-line labels.
int priority
Label priority.
double angleOffset
Label rotation, in degrees clockwise.
Qgis::RenderUnit offsetUnits
Units for offsets of label.
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'
const QgsLabelLineSettings & lineSettings() const
Returns the label line settings, which contain settings related to how the label engine places and fo...
double dist
Distance from feature to the label.
Qgis::RenderUnit distUnits
Units the distance from feature to the label.
@ LinePlacementOptions
Line placement flags.
@ LabelRotation
Label rotation.
@ FontStyle
Font style name.
@ FontLetterSpacing
Letter spacing.
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...
const QgsLabelPointSettings & pointSettings() const
Returns the label point settings, which contain settings related to how the label engine places and f...
A grouped map of multiple QgsProperty objects, each referenced by a integer key value.
QVariant value(int key, const QgsExpressionContext &context, const QVariant &defaultValue=QVariant()) const final
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 final
Returns true if the collection contains an active property with the specified key.
QgsProperty property(int key) const final
Returns a matching property from the collection, if one exists.
A store for object properties.
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.
void setExpressionString(const QString &expression)
Sets the expression to use for the property value.
static QgsProviderRegistry * instance(const QString &pluginPath=QString())
Means of accessing canonical single instance.
A class for filling symbols with a repeated raster image.
void setSizeUnit(Qgis::RenderUnit unit)
Sets the unit for the image's width and height.
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 setWidth(double width)
Sets the width for scaling the image used in the fill.
void setCoordinateMode(Qgis::SymbolCoordinateReference mode)
Set the coordinate mode for fill.
Represents a raster layer.
Line symbol layer type which draws line sections using a raster image file.
void setOutputUnit(Qgis::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
Raster marker symbol layer class.
void setOpacity(double opacity)
Set the marker opacity.
void setPath(const QString &path)
Set the marker raster image path.
@ RendererOpacity
Raster renderer global opacity.
Renders polygons using a single fill and stroke color.
void setBrushStyle(Qt::BrushStyle style)
void setOutputUnit(Qgis::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
void setStrokeWidth(double strokeWidth)
void setStrokeStyle(Qt::PenStyle strokeStyle)
void setOffsetUnit(Qgis::RenderUnit unit)
Sets the unit for the fill's offset.
void setFillColor(const QColor &color) override
Sets the fill color for the symbol layer.
void setOffset(QPointF offset)
Sets an offset by which polygons will be translated during rendering.
void setStrokeColor(const QColor &strokeColor) override
Sets the stroke color for the symbol layer.
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 setOutputUnit(Qgis::RenderUnit unit) override
Sets the units to use for sizes and widths within the symbol layer.
void setPenJoinStyle(Qt::PenJoinStyle style)
Sets the pen join style used to render the line (e.g.
Simple marker symbol layer, consisting of a rendered shape with solid fill color and an stroke.
void setFillColor(const QColor &color) override
Sets the fill color for the symbol layer.
void setStrokeWidthUnit(Qgis::RenderUnit u)
Sets the unit for the width of the marker's stroke.
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,...
@ File
Filename, eg for svg files.
@ CustomDash
Custom dash pattern.
@ Name
Name, eg shape name for simple markers.
@ StrokeColor
Stroke color.
@ Interval
Line marker interval.
@ StrokeWidth
Stroke width.
@ Offset
Symbol offset.
@ LayerEnabled
Whether symbol layer is enabled.
virtual void setColor(const QColor &color)
Sets the "representative" color for the symbol layer.
void setDataDefinedProperties(const QgsPropertyCollection &collection)
Sets the symbol layer's property collection, used for data defined overrides.
void setPlacements(Qgis::MarkerLinePlacements placements)
Sets the placement of the symbols.
Container for settings relating to a text background object.
void setMarkerSymbol(QgsMarkerSymbol *symbol)
Sets the current marker symbol for the background shape.
void setSizeType(SizeType type)
Sets the method used to determine the size of the background shape (e.g., fixed size or buffer around...
void setSizeUnit(Qgis::RenderUnit unit)
Sets the units used for the shape's size.
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 setColor(const QColor &color)
Sets the color for the buffer.
void setOpacity(double opacity)
Sets the buffer opacity.
void setSizeUnit(Qgis::RenderUnit unit)
Sets the units used for the buffer size.
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 setSize(double size)
Sets the size of the buffer.
Container for all settings relating to text rendering.
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(Qgis::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.
Configuration of a single style within QgsVectorTileBasicLabeling.
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 setGeometryType(Qgis::GeometryType geomType)
Sets type of the geometry that will be used (point / line / polygon)
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 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).
void setGeometryType(Qgis::GeometryType geomType)
Sets type of the geometry that will be used (point / line / polygon)
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.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition qgis.h:5800
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:6074
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5883
#define QgsDebugError(str)
Definition qgslogger.h:38
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30