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