QGIS API Documentation 4.3.0-Master (6313fb5bce0)
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.empty() )
722 {
723 if ( dashSource.at( 0 ).userType() == QMetaType::Type::QString )
724 {
725 QgsProperty property = parseValueList( dashSource, PropertyType::DashArray, context, 1, 255, nullptr, nullptr );
726 if ( !lineWidthProperty.asExpression().isEmpty() )
727 {
729 u"array_to_string(array_foreach(%1,@element * (%2)), ';')"_s // skip-keyword-check
730 .arg( property.asExpression(), lineWidthProperty.asExpression() )
731 );
732 }
733 else
734 {
735 property = QgsProperty::fromExpression( u"array_to_string(%1, ';')"_s.arg( property.asExpression() ) );
736 }
737 ddProperties.setProperty( QgsSymbolLayer::Property::CustomDash, property );
738 }
739 else
740 {
741 QVector< double > rawDashVectorSizes;
742 rawDashVectorSizes.reserve( dashSource.size() );
743 for ( const QVariant &v : dashSource )
744 {
745 rawDashVectorSizes << v.toDouble();
746 }
747
748 // handle non-compliant dash vector patterns
749 if ( rawDashVectorSizes.size() == 1 )
750 {
751 // match behavior of MapBox style rendering -- if a user makes a line dash array with one element, it's ignored
752 rawDashVectorSizes.clear();
753 }
754 else if ( rawDashVectorSizes.size() % 2 == 1 )
755 {
756 // odd number of dash pattern sizes -- this isn't permitted by Qt/QGIS, but isn't explicitly blocked by the MapBox specs
757 // MapBox seems to implicitly add a 0 length gap to the array if odd length.
758 rawDashVectorSizes.append( 0 );
759 }
760
761 if ( !rawDashVectorSizes.isEmpty() && ( !lineWidthProperty.asExpression().isEmpty() ) )
762 {
763 QStringList dashArrayStringParts;
764 dashArrayStringParts.reserve( rawDashVectorSizes.size() );
765 for ( double v : std::as_const( rawDashVectorSizes ) )
766 {
767 dashArrayStringParts << qgsDoubleToString( v );
768 }
769
770 QString arrayExpression = u"array_to_string(array_foreach(array(%1),@element * (%2)), ';')"_s // skip-keyword-check
771 .arg( dashArrayStringParts.join( ',' ), lineWidthProperty.asExpression() );
773 }
774
775 // dash vector sizes for QGIS symbols must be multiplied by the target line width
776 for ( double v : std::as_const( rawDashVectorSizes ) )
777 {
778 dashVector << v * lineWidth;
779 }
780 }
781 }
782 break;
783 }
784
785 default:
786 context.pushWarning(
787 QObject::tr( "%1: Skipping unsupported line-dasharray type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonLineDashArray.userType() ) ) )
788 );
789 break;
790 }
791 }
792
793 Qt::PenCapStyle penCapStyle = Qt::FlatCap;
794 Qt::PenJoinStyle penJoinStyle = Qt::MiterJoin;
795 if ( jsonLayer.contains( u"layout"_s ) )
796 {
797 const QVariantMap jsonLayout = jsonLayer.value( u"layout"_s ).toMap();
798 if ( jsonLayout.contains( u"line-cap"_s ) )
799 {
800 penCapStyle = parseCapStyle( jsonLayout.value( u"line-cap"_s ).toString() );
801 }
802 if ( jsonLayout.contains( u"line-join"_s ) )
803 {
804 penJoinStyle = parseJoinStyle( jsonLayout.value( u"line-join"_s ).toString() );
805 }
806 }
807
808 std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsLineSymbol >() );
809 symbol->setOutputUnit( context.targetUnit() );
810
811 if ( !rasterLineSprite.isEmpty() )
812 {
813 QgsRasterLineSymbolLayer *lineSymbol = new QgsRasterLineSymbolLayer( rasterLineSprite );
814 lineSymbol->setOutputUnit( context.targetUnit() );
815 lineSymbol->setPenCapStyle( penCapStyle );
816 lineSymbol->setPenJoinStyle( penJoinStyle );
817 lineSymbol->setDataDefinedProperties( ddProperties );
818 lineSymbol->setOffset( lineOffset );
819 lineSymbol->setOffsetUnit( context.targetUnit() );
820
821 if ( lineOpacity != -1 )
822 {
823 symbol->setOpacity( lineOpacity );
824 }
825 if ( !lineOpacityProperty.asExpression().isEmpty() )
826 {
827 QgsPropertyCollection ddProperties;
828 ddProperties.setProperty( QgsSymbol::Property::Opacity, lineOpacityProperty );
829 symbol->setDataDefinedProperties( ddProperties );
830 }
831 if ( lineWidth != -1 )
832 {
833 lineSymbol->setWidth( lineWidth );
834 }
835 symbol->changeSymbolLayer( 0, lineSymbol );
836 }
837 else
838 {
839 QgsSimpleLineSymbolLayer *lineSymbol = dynamic_cast< QgsSimpleLineSymbolLayer * >( symbol->symbolLayer( 0 ) );
840 Q_ASSERT( lineSymbol ); // should not fail since QgsLineSymbol() constructor instantiates a QgsSimpleLineSymbolLayer
841
842 // set render units
843 lineSymbol->setOutputUnit( context.targetUnit() );
844 lineSymbol->setPenCapStyle( penCapStyle );
845 lineSymbol->setPenJoinStyle( penJoinStyle );
846 lineSymbol->setDataDefinedProperties( ddProperties );
847 lineSymbol->setOffset( lineOffset );
848 lineSymbol->setOffsetUnit( context.targetUnit() );
849
850 if ( lineOpacity != -1 )
851 {
852 symbol->setOpacity( lineOpacity );
853 }
854 if ( !lineOpacityProperty.asExpression().isEmpty() )
855 {
856 QgsPropertyCollection ddProperties;
857 ddProperties.setProperty( QgsSymbol::Property::Opacity, lineOpacityProperty );
858 symbol->setDataDefinedProperties( ddProperties );
859 }
860 if ( lineColor.isValid() )
861 {
862 lineSymbol->setColor( lineColor );
863 }
864 if ( lineWidth != -1 )
865 {
866 lineSymbol->setWidth( lineWidth );
867 }
868 if ( !dashVector.empty() )
869 {
870 lineSymbol->setUseCustomDashPattern( true );
871 lineSymbol->setCustomDashVector( dashVector );
872 }
873 }
874
876 style.setSymbol( symbol.release() );
877 return true;
878}
879
881{
882 if ( !jsonLayer.contains( u"paint"_s ) )
883 {
884 context.pushWarning( QObject::tr( "%1: Style has no paint property, skipping" ).arg( context.layerId() ) );
885 return false;
886 }
887
888 const QVariantMap jsonPaint = jsonLayer.value( u"paint"_s ).toMap();
889
890 QgsPropertyCollection ddProperties;
891
892 // circle color
893 QColor circleFillColor;
894 if ( jsonPaint.contains( u"circle-color"_s ) )
895 {
896 const QVariant jsonCircleColor = jsonPaint.value( u"circle-color"_s );
897 switch ( jsonCircleColor.userType() )
898 {
899 case QMetaType::Type::QVariantMap:
900 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateColorByZoom( jsonCircleColor.toMap(), context, &circleFillColor ) );
901 break;
902
903 case QMetaType::Type::QVariantList:
904 case QMetaType::Type::QStringList:
905 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonCircleColor.toList(), PropertyType::Color, context, 1, 255, &circleFillColor ) );
906 break;
907
908 case QMetaType::Type::QString:
909 circleFillColor = parseColor( jsonCircleColor.toString(), context );
910 break;
911
912 default:
913 context.pushWarning( QObject::tr( "%1: Skipping unsupported circle-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleColor.userType() ) ) ) );
914 break;
915 }
916 }
917 else
918 {
919 // defaults to #000000
920 circleFillColor = QColor( 0, 0, 0 );
921 }
922
923 // circle radius
924 double circleDiameter = 10.0;
925 if ( jsonPaint.contains( u"circle-radius"_s ) )
926 {
927 const QVariant jsonCircleRadius = jsonPaint.value( u"circle-radius"_s );
928 switch ( jsonCircleRadius.userType() )
929 {
930 case QMetaType::Type::Int:
931 case QMetaType::Type::LongLong:
932 case QMetaType::Type::Double:
933 circleDiameter = jsonCircleRadius.toDouble() * context.pixelSizeConversionFactor() * 2;
934 break;
935
936 case QMetaType::Type::QVariantMap:
937 circleDiameter = -1;
938 ddProperties.setProperty( QgsSymbolLayer::Property::Size, parseInterpolateByZoom( jsonCircleRadius.toMap(), context, context.pixelSizeConversionFactor() * 2, &circleDiameter ) );
939 break;
940
941 case QMetaType::Type::QVariantList:
942 case QMetaType::Type::QStringList:
943 ddProperties.setProperty( QgsSymbolLayer::Property::Size, parseValueList( jsonCircleRadius.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * 2, 255, nullptr, &circleDiameter ) );
944 break;
945
946 default:
947 context.pushWarning(
948 QObject::tr( "%1: Skipping unsupported circle-radius type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleRadius.userType() ) ) )
949 );
950 break;
951 }
952 }
953
954 double circleOpacity = -1.0;
955 if ( jsonPaint.contains( u"circle-opacity"_s ) )
956 {
957 const QVariant jsonCircleOpacity = jsonPaint.value( u"circle-opacity"_s );
958 switch ( jsonCircleOpacity.userType() )
959 {
960 case QMetaType::Type::Int:
961 case QMetaType::Type::LongLong:
962 case QMetaType::Type::Double:
963 circleOpacity = jsonCircleOpacity.toDouble();
964 break;
965
966 case QMetaType::Type::QVariantMap:
967 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseInterpolateOpacityByZoom( jsonCircleOpacity.toMap(), circleFillColor.isValid() ? circleFillColor.alpha() : 255, &context ) );
968 break;
969
970 case QMetaType::Type::QVariantList:
971 case QMetaType::Type::QStringList:
972 ddProperties.setProperty( QgsSymbolLayer::Property::FillColor, parseValueList( jsonCircleOpacity.toList(), PropertyType::Opacity, context, 1, circleFillColor.isValid() ? circleFillColor.alpha() : 255 ) );
973 break;
974
975 default:
976 context.pushWarning(
977 QObject::tr( "%1: Skipping unsupported circle-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleOpacity.userType() ) ) )
978 );
979 break;
980 }
981 }
982 if ( ( circleOpacity != -1 ) && circleFillColor.isValid() )
983 {
984 circleFillColor.setAlphaF( circleOpacity );
985 }
986
987 // circle stroke color
988 QColor circleStrokeColor;
989 if ( jsonPaint.contains( u"circle-stroke-color"_s ) )
990 {
991 const QVariant jsonCircleStrokeColor = jsonPaint.value( u"circle-stroke-color"_s );
992 switch ( jsonCircleStrokeColor.userType() )
993 {
994 case QMetaType::Type::QVariantMap:
995 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateColorByZoom( jsonCircleStrokeColor.toMap(), context, &circleStrokeColor ) );
996 break;
997
998 case QMetaType::Type::QVariantList:
999 case QMetaType::Type::QStringList:
1000 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonCircleStrokeColor.toList(), PropertyType::Color, context, 1, 255, &circleStrokeColor ) );
1001 break;
1002
1003 case QMetaType::Type::QString:
1004 circleStrokeColor = parseColor( jsonCircleStrokeColor.toString(), context );
1005 break;
1006
1007 default:
1008 context.pushWarning(
1009 QObject::tr( "%1: Skipping unsupported circle-stroke-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleStrokeColor.userType() ) ) )
1010 );
1011 break;
1012 }
1013 }
1014
1015 // circle stroke width
1016 double circleStrokeWidth = -1.0;
1017 if ( jsonPaint.contains( u"circle-stroke-width"_s ) )
1018 {
1019 const QVariant circleStrokeWidthJson = jsonPaint.value( u"circle-stroke-width"_s );
1020 switch ( circleStrokeWidthJson.userType() )
1021 {
1022 case QMetaType::Type::Int:
1023 case QMetaType::Type::LongLong:
1024 case QMetaType::Type::Double:
1025 circleStrokeWidth = circleStrokeWidthJson.toDouble() * context.pixelSizeConversionFactor();
1026 break;
1027
1028 case QMetaType::Type::QVariantMap:
1029 circleStrokeWidth = -1.0;
1030 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, parseInterpolateByZoom( circleStrokeWidthJson.toMap(), context, context.pixelSizeConversionFactor(), &circleStrokeWidth ) );
1031 break;
1032
1033 case QMetaType::Type::QVariantList:
1034 case QMetaType::Type::QStringList:
1035 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeWidth, parseValueList( circleStrokeWidthJson.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &circleStrokeWidth ) );
1036 break;
1037
1038 default:
1039 context.pushWarning(
1040 QObject::tr( "%1: Skipping unsupported circle-stroke-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( circleStrokeWidthJson.userType() ) ) )
1041 );
1042 break;
1043 }
1044 }
1045
1046 double circleStrokeOpacity = -1.0;
1047 if ( jsonPaint.contains( u"circle-stroke-opacity"_s ) )
1048 {
1049 const QVariant jsonCircleStrokeOpacity = jsonPaint.value( u"circle-stroke-opacity"_s );
1050 switch ( jsonCircleStrokeOpacity.userType() )
1051 {
1052 case QMetaType::Type::Int:
1053 case QMetaType::Type::LongLong:
1054 case QMetaType::Type::Double:
1055 circleStrokeOpacity = jsonCircleStrokeOpacity.toDouble();
1056 break;
1057
1058 case QMetaType::Type::QVariantMap:
1059 ddProperties.setProperty( QgsSymbolLayer::Property::StrokeColor, parseInterpolateOpacityByZoom( jsonCircleStrokeOpacity.toMap(), circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255, &context ) );
1060 break;
1061
1062 case QMetaType::Type::QVariantList:
1063 case QMetaType::Type::QStringList:
1064 ddProperties
1065 .setProperty( QgsSymbolLayer::Property::StrokeColor, parseValueList( jsonCircleStrokeOpacity.toList(), PropertyType::Opacity, context, 1, circleStrokeColor.isValid() ? circleStrokeColor.alpha() : 255 ) );
1066 break;
1067
1068 default:
1069 context.pushWarning(
1070 QObject::tr( "%1: Skipping unsupported circle-stroke-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleStrokeOpacity.userType() ) ) )
1071 );
1072 break;
1073 }
1074 }
1075 if ( ( circleStrokeOpacity != -1 ) && circleStrokeColor.isValid() )
1076 {
1077 circleStrokeColor.setAlphaF( circleStrokeOpacity );
1078 }
1079
1080 // translate
1081 QPointF circleTranslate;
1082 if ( jsonPaint.contains( u"circle-translate"_s ) )
1083 {
1084 const QVariant jsonCircleTranslate = jsonPaint.value( u"circle-translate"_s );
1085 switch ( jsonCircleTranslate.userType() )
1086 {
1087 case QMetaType::Type::QVariantMap:
1088 ddProperties.setProperty( QgsSymbolLayer::Property::Offset, parseInterpolatePointByZoom( jsonCircleTranslate.toMap(), context, context.pixelSizeConversionFactor(), &circleTranslate ) );
1089 break;
1090
1091 case QMetaType::Type::QVariantList:
1092 case QMetaType::Type::QStringList:
1093 circleTranslate
1094 = QPointF( jsonCircleTranslate.toList().value( 0 ).toDouble() * context.pixelSizeConversionFactor(), jsonCircleTranslate.toList().value( 1 ).toDouble() * context.pixelSizeConversionFactor() );
1095 break;
1096
1097 default:
1098 context.pushWarning(
1099 QObject::tr( "%1: Skipping unsupported circle-translate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonCircleTranslate.userType() ) ) )
1100 );
1101 break;
1102 }
1103 }
1104
1105 std::unique_ptr< QgsSymbol > symbol( std::make_unique< QgsMarkerSymbol >() );
1106 QgsSimpleMarkerSymbolLayer *markerSymbolLayer = dynamic_cast< QgsSimpleMarkerSymbolLayer * >( symbol->symbolLayer( 0 ) );
1107 Q_ASSERT( markerSymbolLayer );
1108
1109 // set render units
1110 symbol->setOutputUnit( context.targetUnit() );
1111 markerSymbolLayer->setDataDefinedProperties( ddProperties );
1112
1113 if ( !circleTranslate.isNull() )
1114 {
1115 markerSymbolLayer->setOffset( circleTranslate );
1116 markerSymbolLayer->setOffsetUnit( context.targetUnit() );
1117 }
1118
1119 if ( circleFillColor.isValid() )
1120 {
1121 markerSymbolLayer->setFillColor( circleFillColor );
1122 }
1123 if ( circleDiameter != -1 )
1124 {
1125 markerSymbolLayer->setSize( circleDiameter );
1126 markerSymbolLayer->setSizeUnit( context.targetUnit() );
1127 }
1128 if ( circleStrokeColor.isValid() )
1129 {
1130 markerSymbolLayer->setStrokeColor( circleStrokeColor );
1131 }
1132 if ( circleStrokeWidth != -1 )
1133 {
1134 markerSymbolLayer->setStrokeWidth( circleStrokeWidth );
1135 markerSymbolLayer->setStrokeWidthUnit( context.targetUnit() );
1136 }
1137
1139 style.setSymbol( symbol.release() );
1140 return true;
1141}
1142
1144 const QVariantMap &jsonLayer, QgsVectorTileBasicRendererStyle &renderer, bool &hasRenderer, QgsVectorTileBasicLabelingStyle &labelingStyle, bool &hasLabeling, QgsMapBoxGlStyleConversionContext &context
1145)
1146{
1147 hasLabeling = false;
1148 hasRenderer = false;
1149
1150 if ( !jsonLayer.contains( u"layout"_s ) )
1151 {
1152 context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
1153 return;
1154 }
1155 const QVariantMap jsonLayout = jsonLayer.value( u"layout"_s ).toMap();
1156 if ( !jsonLayout.contains( u"text-field"_s ) )
1157 {
1158 hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
1159 return;
1160 }
1161
1162 const QVariantMap jsonPaint = jsonLayer.value( u"paint"_s ).toMap();
1163
1164 QgsPropertyCollection ddLabelProperties;
1165
1166 double textSize = 16.0 * context.pixelSizeConversionFactor();
1167 QgsProperty textSizeProperty;
1168 if ( jsonLayout.contains( u"text-size"_s ) )
1169 {
1170 const QVariant jsonTextSize = jsonLayout.value( u"text-size"_s );
1171 switch ( jsonTextSize.userType() )
1172 {
1173 case QMetaType::Type::Int:
1174 case QMetaType::Type::LongLong:
1175 case QMetaType::Type::Double:
1176 textSize = jsonTextSize.toDouble() * context.pixelSizeConversionFactor();
1177 break;
1178
1179 case QMetaType::Type::QVariantMap:
1180 textSize = -1;
1181 textSizeProperty = parseInterpolateByZoom( jsonTextSize.toMap(), context, context.pixelSizeConversionFactor(), &textSize );
1182
1183 break;
1184
1185 case QMetaType::Type::QVariantList:
1186 case QMetaType::Type::QStringList:
1187 textSize = -1;
1188 textSizeProperty = parseValueList( jsonTextSize.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &textSize );
1189 break;
1190
1191 default:
1192 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextSize.userType() ) ) ) );
1193 break;
1194 }
1195
1196 if ( textSizeProperty )
1197 {
1198 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Size, textSizeProperty );
1199 }
1200 }
1201
1202 // a rough average of ems to character count conversion for a variety of fonts
1203 constexpr double EM_TO_CHARS = 2.0;
1204
1205 double textMaxWidth = -1;
1206 if ( jsonLayout.contains( u"text-max-width"_s ) )
1207 {
1208 const QVariant jsonTextMaxWidth = jsonLayout.value( u"text-max-width"_s );
1209 switch ( jsonTextMaxWidth.userType() )
1210 {
1211 case QMetaType::Type::Int:
1212 case QMetaType::Type::LongLong:
1213 case QMetaType::Type::Double:
1214 textMaxWidth = jsonTextMaxWidth.toDouble() * EM_TO_CHARS;
1215 break;
1216
1217 case QMetaType::Type::QVariantMap:
1218 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::AutoWrapLength, parseInterpolateByZoom( jsonTextMaxWidth.toMap(), context, EM_TO_CHARS, &textMaxWidth ) );
1219 break;
1220
1221 case QMetaType::Type::QVariantList:
1222 case QMetaType::Type::QStringList:
1223 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::AutoWrapLength, parseValueList( jsonTextMaxWidth.toList(), PropertyType::Numeric, context, EM_TO_CHARS, 255, nullptr, &textMaxWidth ) );
1224 break;
1225
1226 default:
1227 context.pushWarning(
1228 QObject::tr( "%1: Skipping unsupported text-max-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextMaxWidth.userType() ) ) )
1229 );
1230 break;
1231 }
1232 }
1233 else
1234 {
1235 // defaults to 10
1236 textMaxWidth = 10 * EM_TO_CHARS;
1237 }
1238
1239 double textLetterSpacing = -1;
1240 if ( jsonLayout.contains( u"text-letter-spacing"_s ) )
1241 {
1242 const QVariant jsonTextLetterSpacing = jsonLayout.value( u"text-letter-spacing"_s );
1243 switch ( jsonTextLetterSpacing.userType() )
1244 {
1245 case QMetaType::Type::Int:
1246 case QMetaType::Type::LongLong:
1247 case QMetaType::Type::Double:
1248 textLetterSpacing = jsonTextLetterSpacing.toDouble();
1249 break;
1250
1251 case QMetaType::Type::QVariantMap:
1252 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::FontLetterSpacing, parseInterpolateByZoom( jsonTextLetterSpacing.toMap(), context, 1, &textLetterSpacing ) );
1253 break;
1254
1255 case QMetaType::Type::QVariantList:
1256 case QMetaType::Type::QStringList:
1257 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::FontLetterSpacing, parseValueList( jsonTextLetterSpacing.toList(), PropertyType::Numeric, context, 1, 255, nullptr, &textLetterSpacing ) );
1258 break;
1259
1260 default:
1261 context.pushWarning(
1262 QObject::tr( "%1: Skipping unsupported text-letter-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextLetterSpacing.userType() ) ) )
1263 );
1264 break;
1265 }
1266 }
1267
1268 QFont textFont;
1269 bool foundFont = false;
1270 QString fontName;
1271 QString fontStyleName;
1272
1273 bool allowOverlap = jsonLayout.contains( u"text-allow-overlap"_s ) && jsonLayout.value( u"text-allow-overlap"_s ).toBool();
1274
1275 if ( jsonLayout.contains( u"text-font"_s ) )
1276 {
1277 auto splitFontFamily = []( const QString &fontName, QString &family, QString &style ) -> bool {
1278 QString matchedFamily;
1279 const QStringList textFontParts = fontName.split( ' ' );
1280 for ( int i = textFontParts.size() - 1; i >= 1; --i )
1281 {
1282 const QString candidateFontFamily = textFontParts.mid( 0, i ).join( ' ' );
1283 const QString candidateFontStyle = textFontParts.mid( i ).join( ' ' );
1284
1285 const QString processedFontFamily = QgsApplication::fontManager()->processFontFamilyName( candidateFontFamily );
1286 if ( QgsFontUtils::fontFamilyHasStyle( processedFontFamily, candidateFontStyle ) )
1287 {
1288 family = processedFontFamily;
1289 style = candidateFontStyle;
1290 return true;
1291 }
1292 else if ( QgsApplication::fontManager()->tryToDownloadFontFamily( processedFontFamily, matchedFamily ) )
1293 {
1294 if ( processedFontFamily == matchedFamily )
1295 {
1296 family = processedFontFamily;
1297 style = candidateFontStyle;
1298 }
1299 else
1300 {
1301 family = matchedFamily;
1302 style = processedFontFamily;
1303 style.replace( matchedFamily, QString() );
1304 style = style.trimmed();
1305 if ( !style.isEmpty() && !candidateFontStyle.isEmpty() )
1306 {
1307 style += u" %1"_s.arg( candidateFontStyle );
1308 }
1309 }
1310 return true;
1311 }
1312 }
1313
1314 const QString processedFontFamily = QgsApplication::fontManager()->processFontFamilyName( fontName );
1315 if ( QFontDatabase().hasFamily( processedFontFamily ) )
1316 {
1317 // the json isn't following the spec correctly!!
1318 family = processedFontFamily;
1319 style.clear();
1320 return true;
1321 }
1322 else if ( QgsApplication::fontManager()->tryToDownloadFontFamily( processedFontFamily, matchedFamily ) )
1323 {
1324 family = matchedFamily;
1325 style.clear();
1326 return true;
1327 }
1328 return false;
1329 };
1330
1331 const QVariant jsonTextFont = jsonLayout.value( u"text-font"_s );
1332 if ( jsonTextFont.userType() != QMetaType::Type::QVariantList
1333 && jsonTextFont.userType() != QMetaType::Type::QStringList
1334 && jsonTextFont.userType() != QMetaType::Type::QString
1335 && jsonTextFont.userType() != QMetaType::Type::QVariantMap )
1336 {
1337 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-font type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextFont.userType() ) ) ) );
1338 }
1339 else
1340 {
1341 switch ( jsonTextFont.userType() )
1342 {
1343 case QMetaType::Type::QVariantList:
1344 case QMetaType::Type::QStringList:
1345 fontName = jsonTextFont.toList().value( 0 ).toString();
1346 break;
1347
1348 case QMetaType::Type::QString:
1349 fontName = jsonTextFont.toString();
1350 break;
1351
1352 case QMetaType::Type::QVariantMap:
1353 {
1354 QString familyCaseString = u"CASE "_s;
1355 QString styleCaseString = u"CASE "_s;
1356 QString fontFamily;
1357 const QVariantList stops = jsonTextFont.toMap().value( u"stops"_s ).toList();
1358
1359 bool error = false;
1360 for ( int i = 0; i < stops.length() - 1; ++i )
1361 {
1362 // bottom zoom and value
1363 const QVariant bz = stops.value( i ).toList().value( 0 );
1364 const QString bv = stops.value( i ).toList().value( 1 ).userType() == QMetaType::Type::QString ? stops.value( i ).toList().value( 1 ).toString()
1365 : stops.value( i ).toList().value( 1 ).toList().value( 0 ).toString();
1366 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
1367 {
1368 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1369 error = true;
1370 break;
1371 }
1372
1373 // top zoom
1374 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
1375 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
1376 {
1377 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
1378 error = true;
1379 break;
1380 }
1381
1382 if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1383 {
1384 familyCaseString += QStringLiteral(
1385 "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1386 "THEN %3 "
1387 )
1388 .arg( bz.toString(), tz.toString(), QgsExpression::quotedValue( fontFamily ) );
1389 styleCaseString += QStringLiteral(
1390 "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
1391 "THEN %3 "
1392 )
1393 .arg( bz.toString(), tz.toString(), QgsExpression::quotedValue( fontStyleName ) );
1394 }
1395 else
1396 {
1397 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1398 }
1399 }
1400 if ( error )
1401 break;
1402
1403 const QString bv = stops.constLast().toList().value( 1 ).userType() == QMetaType::Type::QString ? stops.constLast().toList().value( 1 ).toString()
1404 : stops.constLast().toList().value( 1 ).toList().value( 0 ).toString();
1405 if ( splitFontFamily( bv, fontFamily, fontStyleName ) )
1406 {
1407 familyCaseString += u"ELSE %1 END"_s.arg( QgsExpression::quotedValue( fontFamily ) );
1408 styleCaseString += u"ELSE %1 END"_s.arg( QgsExpression::quotedValue( fontStyleName ) );
1409 }
1410 else
1411 {
1412 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), bv ) );
1413 }
1414
1415 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Family, QgsProperty::fromExpression( familyCaseString ) );
1417
1418 foundFont = true;
1419 fontName = fontFamily;
1420
1421 break;
1422 }
1423
1424 default:
1425 break;
1426 }
1427
1428 QString fontFamily;
1429 if ( splitFontFamily( fontName, fontFamily, fontStyleName ) )
1430 {
1431 textFont = QgsFontUtils::createFont( fontFamily );
1432 if ( !fontStyleName.isEmpty() )
1433 textFont.setStyleName( fontStyleName );
1434 foundFont = true;
1435 }
1436 }
1437 }
1438 else
1439 {
1440 // Defaults to ["Open Sans Regular","Arial Unicode MS Regular"].
1441 if ( QgsFontUtils::fontFamilyHasStyle( u"Open Sans"_s, u"Regular"_s ) )
1442 {
1443 fontName = u"Open Sans"_s;
1444 textFont = QgsFontUtils::createFont( fontName );
1445 textFont.setStyleName( u"Regular"_s );
1446 fontStyleName = u"Regular"_s;
1447 foundFont = true;
1448 }
1449 else if ( QgsFontUtils::fontFamilyHasStyle( u"Arial Unicode MS"_s, u"Regular"_s ) )
1450 {
1451 fontName = u"Arial Unicode MS"_s;
1452 textFont = QgsFontUtils::createFont( fontName );
1453 textFont.setStyleName( u"Regular"_s );
1454 fontStyleName = u"Regular"_s;
1455 foundFont = true;
1456 }
1457 else
1458 {
1459 fontName = u"Open Sans, Arial Unicode MS"_s;
1460 }
1461 }
1462 if ( !foundFont && !fontName.isEmpty() )
1463 {
1464 context.pushWarning( QObject::tr( "%1: Referenced font %2 is not available on system" ).arg( context.layerId(), fontName ) );
1465 }
1466
1467 // text color
1468 QColor textColor;
1469 if ( jsonPaint.contains( u"text-color"_s ) )
1470 {
1471 const QVariant jsonTextColor = jsonPaint.value( u"text-color"_s );
1472 switch ( jsonTextColor.userType() )
1473 {
1474 case QMetaType::Type::QVariantMap:
1475 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Color, parseInterpolateColorByZoom( jsonTextColor.toMap(), context, &textColor ) );
1476 break;
1477
1478 case QMetaType::Type::QVariantList:
1479 case QMetaType::Type::QStringList:
1480 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::Color, parseValueList( jsonTextColor.toList(), PropertyType::Color, context, 1, 255, &textColor ) );
1481 break;
1482
1483 case QMetaType::Type::QString:
1484 textColor = parseColor( jsonTextColor.toString(), context );
1485 break;
1486
1487 default:
1488 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextColor.userType() ) ) ) );
1489 break;
1490 }
1491 }
1492 else
1493 {
1494 // defaults to #000000
1495 textColor = QColor( 0, 0, 0 );
1496 }
1497
1498 // buffer color
1499 QColor bufferColor( 0, 0, 0, 0 );
1500 if ( jsonPaint.contains( u"text-halo-color"_s ) )
1501 {
1502 const QVariant jsonBufferColor = jsonPaint.value( u"text-halo-color"_s );
1503 switch ( jsonBufferColor.userType() )
1504 {
1505 case QMetaType::Type::QVariantMap:
1506 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferColor, parseInterpolateColorByZoom( jsonBufferColor.toMap(), context, &bufferColor ) );
1507 break;
1508
1509 case QMetaType::Type::QVariantList:
1510 case QMetaType::Type::QStringList:
1511 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferColor, parseValueList( jsonBufferColor.toList(), PropertyType::Color, context, 1, 255, &bufferColor ) );
1512 break;
1513
1514 case QMetaType::Type::QString:
1515 bufferColor = parseColor( jsonBufferColor.toString(), context );
1516 break;
1517
1518 default:
1519 context.pushWarning(
1520 QObject::tr( "%1: Skipping unsupported text-halo-color type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonBufferColor.userType() ) ) )
1521 );
1522 break;
1523 }
1524 }
1525
1526 double bufferSize = 0.0;
1527 // the pixel based text buffers appear larger when rendered on the web - so automatically scale
1528 // them up when converting to a QGIS style
1529 // (this number is based on trial-and-error comparisons only!)
1530 constexpr double BUFFER_SIZE_SCALE = 2.0;
1531 if ( jsonPaint.contains( u"text-halo-width"_s ) )
1532 {
1533 const QVariant jsonHaloWidth = jsonPaint.value( u"text-halo-width"_s );
1534 QString bufferSizeDataDefined;
1535 switch ( jsonHaloWidth.userType() )
1536 {
1537 case QMetaType::Type::Int:
1538 case QMetaType::Type::LongLong:
1539 case QMetaType::Type::Double:
1540 bufferSize = jsonHaloWidth.toDouble() * context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE;
1541 break;
1542
1543 case QMetaType::Type::QVariantMap:
1544 bufferSize = 1;
1545 bufferSizeDataDefined = parseInterpolateByZoom( jsonHaloWidth.toMap(), context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, &bufferSize ).asExpression();
1546 break;
1547
1548 case QMetaType::Type::QVariantList:
1549 case QMetaType::Type::QStringList:
1550 bufferSize = 1;
1551 bufferSizeDataDefined = parseValueList( jsonHaloWidth.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor() * BUFFER_SIZE_SCALE, 255, nullptr, &bufferSize ).asExpression();
1552 break;
1553
1554 default:
1555 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-halo-width type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonHaloWidth.userType() ) ) ) );
1556 break;
1557 }
1558
1559 // from the specs halo should not be larger than 1/4 of the text-size
1560 // https://docs.mapbox.com/style-spec/reference/layers/#paint-symbol-text-halo-width
1561 if ( bufferSize > 0 )
1562 {
1563 if ( textSize > 0 && bufferSizeDataDefined.isEmpty() )
1564 {
1565 bufferSize = std::min( bufferSize, textSize * BUFFER_SIZE_SCALE / 4 );
1566 }
1567 else if ( textSize > 0 && !bufferSizeDataDefined.isEmpty() )
1568 {
1569 bufferSizeDataDefined = u"min(%1/4, %2)"_s.arg( textSize * BUFFER_SIZE_SCALE ).arg( bufferSizeDataDefined );
1570 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1571 }
1572 else if ( !bufferSizeDataDefined.isEmpty() )
1573 {
1574 bufferSizeDataDefined = u"min(%1*%2/4, %3)"_s.arg( textSizeProperty.asExpression() ).arg( BUFFER_SIZE_SCALE ).arg( bufferSizeDataDefined );
1575 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1576 }
1577 else if ( bufferSizeDataDefined.isEmpty() )
1578 {
1579 bufferSizeDataDefined = u"min(%1*%2/4, %3)"_s.arg( textSizeProperty.asExpression() ).arg( BUFFER_SIZE_SCALE ).arg( bufferSize );
1580 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::BufferSize, QgsProperty::fromExpression( bufferSizeDataDefined ) );
1581 }
1582 }
1583 }
1584
1585 double haloBlurSize = 0;
1586 if ( jsonPaint.contains( u"text-halo-blur"_s ) )
1587 {
1588 const QVariant jsonTextHaloBlur = jsonPaint.value( u"text-halo-blur"_s );
1589 switch ( jsonTextHaloBlur.userType() )
1590 {
1591 case QMetaType::Type::Int:
1592 case QMetaType::Type::LongLong:
1593 case QMetaType::Type::Double:
1594 {
1595 haloBlurSize = jsonTextHaloBlur.toDouble() * context.pixelSizeConversionFactor();
1596 break;
1597 }
1598
1599 default:
1600 context.pushWarning(
1601 QObject::tr( "%1: Skipping unsupported text-halo-blur type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextHaloBlur.userType() ) ) )
1602 );
1603 break;
1604 }
1605 }
1606
1607 QgsTextFormat format;
1608 format.setSizeUnit( context.targetUnit() );
1609 if ( textColor.isValid() )
1610 format.setColor( textColor );
1611 if ( textSize >= 0 )
1612 format.setSize( textSize );
1613 if ( foundFont )
1614 {
1615 format.setFont( textFont );
1616 if ( !fontStyleName.isEmpty() )
1617 format.setNamedStyle( fontStyleName );
1618 }
1619 if ( textLetterSpacing > 0 )
1620 {
1621 QFont f = format.font();
1622 f.setLetterSpacing( QFont::AbsoluteSpacing, textLetterSpacing );
1623 format.setFont( f );
1624 }
1625
1626 if ( bufferSize > 0 )
1627 {
1628 // Color and opacity are separate components in QGIS
1629 const double opacity = bufferColor.alphaF();
1630 bufferColor.setAlphaF( 1.0 );
1631
1632 format.buffer().setEnabled( true );
1633 format.buffer().setSize( bufferSize );
1634 format.buffer().setSizeUnit( context.targetUnit() );
1635 format.buffer().setColor( bufferColor );
1636 format.buffer().setOpacity( opacity );
1637
1638 if ( haloBlurSize > 0 )
1639 {
1640 QgsEffectStack *stack = new QgsEffectStack();
1641 QgsBlurEffect *blur = new QgsBlurEffect();
1642 blur->setEnabled( true );
1643 blur->setBlurUnit( context.targetUnit() );
1644 blur->setBlurLevel( haloBlurSize );
1646 stack->appendEffect( blur );
1647 stack->setEnabled( true );
1648 format.buffer().setPaintEffect( stack );
1649 }
1650 }
1651
1652 QgsPalLayerSettings labelSettings;
1653 if ( allowOverlap )
1654 {
1655 QgsLabelPlacementSettings placementSettings = labelSettings.placementSettings();
1657 placementSettings.setAllowDegradedPlacement( true );
1658 labelSettings.setPlacementSettings( placementSettings );
1659 }
1660
1661 if ( textMaxWidth > 0 )
1662 {
1663 labelSettings.autoWrapLength = textMaxWidth;
1664 }
1665
1666 // convert field name
1667 if ( jsonLayout.contains( u"text-field"_s ) )
1668 {
1669 const QVariant jsonTextField = jsonLayout.value( u"text-field"_s );
1670 switch ( jsonTextField.userType() )
1671 {
1672 case QMetaType::Type::QString:
1673 {
1674 labelSettings.fieldName = processLabelField( jsonTextField.toString(), labelSettings.isExpression );
1675 break;
1676 }
1677
1678 case QMetaType::Type::QVariantList:
1679 case QMetaType::Type::QStringList:
1680 {
1681 const QVariantList textFieldList = jsonTextField.toList();
1682 /*
1683 * e.g.
1684 * "text-field": ["format",
1685 * "foo", { "font-scale": 1.2 },
1686 * "bar", { "font-scale": 0.8 }
1687 * ]
1688 */
1689 if ( textFieldList.size() > 2 && textFieldList.at( 0 ).toString() == "format"_L1 )
1690 {
1691 QStringList parts;
1692 for ( int i = 1; i < textFieldList.size(); ++i )
1693 {
1694 bool isExpression = false;
1695 const QString part = processLabelField( textFieldList.at( i ).toString(), isExpression );
1696 if ( !isExpression )
1697 parts << QgsExpression::quotedColumnRef( part );
1698 else
1699 parts << part;
1700 // TODO -- we could also translate font color, underline, overline, strikethrough to HTML tags!
1701 i += 1;
1702 }
1703 labelSettings.fieldName = u"concat(%1)"_s.arg( parts.join( ',' ) );
1704 labelSettings.isExpression = true;
1705 }
1706 else
1707 {
1708 /*
1709 * e.g.
1710 * "text-field": ["to-string", ["get", "name"]]
1711 */
1712 labelSettings.fieldName = parseExpression( textFieldList, context );
1713 labelSettings.isExpression = true;
1714 }
1715 break;
1716 }
1717
1718 case QMetaType::Type::QVariantMap:
1719 {
1720 const QVariantList stops = jsonTextField.toMap().value( u"stops"_s ).toList();
1721 if ( !stops.empty() )
1722 {
1723 labelSettings.fieldName = parseLabelStops( stops, context );
1724 labelSettings.isExpression = true;
1725 }
1726 else
1727 {
1728 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field dictionary" ).arg( context.layerId() ) );
1729 }
1730 break;
1731 }
1732
1733 default:
1734 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-field type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextField.userType() ) ) ) );
1735 break;
1736 }
1737 }
1738
1739 if ( jsonLayout.contains( u"text-rotate"_s ) )
1740 {
1741 const QVariant jsonTextRotate = jsonLayout.value( u"text-rotate"_s );
1742 switch ( jsonTextRotate.userType() )
1743 {
1744 case QMetaType::Type::Double:
1745 case QMetaType::Type::Int:
1746 {
1747 labelSettings.angleOffset = jsonTextRotate.toDouble();
1748 break;
1749 }
1750
1751 case QMetaType::Type::QVariantList:
1752 case QMetaType::Type::QStringList:
1753 {
1754 const QgsProperty property = parseValueList( jsonTextRotate.toList(), PropertyType::Numeric, context );
1755 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelRotation, property );
1756 break;
1757 }
1758
1759 case QMetaType::Type::QVariantMap:
1760 {
1761 QVariantMap rotateMap = jsonTextRotate.toMap();
1762 if ( rotateMap.contains( u"property"_s ) && rotateMap[u"type"_s].toString() == "identity"_L1 )
1763 {
1764 const QgsProperty property = QgsProperty::fromExpression( rotateMap[u"property"_s].toString() );
1765 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelRotation, property );
1766 }
1767 else
1768 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-rotate map content (%2)" ).arg( context.layerId(), QString( QJsonDocument::fromVariant( rotateMap ).toJson() ) ) );
1769 break;
1770 }
1771
1772 default:
1773 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextRotate.userType() ) ) ) );
1774 break;
1775 }
1776 }
1777
1778 if ( jsonLayout.contains( u"text-transform"_s ) )
1779 {
1780 const QString textTransform = jsonLayout.value( u"text-transform"_s ).toString();
1781 if ( textTransform == "uppercase"_L1 )
1782 {
1783 labelSettings.fieldName = u"upper(%1)"_s.arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1784 }
1785 else if ( textTransform == "lowercase"_L1 )
1786 {
1787 labelSettings.fieldName = u"lower(%1)"_s.arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1788 }
1789 labelSettings.isExpression = true;
1790 }
1791
1794 if ( jsonLayout.contains( u"symbol-placement"_s ) )
1795 {
1796 const QString symbolPlacement = jsonLayout.value( u"symbol-placement"_s ).toString();
1797 if ( symbolPlacement == "line"_L1 )
1798 {
1801 geometryType = Qgis::GeometryType::Line;
1802
1803 if ( jsonLayout.contains( u"text-rotation-alignment"_s ) )
1804 {
1805 const QString textRotationAlignment = jsonLayout.value( u"text-rotation-alignment"_s ).toString();
1806 if ( textRotationAlignment == "viewport"_L1 )
1807 {
1809 }
1810 }
1811
1812 if ( labelSettings.placement == Qgis::LabelPlacement::Curved )
1813 {
1814 QPointF textOffset;
1815 QgsProperty textOffsetProperty;
1816 if ( jsonLayout.contains( u"text-offset"_s ) )
1817 {
1818 const QVariant jsonTextOffset = jsonLayout.value( u"text-offset"_s );
1819
1820 // units are ems!
1821 switch ( jsonTextOffset.userType() )
1822 {
1823 case QMetaType::Type::QVariantMap:
1824 textOffsetProperty = parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, !textSizeProperty ? textSize : 1.0, &textOffset );
1825 if ( !textSizeProperty )
1826 {
1827 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelDistance, u"abs(array_get(%1,1))-%2"_s.arg( textOffsetProperty.asExpression() ).arg( textSize ) );
1828 }
1829 else
1830 {
1831 ddLabelProperties.setProperty(
1833 u"with_variable('text_size',%2,abs(array_get(%1,1))*@text_size-@text_size)"_s.arg( textOffsetProperty.asExpression(), textSizeProperty.asExpression() )
1834 );
1835 }
1836 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LinePlacementOptions, u"if(array_get(%1,1)>0,'BL','AL')"_s.arg( textOffsetProperty.asExpression() ) );
1837 break;
1838
1839 case QMetaType::Type::QVariantList:
1840 case QMetaType::Type::QStringList:
1841 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize, jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1842 break;
1843
1844 default:
1845 context.pushWarning(
1846 QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) )
1847 );
1848 break;
1849 }
1850
1851 if ( !textOffset.isNull() )
1852 {
1853 labelSettings.distUnits = context.targetUnit();
1854 labelSettings.dist = std::abs( textOffset.y() ) - textSize;
1856 if ( textSizeProperty && !textOffsetProperty )
1857 {
1858 ddLabelProperties.setProperty(
1860 u"with_variable('text_size',%2,%1*@text_size-@text_size)"_s.arg( std::abs( textOffset.y() / textSize ) ).arg( textSizeProperty.asExpression() )
1861 );
1862 }
1863 }
1864 }
1865
1866 if ( textOffset.isNull() )
1867 {
1869 }
1870 }
1871 }
1872 }
1873
1874 if ( jsonLayout.contains( u"text-justify"_s ) )
1875 {
1876 const QVariant jsonTextJustify = jsonLayout.value( u"text-justify"_s );
1877
1878 // default is center
1879 QString textAlign = u"center"_s;
1880
1881 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 } };
1882
1883 switch ( jsonTextJustify.userType() )
1884 {
1885 case QMetaType::Type::QString:
1886 textAlign = jsonTextJustify.toString();
1887 break;
1888
1889 case QMetaType::Type::QVariantList:
1890 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextJustify.toList(), context, conversionMap, &textAlign ) ) );
1891 break;
1892
1893 case QMetaType::Type::QVariantMap:
1894 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextJustify.toMap(), context, conversionMap, &textAlign ) );
1895 break;
1896
1897 default:
1898 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-justify type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextJustify.userType() ) ) ) );
1899 break;
1900 }
1901
1902 if ( textAlign == "left"_L1 )
1904 else if ( textAlign == "right"_L1 )
1906 else if ( textAlign == "center"_L1 )
1908 else if ( textAlign == "follow"_L1 )
1910 }
1911 else
1912 {
1914 }
1915
1916 if ( labelSettings.placement == Qgis::LabelPlacement::OverPoint )
1917 {
1918 if ( jsonLayout.contains( u"text-anchor"_s ) )
1919 {
1920 const QVariant jsonTextAnchor = jsonLayout.value( u"text-anchor"_s );
1921 QString textAnchor;
1922
1923 const QVariantMap conversionMap {
1924 { u"center"_s, 4 },
1925 { u"left"_s, 5 },
1926 { u"right"_s, 3 },
1927 { u"top"_s, 7 },
1928 { u"bottom"_s, 1 },
1929 { u"top-left"_s, 8 },
1930 { u"top-right"_s, 6 },
1931 { u"bottom-left"_s, 2 },
1932 { u"bottom-right"_s, 0 },
1933 };
1934
1935 switch ( jsonTextAnchor.userType() )
1936 {
1937 case QMetaType::Type::QString:
1938 textAnchor = jsonTextAnchor.toString();
1939 break;
1940
1941 case QMetaType::Type::QVariantList:
1942 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextAnchor.toList(), context, conversionMap, &textAnchor ) ) );
1943 break;
1944
1945 case QMetaType::Type::QVariantMap:
1946 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextAnchor.toMap(), context, conversionMap, &textAnchor ) );
1947 break;
1948
1949 default:
1950 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-anchor type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextAnchor.userType() ) ) ) );
1951 break;
1952 }
1953
1954 if ( textAnchor == "center"_L1 )
1956 else if ( textAnchor == "left"_L1 )
1958 else if ( textAnchor == "right"_L1 )
1960 else if ( textAnchor == "top"_L1 )
1962 else if ( textAnchor == "bottom"_L1 )
1964 else if ( textAnchor == "top-left"_L1 )
1966 else if ( textAnchor == "top-right"_L1 )
1968 else if ( textAnchor == "bottom-left"_L1 )
1970 else if ( textAnchor == "bottom-right"_L1 )
1972 }
1973
1974 QPointF textOffset;
1975 if ( jsonLayout.contains( u"text-offset"_s ) )
1976 {
1977 const QVariant jsonTextOffset = jsonLayout.value( u"text-offset"_s );
1978
1979 // units are ems!
1980 switch ( jsonTextOffset.userType() )
1981 {
1982 case QMetaType::Type::QVariantMap:
1983 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetXY, parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, textSize, &textOffset ) );
1984 break;
1985
1986 case QMetaType::Type::QVariantList:
1987 case QMetaType::Type::QStringList:
1988 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize, jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1989 break;
1990
1991 default:
1992 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) ) );
1993 break;
1994 }
1995
1996 if ( !textOffset.isNull() )
1997 {
1998 labelSettings.offsetUnits = context.targetUnit();
1999 labelSettings.xOffset = textOffset.x();
2000 labelSettings.yOffset = textOffset.y();
2001 }
2002 }
2003 }
2004
2005 if ( jsonLayout.contains( u"icon-image"_s ) && ( labelSettings.placement == Qgis::LabelPlacement::Horizontal || labelSettings.placement == Qgis::LabelPlacement::Curved ) )
2006 {
2007 QSize spriteSize;
2008 QString spriteProperty, spriteSizeProperty;
2009 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( u"icon-image"_s ), context, spriteSize, spriteProperty, spriteSizeProperty );
2010 if ( !sprite.isEmpty() )
2011 {
2012 double size = 1.0;
2013 if ( jsonLayout.contains( u"icon-size"_s ) )
2014 {
2015 QgsProperty property;
2016 const QVariant jsonIconSize = jsonLayout.value( u"icon-size"_s );
2017 switch ( jsonIconSize.userType() )
2018 {
2019 case QMetaType::Type::Int:
2020 case QMetaType::Type::LongLong:
2021 case QMetaType::Type::Double:
2022 {
2023 size = jsonIconSize.toDouble();
2024 if ( !spriteSizeProperty.isEmpty() )
2025 {
2026 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::ShapeSizeX, QgsProperty::fromExpression( u"with_variable('marker_size',%1,%2*@marker_size)"_s.arg( spriteSizeProperty ).arg( size ) ) );
2027 }
2028 break;
2029 }
2030
2031 case QMetaType::Type::QVariantMap:
2032 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2033 break;
2034
2035 case QMetaType::Type::QVariantList:
2036 case QMetaType::Type::QStringList:
2037 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2038 break;
2039 default:
2040 context.pushWarning(
2041 QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) )
2042 );
2043 break;
2044 }
2045
2046 if ( !property.expressionString().isEmpty() )
2047 {
2048 if ( !spriteSizeProperty.isEmpty() )
2049 {
2050 ddLabelProperties
2051 .setProperty( QgsPalLayerSettings::Property::ShapeSizeX, QgsProperty::fromExpression( u"with_variable('marker_size',%1,(%2)*@marker_size)"_s.arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2052 }
2053 else
2054 {
2055 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::ShapeSizeX, QgsProperty::fromExpression( u"(%2)*%1"_s.arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2056 }
2057 }
2058 }
2059
2061 markerLayer->setPath( sprite );
2062 markerLayer->setSize( spriteSize.width() );
2063 markerLayer->setSizeUnit( context.targetUnit() );
2064
2065 if ( !spriteProperty.isEmpty() )
2066 {
2067 QgsPropertyCollection markerDdProperties;
2068 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2069 markerLayer->setDataDefinedProperties( markerDdProperties );
2070 }
2071
2072 QgsTextBackgroundSettings backgroundSettings;
2073 backgroundSettings.setEnabled( true );
2075 backgroundSettings.setSize( spriteSize * size );
2076 backgroundSettings.setSizeUnit( context.targetUnit() );
2078 backgroundSettings.setMarkerSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2079 format.setBackground( backgroundSettings );
2080 }
2081 }
2082
2083#if 0
2084 // TODO: re-enable when the cost of label duplicate removal within distance is more reasonable
2085 if ( jsonLayout.contains( u"symbol-spacing"_s ) )
2086 {
2087 double spacing;
2088 const QVariant jsonSpacing = jsonLayout.value( u"symbol-spacing"_s );
2089
2090 // main checkbox in labeling GUI
2091 QgsLabelThinningSettings thinningSettings = labelSettings.thinningSettings();
2092 thinningSettings.setAllowDuplicateRemoval( true );
2093 thinningSettings.setMinimumDistanceToDuplicateUnit( context.targetUnit() );
2094 labelSettings.setThinningSettings( thinningSettings );
2095
2096 QgsProperty spacingProp;
2097
2098 switch ( jsonSpacing.userType() )
2099 {
2100 case QMetaType::Type::Int:
2101 case QMetaType::Type::LongLong:
2102 case QMetaType::Type::Double:
2103 {
2104 spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
2105 spacingProp = QgsProperty::fromValue( spacing );
2106 break;
2107 }
2108
2109 case QMetaType::Type::QVariantMap:
2110 {
2111 spacingProp = parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing );
2112 break;
2113 }
2114
2115 case QMetaType::Type::QVariantList:
2116 case QMetaType::Type::QStringList:
2117 {
2118 spacingProp = parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing );
2119 break;
2120 }
2121
2122 default:
2123 context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonSpacing.userType() ) ) ) );
2124 break;
2125 }
2126
2127 spacingProp.setActive( true );
2129 }
2130#endif
2131
2132 if ( textSize >= 0 )
2133 {
2134 // TODO -- this probably needs revisiting -- it was copied from the MapTiler code, but may be wrong...
2135 labelSettings.priority = std::min( textSize / ( context.pixelSizeConversionFactor() * 3 ), 10.0 );
2136 }
2137
2138 labelSettings.setFormat( format );
2139
2140 // use a low obstacle weight for layers by default -- we'd rather have more labels for these layers, even if placement isn't ideal
2141 labelSettings.obstacleSettings().setFactor( 0.1 );
2142
2143 labelSettings.setDataDefinedProperties( ddLabelProperties );
2144
2145 labelingStyle.setGeometryType( geometryType );
2146 labelingStyle.setLabelSettings( labelSettings );
2147
2148 hasLabeling = true;
2149
2150 hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
2151}
2152
2154{
2155 if ( !jsonLayer.contains( u"layout"_s ) )
2156 {
2157 context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
2158 return false;
2159 }
2160 const QVariantMap jsonLayout = jsonLayer.value( u"layout"_s ).toMap();
2161
2162 if ( jsonLayout.value( u"symbol-placement"_s ).toString() == "line"_L1 && !jsonLayout.contains( u"text-field"_s ) )
2163 {
2164 QgsPropertyCollection ddProperties;
2165
2166 double spacing = -1.0;
2167 if ( jsonLayout.contains( u"symbol-spacing"_s ) )
2168 {
2169 const QVariant jsonSpacing = jsonLayout.value( u"symbol-spacing"_s );
2170 switch ( jsonSpacing.userType() )
2171 {
2172 case QMetaType::Type::Int:
2173 case QMetaType::Type::LongLong:
2174 case QMetaType::Type::Double:
2175 spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
2176 break;
2177
2178 case QMetaType::Type::QVariantMap:
2179 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing ) );
2180 break;
2181
2182 case QMetaType::Type::QVariantList:
2183 case QMetaType::Type::QStringList:
2184 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing ) );
2185 break;
2186
2187 default:
2188 context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonSpacing.userType() ) ) ) );
2189 break;
2190 }
2191 }
2192 else
2193 {
2194 // defaults to 250
2195 spacing = 250 * context.pixelSizeConversionFactor();
2196 }
2197
2198 bool rotateMarkers = true;
2199 if ( jsonLayout.contains( u"icon-rotation-alignment"_s ) )
2200 {
2201 const QString alignment = jsonLayout.value( u"icon-rotation-alignment"_s ).toString();
2202 if ( alignment == "map"_L1 || alignment == "auto"_L1 )
2203 {
2204 rotateMarkers = true;
2205 }
2206 else if ( alignment == "viewport"_L1 )
2207 {
2208 rotateMarkers = false;
2209 }
2210 }
2211
2212 QgsPropertyCollection markerDdProperties;
2213 double rotation = 0.0;
2214 if ( jsonLayout.contains( u"icon-rotate"_s ) )
2215 {
2216 const QVariant jsonIconRotate = jsonLayout.value( u"icon-rotate"_s );
2217 switch ( jsonIconRotate.userType() )
2218 {
2219 case QMetaType::Type::Int:
2220 case QMetaType::Type::LongLong:
2221 case QMetaType::Type::Double:
2222 rotation = jsonIconRotate.toDouble();
2223 break;
2224
2225 case QMetaType::Type::QVariantMap:
2226 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2227 break;
2228
2229 case QMetaType::Type::QVariantList:
2230 case QMetaType::Type::QStringList:
2231 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2232 break;
2233
2234 default:
2235 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) ) );
2236 break;
2237 }
2238 }
2239
2240 QgsMarkerLineSymbolLayer *lineSymbol = new QgsMarkerLineSymbolLayer( rotateMarkers, spacing > 0 ? spacing : 1 );
2241 lineSymbol->setOutputUnit( context.targetUnit() );
2242 lineSymbol->setDataDefinedProperties( ddProperties );
2243 if ( spacing < 1 )
2244 {
2245 // if spacing isn't specified, it's a central point marker only
2247 }
2248
2250 QSize spriteSize;
2251 QString spriteProperty, spriteSizeProperty;
2252 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( u"icon-image"_s ), context, spriteSize, spriteProperty, spriteSizeProperty );
2253 if ( !sprite.isNull() )
2254 {
2255 markerLayer->setPath( sprite );
2256 markerLayer->setSize( spriteSize.width() );
2257 markerLayer->setSizeUnit( context.targetUnit() );
2258
2259 if ( !spriteProperty.isEmpty() )
2260 {
2261 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2262 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2263 }
2264 }
2265
2266 if ( jsonLayout.contains( u"icon-size"_s ) )
2267 {
2268 const QVariant jsonIconSize = jsonLayout.value( u"icon-size"_s );
2269 double size = 1.0;
2270 QgsProperty property;
2271 switch ( jsonIconSize.userType() )
2272 {
2273 case QMetaType::Type::Int:
2274 case QMetaType::Type::LongLong:
2275 case QMetaType::Type::Double:
2276 {
2277 size = jsonIconSize.toDouble();
2278 if ( !spriteSizeProperty.isEmpty() )
2279 {
2280 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"with_variable('marker_size',%1,%2*@marker_size)"_s.arg( spriteSizeProperty ).arg( size ) ) );
2281 }
2282 break;
2283 }
2284
2285 case QMetaType::Type::QVariantMap:
2286 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2287 break;
2288
2289 case QMetaType::Type::QVariantList:
2290 case QMetaType::Type::QStringList:
2291 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2292 break;
2293 default:
2294 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
2295 break;
2296 }
2297 markerLayer->setSize( size * spriteSize.width() );
2298 if ( !property.expressionString().isEmpty() )
2299 {
2300 if ( !spriteSizeProperty.isEmpty() )
2301 {
2302 markerDdProperties
2303 .setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"with_variable('marker_size',%1,(%2)*@marker_size)"_s.arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2304 }
2305 else
2306 {
2307 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"(%2)*%1"_s.arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2308 }
2309 }
2310 }
2311
2312 markerLayer->setDataDefinedProperties( markerDdProperties );
2313 markerLayer->setAngle( rotation );
2314 lineSymbol->setSubSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2315
2316 std::unique_ptr< QgsSymbol > symbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << lineSymbol );
2317
2318 // set render units
2319 symbol->setOutputUnit( context.targetUnit() );
2320 lineSymbol->setOutputUnit( context.targetUnit() );
2321
2323 rendererStyle.setSymbol( symbol.release() );
2324 return true;
2325 }
2326 else if ( jsonLayout.contains( u"icon-image"_s ) )
2327 {
2328 const QVariantMap jsonPaint = jsonLayer.value( u"paint"_s ).toMap();
2329
2330 QSize spriteSize;
2331 QString spriteProperty, spriteSizeProperty;
2332 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( u"icon-image"_s ), context, spriteSize, spriteProperty, spriteSizeProperty );
2333 if ( !sprite.isEmpty() || !spriteProperty.isEmpty() )
2334 {
2336 rasterMarker->setPath( sprite );
2337 rasterMarker->setSize( spriteSize.width() );
2338 rasterMarker->setSizeUnit( context.targetUnit() );
2339
2340 QgsPropertyCollection markerDdProperties;
2341 if ( !spriteProperty.isEmpty() )
2342 {
2343 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2344 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2345 }
2346
2347 if ( jsonLayout.contains( u"icon-size"_s ) )
2348 {
2349 const QVariant jsonIconSize = jsonLayout.value( u"icon-size"_s );
2350 double size = 1.0;
2351 QgsProperty property;
2352 switch ( jsonIconSize.userType() )
2353 {
2354 case QMetaType::Type::Int:
2355 case QMetaType::Type::LongLong:
2356 case QMetaType::Type::Double:
2357 {
2358 size = jsonIconSize.toDouble();
2359 if ( !spriteSizeProperty.isEmpty() )
2360 {
2361 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"with_variable('marker_size',%1,%2*@marker_size)"_s.arg( spriteSizeProperty ).arg( size ) ) );
2362 }
2363 break;
2364 }
2365
2366 case QMetaType::Type::QVariantMap:
2367 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2368 break;
2369
2370 case QMetaType::Type::QVariantList:
2371 case QMetaType::Type::QStringList:
2372 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2373 break;
2374 default:
2375 context.pushWarning(
2376 QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) )
2377 );
2378 break;
2379 }
2380 rasterMarker->setSize( size * spriteSize.width() );
2381 if ( !property.expressionString().isEmpty() )
2382 {
2383 if ( !spriteSizeProperty.isEmpty() )
2384 {
2385 markerDdProperties
2386 .setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"with_variable('marker_size',%1,(%2)*@marker_size)"_s.arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2387 }
2388 else
2389 {
2390 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( u"(%2)*%1"_s.arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2391 }
2392 }
2393 }
2394
2395 double rotation = 0.0;
2396 if ( jsonLayout.contains( u"icon-rotate"_s ) )
2397 {
2398 const QVariant jsonIconRotate = jsonLayout.value( u"icon-rotate"_s );
2399 switch ( jsonIconRotate.userType() )
2400 {
2401 case QMetaType::Type::Int:
2402 case QMetaType::Type::LongLong:
2403 case QMetaType::Type::Double:
2404 rotation = jsonIconRotate.toDouble();
2405 break;
2406
2407 case QMetaType::Type::QVariantMap:
2408 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2409 break;
2410
2411 case QMetaType::Type::QVariantList:
2412 case QMetaType::Type::QStringList:
2413 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2414 break;
2415
2416 default:
2417 context.pushWarning(
2418 QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) )
2419 );
2420 break;
2421 }
2422 }
2423
2424 double iconOpacity = -1.0;
2425 if ( jsonPaint.contains( u"icon-opacity"_s ) )
2426 {
2427 const QVariant jsonIconOpacity = jsonPaint.value( u"icon-opacity"_s );
2428 switch ( jsonIconOpacity.userType() )
2429 {
2430 case QMetaType::Type::Int:
2431 case QMetaType::Type::LongLong:
2432 case QMetaType::Type::Double:
2433 iconOpacity = jsonIconOpacity.toDouble();
2434 break;
2435
2436 case QMetaType::Type::QVariantMap:
2437 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseInterpolateByZoom( jsonIconOpacity.toMap(), context, 100, &iconOpacity ) );
2438 break;
2439
2440 case QMetaType::Type::QVariantList:
2441 case QMetaType::Type::QStringList:
2442 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseValueList( jsonIconOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &iconOpacity ) );
2443 break;
2444
2445 default:
2446 context.pushWarning(
2447 QObject::tr( "%1: Skipping unsupported icon-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconOpacity.userType() ) ) )
2448 );
2449 break;
2450 }
2451 }
2452
2453 rasterMarker->setDataDefinedProperties( markerDdProperties );
2454 rasterMarker->setAngle( rotation );
2455 if ( iconOpacity >= 0 )
2456 rasterMarker->setOpacity( iconOpacity );
2457
2458 QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << rasterMarker );
2459 rendererStyle.setSymbol( markerSymbol );
2461 return true;
2462 }
2463 }
2464
2465 return false;
2466}
2467
2469{
2470 const double base = json.value( u"base"_s, u"1"_s ).toDouble();
2471 const QVariantList stops = json.value( u"stops"_s ).toList();
2472 if ( stops.empty() )
2473 return QgsProperty();
2474
2475 QString caseString = u"CASE "_s;
2476 const QString colorComponent( "color_part(%1,'%2')" );
2477
2478 for ( int i = 0; i < stops.length() - 1; ++i )
2479 {
2480 // step bottom zoom
2481 const QString bz = stops.at( i ).toList().value( 0 ).toString();
2482 // step top zoom
2483 const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
2484
2485 const QVariant bcVariant = stops.at( i ).toList().value( 1 );
2486 const QVariant tcVariant = stops.at( i + 1 ).toList().value( 1 );
2487
2488 const QColor bottomColor = parseColor( bcVariant.toString(), context );
2489 const QColor topColor = parseColor( tcVariant.toString(), context );
2490
2491 if ( i == 0 && bottomColor.isValid() )
2492 {
2493 int bcHue;
2494 int bcSat;
2495 int bcLight;
2496 int bcAlpha;
2497 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2498 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 );
2499 }
2500
2501 if ( bottomColor.isValid() && topColor.isValid() )
2502 {
2503 int bcHue;
2504 int bcSat;
2505 int bcLight;
2506 int bcAlpha;
2507 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2508 int tcHue;
2509 int tcSat;
2510 int tcLight;
2511 int tcAlpha;
2512 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2513 caseString += QStringLiteral(
2514 "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2515 "%3, %4, %5, %6) "
2516 )
2517 .arg(
2518 bz,
2519 tz,
2521 bz.toDouble(),
2522 tz.toDouble(),
2523 bcHue,
2524 tcHue,
2525 base,
2526 1,
2527 json.value( u"x1"_s ).toDouble(),
2528 json.value( u"y1"_s ).toDouble(),
2529 json.value( u"x2"_s ).toDouble(),
2530 json.value( u"y2"_s ).toDouble(),
2531 type,
2532 &context
2533 ),
2535 bz.toDouble(),
2536 tz.toDouble(),
2537 bcSat,
2538 tcSat,
2539 base,
2540 1,
2541 json.value( u"x1"_s ).toDouble(),
2542 json.value( u"y1"_s ).toDouble(),
2543 json.value( u"x2"_s ).toDouble(),
2544 json.value( u"y2"_s ).toDouble(),
2545 type,
2546 &context
2547 ),
2549 bz.toDouble(),
2550 tz.toDouble(),
2551 bcLight,
2552 tcLight,
2553 base,
2554 1,
2555 json.value( u"x1"_s ).toDouble(),
2556 json.value( u"y1"_s ).toDouble(),
2557 json.value( u"x2"_s ).toDouble(),
2558 json.value( u"y2"_s ).toDouble(),
2559 type,
2560 &context
2561 ),
2563 bz.toDouble(),
2564 tz.toDouble(),
2565 bcAlpha,
2566 tcAlpha,
2567 base,
2568 1,
2569 json.value( u"x1"_s ).toDouble(),
2570 json.value( u"y1"_s ).toDouble(),
2571 json.value( u"x2"_s ).toDouble(),
2572 json.value( u"y2"_s ).toDouble(),
2573 type,
2574 &context
2575 )
2576 );
2577 }
2578 else
2579 {
2580 const QString bottomColorExpr = parseColorExpression( bcVariant, context );
2581 const QString topColorExpr = parseColorExpression( tcVariant, context );
2582
2583 caseString += QStringLiteral(
2584 "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2585 "%3, %4, %5, %6) "
2586 )
2587 .arg(
2588 bz,
2589 tz,
2591 bz.toDouble(),
2592 tz.toDouble(),
2593 colorComponent.arg( bottomColorExpr ).arg( "hsl_hue" ),
2594 colorComponent.arg( topColorExpr ).arg( "hsl_hue" ),
2595 base,
2596 1,
2597 json.value( u"x1"_s ).toDouble(),
2598 json.value( u"y1"_s ).toDouble(),
2599 json.value( u"x2"_s ).toDouble(),
2600 json.value( u"y2"_s ).toDouble(),
2601 type,
2602 &context
2603 ),
2605 bz.toDouble(),
2606 tz.toDouble(),
2607 colorComponent.arg( bottomColorExpr ).arg( "hsl_saturation" ),
2608 colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ),
2609 base,
2610 1,
2611 json.value( u"x1"_s ).toDouble(),
2612 json.value( u"y1"_s ).toDouble(),
2613 json.value( u"x2"_s ).toDouble(),
2614 json.value( u"y2"_s ).toDouble(),
2615 type,
2616 &context
2617 ),
2619 bz.toDouble(),
2620 tz.toDouble(),
2621 colorComponent.arg( bottomColorExpr ).arg( "lightness" ),
2622 colorComponent.arg( topColorExpr ).arg( "lightness" ),
2623 base,
2624 1,
2625 json.value( u"x1"_s ).toDouble(),
2626 json.value( u"y1"_s ).toDouble(),
2627 json.value( u"x2"_s ).toDouble(),
2628 json.value( u"y2"_s ).toDouble(),
2629 type,
2630 &context
2631 ),
2633 bz.toDouble(),
2634 tz.toDouble(),
2635 colorComponent.arg( bottomColorExpr ).arg( "alpha" ),
2636 colorComponent.arg( topColorExpr ).arg( "alpha" ),
2637 base,
2638 1,
2639 json.value( u"x1"_s ).toDouble(),
2640 json.value( u"y1"_s ).toDouble(),
2641 json.value( u"x2"_s ).toDouble(),
2642 json.value( u"y2"_s ).toDouble(),
2643 type,
2644 &context
2645 )
2646 );
2647 }
2648 }
2649
2650 // top color
2651 const QString tz = stops.last().toList().value( 0 ).toString();
2652 const QVariant tcVariant = stops.last().toList().value( 1 );
2653 QColor topColor;
2654 if ( tcVariant.userType() == QMetaType::Type::QString )
2655 {
2656 topColor = parseColor( tcVariant, context );
2657 if ( topColor.isValid() )
2658 {
2659 int tcHue;
2660 int tcSat;
2661 int tcLight;
2662 int tcAlpha;
2663 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2664 caseString += QStringLiteral(
2665 "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2666 "ELSE color_hsla(%2, %3, %4, %5) END"
2667 )
2668 .arg( tz )
2669 .arg( tcHue )
2670 .arg( tcSat )
2671 .arg( tcLight )
2672 .arg( tcAlpha );
2673 }
2674 }
2675 else if ( tcVariant.userType() == QMetaType::QVariantList )
2676 {
2677 const QString topColorExpr = parseColorExpression( tcVariant, context );
2678
2679 caseString += QStringLiteral(
2680 "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2681 "ELSE color_hsla(%2, %3, %4, %5) END"
2682 )
2683 .arg( tz )
2684 .arg( colorComponent.arg( topColorExpr ).arg( "hsl_hue" ) )
2685 .arg( colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ) )
2686 .arg( colorComponent.arg( topColorExpr ).arg( "lightness" ) )
2687 .arg( colorComponent.arg( topColorExpr ).arg( "alpha" ) );
2688 }
2689
2690 if ( !stops.empty() && defaultColor )
2691 *defaultColor = parseColor( stops.value( 0 ).toList().value( 1 ).toString(), context );
2692
2693 return QgsProperty::fromExpression( caseString );
2694}
2695
2696QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, double *defaultNumber, InterpolationType type )
2697{
2698 const double base = json.value( u"base"_s, u"1"_s ).toDouble();
2699 const QVariantList stops = json.value( u"stops"_s ).toList();
2700 if ( stops.empty() )
2701 return QgsProperty();
2702
2703 QString scaleExpression;
2704 if ( stops.size() <= 2 )
2705 {
2706 scaleExpression = interpolateExpression(
2707 stops.value( 0 ).toList().value( 0 ).toDouble(), // zoomMin
2708 stops.last().toList().value( 0 ).toDouble(), // zoomMax
2709 stops.value( 0 ).toList().value( 1 ), // valueMin
2710 stops.last().toList().value( 1 ), // valueMax
2711 base,
2712 multiplier,
2713 json.value( u"x1"_s ).toDouble(),
2714 json.value( u"y1"_s ).toDouble(),
2715 json.value( u"x2"_s ).toDouble(),
2716 json.value( u"y2"_s ).toDouble(),
2717 type,
2718 &context
2719 );
2720 }
2721 else
2722 {
2723 scaleExpression
2724 = 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() );
2725 }
2726
2727 if ( !stops.empty() && defaultNumber )
2728 *defaultNumber = stops.value( 0 ).toList().value( 1 ).toDouble() * multiplier;
2729
2730 return QgsProperty::fromExpression( scaleExpression );
2731}
2732
2734{
2736 if ( contextPtr )
2737 {
2738 context = *contextPtr;
2739 }
2740 const double base = json.value( u"base"_s, u"1"_s ).toDouble();
2741 const QVariantList stops = json.value( u"stops"_s ).toList();
2742 if ( stops.empty() )
2743 return QgsProperty();
2744
2745 QString scaleExpression;
2746 if ( stops.length() <= 2 )
2747 {
2748 const QVariant bv = stops.value( 0 ).toList().value( 1 );
2749 const QVariant tv = stops.last().toList().value( 1 );
2750 double bottom = 0.0;
2751 double top = 0.0;
2752 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2753 scaleExpression = u"set_color_part(@symbol_color, 'alpha', %1)"_s.arg( interpolateExpression(
2754 stops.value( 0 ).toList().value( 0 ).toDouble(),
2755 stops.last().toList().value( 0 ).toDouble(),
2756 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2757 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2758 base,
2759 1,
2760 json.value( u"x1"_s ).toDouble(),
2761 json.value( u"y1"_s ).toDouble(),
2762 json.value( u"x2"_s ).toDouble(),
2763 json.value( u"y2"_s ).toDouble(),
2764 type,
2765 &context
2766 ) );
2767 }
2768 else
2769 {
2770 scaleExpression
2771 = 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() );
2772 }
2773 return QgsProperty::fromExpression( scaleExpression );
2774}
2775
2777 double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context, InterpolationType type, double x1, double y1, double x2, double y2
2778)
2779{
2780 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() )
2781 .arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );
2782
2783 for ( int i = 0; i < stops.size() - 1; ++i )
2784 {
2785 const QVariant bv = stops.value( i ).toList().value( 1 );
2786 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2787 double bottom = 0.0;
2788 double top = 0.0;
2789 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2790
2791 caseString += QStringLiteral(
2792 " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
2793 "THEN set_color_part(@symbol_color, 'alpha', %3)"
2794 )
2795 .arg(
2796 stops.value( i ).toList().value( 0 ).toString(),
2797 stops.value( i + 1 ).toList().value( 0 ).toString(),
2799 stops.value( i ).toList().value( 0 ).toDouble(),
2800 stops.value( i + 1 ).toList().value( 0 ).toDouble(),
2801 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2802 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2803 base,
2804 1,
2805 x1,
2806 y1,
2807 x2,
2808 y2,
2809 type,
2810 &context
2811 )
2812 );
2813 }
2814
2815
2816 bool numeric = false;
2817 const QVariant vv = stops.last().toList().value( 1 );
2818 double dv = vv.toDouble( &numeric );
2819
2820 caseString += QStringLiteral(
2821 " WHEN @vector_tile_zoom >= %1 "
2822 "THEN set_color_part(@symbol_color, 'alpha', %2) END"
2823 )
2824 .arg( stops.last().toList().value( 0 ).toString(), numeric ? QString::number( dv * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( vv, context ) ).arg( maxOpacity ) );
2825 return caseString;
2826}
2827
2828QgsProperty QgsMapBoxGlStyleConverter::parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, QPointF *defaultPoint, InterpolationType type )
2829{
2830 const double base = json.value( u"base"_s, u"1"_s ).toDouble();
2831 const QVariantList stops = json.value( u"stops"_s ).toList();
2832 if ( stops.empty() )
2833 return QgsProperty();
2834
2835 QString scaleExpression;
2836 if ( stops.size() <= 2 )
2837 {
2838 scaleExpression = u"array(%1,%2)"_s.arg(
2840 stops.value( 0 ).toList().value( 0 ).toDouble(),
2841 stops.last().toList().value( 0 ).toDouble(),
2842 stops.value( 0 ).toList().value( 1 ).toList().value( 0 ),
2843 stops.last().toList().value( 1 ).toList().value( 0 ),
2844 base,
2845 multiplier,
2846 json.value( u"x1"_s ).toDouble(),
2847 json.value( u"y1"_s ).toDouble(),
2848 json.value( u"x2"_s ).toDouble(),
2849 json.value( u"y2"_s ).toDouble(),
2850 type,
2851 &context
2852 ),
2854 stops.value( 0 ).toList().value( 0 ).toDouble(),
2855 stops.last().toList().value( 0 ).toDouble(),
2856 stops.value( 0 ).toList().value( 1 ).toList().value( 1 ),
2857 stops.last().toList().value( 1 ).toList().value( 1 ),
2858 base,
2859 multiplier,
2860 json.value( u"x1"_s ).toDouble(),
2861 json.value( u"y1"_s ).toDouble(),
2862 json.value( u"x2"_s ).toDouble(),
2863 json.value( u"y2"_s ).toDouble(),
2864 type,
2865 &context
2866 )
2867 );
2868 }
2869 else
2870 {
2871 scaleExpression
2872 = 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() );
2873 }
2874
2875 if ( !stops.empty() && defaultPoint )
2876 *defaultPoint = QPointF( stops.value( 0 ).toList().value( 1 ).toList().value( 0 ).toDouble() * multiplier, stops.value( 0 ).toList().value( 1 ).toList().value( 1 ).toDouble() * multiplier );
2877
2878 return QgsProperty::fromExpression( scaleExpression );
2879}
2880
2881QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateStringByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
2882{
2883 const QVariantList stops = json.value( u"stops"_s ).toList();
2884 if ( stops.empty() )
2885 return QgsProperty();
2886
2887 const QString scaleExpression = parseStringStops( stops, context, conversionMap, defaultString );
2888
2889 return QgsProperty::fromExpression( scaleExpression );
2890}
2891
2893 double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier, InterpolationType type, double x1, double y1, double x2, double y2
2894)
2895{
2896 QString caseString = u"CASE "_s;
2897
2898 for ( int i = 0; i < stops.length() - 1; ++i )
2899 {
2900 // bottom zoom and value
2901 const QVariant bz = stops.value( i ).toList().value( 0 );
2902 const QVariant bv = stops.value( i ).toList().value( 1 );
2903 if ( bv.userType() != QMetaType::Type::QVariantList && bv.userType() != QMetaType::Type::QStringList )
2904 {
2905 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( bz.userType() ) ) ) );
2906 return QString();
2907 }
2908
2909 // top zoom and value
2910 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2911 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2912 if ( tv.userType() != QMetaType::Type::QVariantList && tv.userType() != QMetaType::Type::QStringList )
2913 {
2914 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( tz.userType() ) ) ) );
2915 return QString();
2916 }
2917
2918 caseString += QStringLiteral(
2919 "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2920 "THEN array(%3,%4)"
2921 )
2922 .arg(
2923 bz.toString(),
2924 tz.toString(),
2925 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ), tv.toList().value( 0 ), base, multiplier, x1, y1, x2, y2, type, &context ),
2926 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ), tv.toList().value( 1 ), base, multiplier, x1, y1, x2, y2, type, &context )
2927 );
2928 }
2929 caseString += "END"_L1;
2930 return caseString;
2931}
2932
2933QString QgsMapBoxGlStyleConverter::parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &, double multiplier )
2934{
2935 if ( stops.length() < 2 )
2936 return QString();
2937
2938 QString caseString = u"CASE"_s;
2939
2940 for ( int i = 0; i < stops.length(); ++i )
2941 {
2942 caseString += " WHEN "_L1;
2943 QStringList conditions;
2944 if ( i > 0 )
2945 {
2946 const QVariant bottomZoom = stops.value( i ).toList().value( 0 );
2947 conditions << u"@vector_tile_zoom > %1"_s.arg( bottomZoom.toString() );
2948 }
2949 if ( i < stops.length() - 1 )
2950 {
2951 const QVariant topZoom = stops.value( i + 1 ).toList().value( 0 );
2952 conditions << u"@vector_tile_zoom <= %1"_s.arg( topZoom.toString() );
2953 }
2954
2955 const QVariantList values = stops.value( i ).toList().value( 1 ).toList();
2956 QStringList valuesFixed;
2957 bool ok = false;
2958 for ( const QVariant &value : values )
2959 {
2960 const double number = value.toDouble( &ok );
2961 if ( ok )
2962 valuesFixed << QString::number( number * multiplier );
2963 }
2964
2965 // top zoom and value
2966 caseString += u"%1 THEN array(%3)"_s.arg( conditions.join( " AND "_L1 ), valuesFixed.join( ',' ) );
2967 }
2968 caseString += " END"_L1;
2969 return caseString;
2970}
2971
2973 double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context, InterpolationType type, double x1, double y1, double x2, double y2
2974)
2975{
2976 QString caseString = u"CASE "_s;
2977
2978 for ( int i = 0; i < stops.length() - 1; ++i )
2979 {
2980 // bottom zoom and value
2981 const QVariant bz = stops.value( i ).toList().value( 0 );
2982 const QVariant bv = stops.value( i ).toList().value( 1 );
2983 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2984 {
2985 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2986 return QString();
2987 }
2988
2989 // top zoom and value
2990 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2991 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2992 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2993 {
2994 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2995 return QString();
2996 }
2997
2998 const QString lowerComparator = i == 0 ? u">="_s : u">"_s;
2999
3000 caseString += QStringLiteral(
3001 "WHEN @vector_tile_zoom %1 %2 AND @vector_tile_zoom <= %3 "
3002 "THEN %4 "
3003 )
3004 .arg( lowerComparator, bz.toString(), tz.toString(), interpolateExpression( bz.toDouble(), tz.toDouble(), bv, tv, base, multiplier, x1, y1, x2, y2, type, &context ) );
3005 }
3006
3007 const QVariant z = stops.last().toList().value( 0 );
3008 const QVariant v = stops.last().toList().value( 1 );
3009 QString vStr = v.toString();
3010 if ( ( QMetaType::Type ) v.userType() == QMetaType::QVariantList )
3011 {
3012 vStr = parseExpression( v.toList(), context );
3013 caseString += QStringLiteral(
3014 "WHEN @vector_tile_zoom > %1 "
3015 "THEN ( ( %2 ) * %3 ) END"
3016 )
3017 .arg( z.toString() )
3018 .arg( vStr )
3019 .arg( multiplier );
3020 }
3021 else
3022 {
3023 caseString += QStringLiteral(
3024 "WHEN @vector_tile_zoom > %1 "
3025 "THEN %2 END"
3026 )
3027 .arg( z.toString() )
3028 .arg( v.toDouble() * multiplier );
3029 }
3030
3031 return caseString;
3032}
3033
3034QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
3035{
3036 QString caseString = u"CASE "_s;
3037
3038 for ( int i = 0; i < stops.length() - 1; ++i )
3039 {
3040 // bottom zoom and value
3041 const QVariant bz = stops.value( i ).toList().value( 0 );
3042 const QString bv = stops.value( i ).toList().value( 1 ).toString();
3043 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
3044 {
3045 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
3046 return QString();
3047 }
3048
3049 // top zoom
3050 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
3051 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
3052 {
3053 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
3054 return QString();
3055 }
3056
3057 caseString += QStringLiteral(
3058 "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
3059 "THEN %3 "
3060 )
3061 .arg( bz.toString(), tz.toString(), QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
3062 }
3063 caseString += u"ELSE %1 END"_s.arg( QgsExpression::quotedValue( conversionMap.value( stops.constLast().toList().value( 1 ).toString(), stops.constLast().toList().value( 1 ) ) ) );
3064 if ( defaultString )
3065 *defaultString = stops.constLast().toList().value( 1 ).toString();
3066 return caseString;
3067}
3068
3070{
3071 QString caseString = u"CASE "_s;
3072
3073 bool isExpression = false;
3074 for ( int i = 0; i < stops.length() - 1; ++i )
3075 {
3076 // bottom zoom and value
3077 const QVariant bz = stops.value( i ).toList().value( 0 );
3078 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
3079 {
3080 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
3081 return QString();
3082 }
3083
3084 // top zoom
3085 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
3086 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
3087 {
3088 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
3089 return QString();
3090 }
3091
3092 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
3093 if ( fieldPart.isEmpty() )
3094 fieldPart = u"''"_s;
3095 else if ( !isExpression )
3096 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
3097
3098 caseString += QStringLiteral(
3099 "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom < %2 "
3100 "THEN %3 "
3101 )
3102 .arg( bz.toString(), tz.toString(), fieldPart );
3103 }
3104
3105 {
3106 const QVariant bz = stops.constLast().toList().value( 0 );
3107 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
3108 {
3109 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
3110 return QString();
3111 }
3112
3113 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
3114 if ( fieldPart.isEmpty() )
3115 fieldPart = u"''"_s;
3116 else if ( !isExpression )
3117 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
3118
3119 caseString += QStringLiteral(
3120 "WHEN @vector_tile_zoom >= %1 "
3121 "THEN %3 "
3122 )
3123 .arg( bz.toString(), fieldPart );
3124 }
3125
3126 QString defaultPart = processLabelField( stops.constFirst().toList().value( 1 ).toString(), isExpression );
3127 if ( defaultPart.isEmpty() )
3128 defaultPart = u"''"_s;
3129 else if ( !isExpression )
3130 defaultPart = QgsExpression::quotedColumnRef( defaultPart );
3131 caseString += u"ELSE %1 END"_s.arg( defaultPart );
3132
3133 return caseString;
3134}
3135
3137 const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber
3138)
3139{
3140 const QString method = json.value( 0 ).toString();
3141 if ( method == "interpolate"_L1 )
3142 {
3143 return parseInterpolateListByZoom( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
3144 }
3145 else if ( method == "match"_L1 )
3146 {
3147 return parseMatchList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
3148 }
3149 else if ( method == "step"_L1 )
3150 {
3151 return parseStepList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
3152 }
3153 else
3154 {
3155 return QgsProperty::fromExpression( parseExpression( json, context, type == PropertyType::Color ) );
3156 }
3157}
3158
3160 const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber
3161)
3162{
3163 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
3164 if ( attribute.isEmpty() )
3165 {
3166 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
3167 return QgsProperty();
3168 }
3169
3170 QString caseString = u"CASE "_s;
3171
3172 for ( int i = 2; i < json.length() - 1; i += 2 )
3173 {
3174 QVariantList keys;
3175 QVariant variantKeys = json.value( i );
3176 if ( variantKeys.userType() == QMetaType::Type::QVariantList || variantKeys.userType() == QMetaType::Type::QStringList )
3177 keys = variantKeys.toList();
3178 else
3179 keys = { variantKeys };
3180
3181 QStringList matchString;
3182 for ( const QVariant &key : keys )
3183 {
3184 matchString << QgsExpression::quotedValue( key );
3185 }
3186
3187 const QVariant value = json.value( i + 1 );
3188
3189 QString valueString;
3190 switch ( type )
3191 {
3193 {
3194 if ( value.userType() == QMetaType::Type::QVariantList || value.userType() == QMetaType::Type::QStringList )
3195 {
3196 valueString = parseMatchList( value.toList(), PropertyType::Color, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
3197 }
3198 else
3199 {
3200 const QColor color = parseColor( value, context );
3201 valueString = QgsExpression::quotedString( color.name() );
3202 }
3203 break;
3204 }
3205
3207 {
3208 const double v = value.toDouble() * multiplier;
3209 valueString = QString::number( v );
3210 break;
3211 }
3212
3214 {
3215 const double v = value.toDouble() * maxOpacity;
3216 valueString = QString::number( v );
3217 break;
3218 }
3219
3221 {
3222 valueString = u"array(%1,%2)"_s.arg( value.toList().value( 0 ).toDouble() * multiplier, value.toList().value( 0 ).toDouble() * multiplier );
3223 break;
3224 }
3225
3227 {
3228 if ( value.toList().count() == 2 && value.toList().first().toString() == "literal"_L1 )
3229 {
3230 valueString = u"array(%1)"_s.arg( value.toList().at( 1 ).toStringList().join( ',' ) );
3231 }
3232 else
3233 {
3234 valueString = u"array(%1)"_s.arg( value.toStringList().join( ',' ) );
3235 }
3236 break;
3237 }
3238
3240 {
3241 if ( value.toList().count() == 2 && value.toList().first().toString() == "literal"_L1 )
3242 {
3243 QStringList dashValues = value.toList().at( 1 ).toStringList();
3244 if ( dashValues.length() % 2 == 1 )
3245 {
3246 dashValues << u"0"_s;
3247 }
3248 valueString = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3249 }
3250 else
3251 {
3252 QStringList dashValues = value.toStringList();
3253 if ( dashValues.length() % 2 == 1 )
3254 {
3255 dashValues << u"0"_s;
3256 }
3257 valueString = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3258 }
3259 break;
3260 }
3261 }
3262
3263 if ( matchString.count() == 1 )
3264 {
3265 caseString += u"WHEN %1 IS %2 THEN %3 "_s.arg( attribute, matchString.at( 0 ), valueString );
3266 }
3267 else
3268 {
3269 caseString += u"WHEN %1 IN (%2) THEN %3 "_s.arg( attribute, matchString.join( ',' ), valueString );
3270 }
3271 }
3272
3273 QVariant lastValue = json.constLast();
3274 QString elseValue;
3275
3276 switch ( lastValue.userType() )
3277 {
3278 case QMetaType::Type::QVariantList:
3279 case QMetaType::Type::QStringList:
3280 elseValue = parseValueList( lastValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
3281 break;
3282
3283 default:
3284 {
3285 switch ( type )
3286 {
3288 {
3289 const QColor color = parseColor( lastValue, context );
3290 if ( defaultColor )
3291 *defaultColor = color;
3292
3293 elseValue = QgsExpression::quotedString( color.name() );
3294 break;
3295 }
3296
3298 {
3299 const double v = json.constLast().toDouble() * multiplier;
3300 if ( defaultNumber )
3301 *defaultNumber = v;
3302 elseValue = QString::number( v );
3303 break;
3304 }
3305
3307 {
3308 const double v = json.constLast().toDouble() * maxOpacity;
3309 if ( defaultNumber )
3310 *defaultNumber = v;
3311 elseValue = QString::number( v );
3312 break;
3313 }
3314
3316 {
3317 elseValue = u"array(%1,%2)"_s.arg( json.constLast().toList().value( 0 ).toDouble() * multiplier ).arg( json.constLast().toList().value( 0 ).toDouble() * multiplier );
3318 break;
3319 }
3320
3322 {
3323 if ( json.constLast().toList().count() == 2 && json.constLast().toList().first().toString() == "literal"_L1 )
3324 {
3325 elseValue = u"array(%1)"_s.arg( json.constLast().toList().at( 1 ).toStringList().join( ',' ) );
3326 }
3327 else
3328 {
3329 elseValue = u"array(%1)"_s.arg( json.constLast().toStringList().join( ',' ) );
3330 }
3331 break;
3332 }
3333
3335 {
3336 if ( json.constLast().toList().count() == 2 && json.constLast().toList().first().toString() == "literal"_L1 )
3337 {
3338 QStringList dashValues = json.constLast().toList().at( 1 ).toStringList();
3339 if ( dashValues.length() % 2 == 1 )
3340 {
3341 dashValues << u"0"_s;
3342 }
3343 elseValue = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3344 }
3345 else
3346 {
3347 QStringList dashValues = json.constLast().toStringList();
3348 if ( dashValues.length() % 2 == 1 )
3349 {
3350 dashValues << u"0"_s;
3351 }
3352 elseValue = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3353 }
3354 break;
3355 }
3356 }
3357 break;
3358 }
3359 }
3360
3361 caseString += u"ELSE %1 END"_s.arg( elseValue );
3362 return QgsProperty::fromExpression( caseString );
3363}
3364
3366 const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber
3367)
3368{
3369 const QString expression = parseExpression( json.value( 1 ).toList(), context );
3370 if ( expression.isEmpty() )
3371 {
3372 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3373 return QgsProperty();
3374 }
3375
3376 QString caseString = u"CASE "_s;
3377
3378
3379 for ( int i = json.length() - 2; i > 0; i -= 2 )
3380 {
3381 const QVariant stepValue = json.value( i + 1 );
3382
3383 QString valueString;
3384 if ( stepValue.canConvert<QVariantList>() && ( stepValue.toList().count() != 2 || type != PropertyType::Point ) && type != PropertyType::NumericArray && type != PropertyType::DashArray )
3385 {
3386 valueString = parseValueList( stepValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).expressionString();
3387 }
3388 else
3389 {
3390 switch ( type )
3391 {
3393 {
3394 const QColor color = parseColor( stepValue, context );
3395 valueString = QgsExpression::quotedString( color.name() );
3396 break;
3397 }
3398
3400 {
3401 const double v = stepValue.toDouble() * multiplier;
3402 valueString = QString::number( v );
3403 break;
3404 }
3405
3407 {
3408 const double v = stepValue.toDouble() * maxOpacity;
3409 valueString = QString::number( v );
3410 break;
3411 }
3412
3414 {
3415 valueString = u"array(%1,%2)"_s.arg( stepValue.toList().value( 0 ).toDouble() * multiplier ).arg( stepValue.toList().value( 0 ).toDouble() * multiplier );
3416 break;
3417 }
3418
3420 {
3421 if ( stepValue.toList().count() == 2 && stepValue.toList().first().toString() == "literal"_L1 )
3422 {
3423 valueString = u"array(%1)"_s.arg( stepValue.toList().at( 1 ).toStringList().join( ',' ) );
3424 }
3425 else
3426 {
3427 valueString = u"array(%1)"_s.arg( stepValue.toStringList().join( ',' ) );
3428 }
3429 break;
3430 }
3431
3433 {
3434 if ( stepValue.toList().count() == 2 && stepValue.toList().first().toString() == "literal"_L1 )
3435 {
3436 QStringList dashValues = stepValue.toList().at( 1 ).toStringList();
3437 if ( dashValues.length() % 2 == 1 )
3438 {
3439 dashValues << u"0"_s;
3440 }
3441 valueString = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3442 }
3443 else
3444 {
3445 QStringList dashValues = stepValue.toStringList();
3446 if ( dashValues.length() % 2 == 1 )
3447 {
3448 dashValues << u"0"_s;
3449 }
3450 valueString = u"array(%1)"_s.arg( dashValues.join( ',' ) );
3451 }
3452 break;
3453 }
3454 }
3455 }
3456
3457 if ( i > 1 )
3458 {
3459 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
3460 caseString += u" WHEN %1 >= %2 THEN (%3) "_s.arg( expression, stepKey, valueString );
3461 }
3462 else
3463 {
3464 caseString += u"ELSE (%1) END"_s.arg( valueString );
3465 }
3466 }
3467 return QgsProperty::fromExpression( caseString );
3468}
3469
3471 const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber
3472)
3473{
3474 if ( json.value( 0 ).toString() != "interpolate"_L1 )
3475 {
3476 context.pushWarning( QObject::tr( "%1: Could not interpret value list" ).arg( context.layerId() ) );
3477 return QgsProperty();
3478 }
3479
3480 const QVariantList parts = json.value( 1 ).toList();
3481 const QString technique = parts.value( 0 ).toString();
3482 InterpolationType interpolationType = InterpolationType::Linear;
3483 QVariantMap props;
3484
3485 if ( technique == "linear"_L1 )
3486 {
3487 props.insert( u"base"_s, 1 );
3488 interpolationType = InterpolationType::Linear;
3489 }
3490 else if ( technique == "exponential"_L1 )
3491 {
3492 props.insert( u"base"_s, parts.value( 1 ).toDouble() );
3493 interpolationType = InterpolationType::Exponential;
3494 }
3495 else if ( technique == "cubic-bezier"_L1 )
3496 {
3497 interpolationType = InterpolationType::CubicBezier;
3498
3499 props.insert( u"x1"_s, parts.value( 1 ).toDouble() );
3500 props.insert( u"y1"_s, parts.value( 2 ).toDouble() );
3501 props.insert( u"x2"_s, parts.value( 3 ).toDouble() );
3502 props.insert( u"y2"_s, parts.value( 4 ).toDouble() );
3503 }
3504 else
3505 {
3506 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation method %2" ).arg( context.layerId(), technique ) );
3507 return QgsProperty();
3508 }
3509
3510 if ( json.value( 2 ).toList().value( 0 ).toString() != "zoom"_L1 )
3511 {
3512 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation input %2" ).arg( context.layerId(), json.value( 2 ).toString() ) );
3513 return QgsProperty();
3514 }
3515
3516 // Convert stops into list of lists
3517 QVariantList stops;
3518 for ( int i = 3; i < json.length(); i += 2 )
3519 {
3520 stops.push_back( QVariantList() << json.value( i ).toString() << json.value( i + 1 ) );
3521 }
3522
3523 props.insert( u"stops"_s, stops );
3524
3525 switch ( type )
3526 {
3528 return parseInterpolateColorByZoom( props, context, defaultColor, interpolationType );
3529
3531 return parseInterpolateByZoom( props, context, multiplier, defaultNumber, interpolationType );
3532
3534 return parseInterpolateOpacityByZoom( props, maxOpacity, &context, interpolationType );
3535
3537 return parseInterpolatePointByZoom( props, context, multiplier, nullptr, interpolationType );
3538
3541 context.pushWarning( QObject::tr( "%1: Skipping unsupported numeric array in interpolate" ).arg( context.layerId() ) );
3542 return QgsProperty();
3543 }
3544 return QgsProperty();
3545}
3546
3548{
3549 if ( ( QMetaType::Type ) colorExpression.userType() == QMetaType::QVariantList )
3550 {
3551 return parseExpression( colorExpression.toList(), context, true );
3552 }
3553 return parseValue( colorExpression, context, true );
3554}
3555
3557{
3558 if ( color.userType() != QMetaType::Type::QString )
3559 {
3560 context.pushWarning( QObject::tr( "%1: Could not parse non-string color %2, skipping" ).arg( context.layerId(), color.toString() ) );
3561 return QColor();
3562 }
3563
3564 return QgsSymbolLayerUtils::parseColor( color.toString() );
3565}
3566
3567void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
3568{
3569 hue = std::max( 0, color.hslHue() );
3570 saturation = color.hslSaturation() / 255.0 * 100;
3571 lightness = color.lightness() / 255.0 * 100;
3572 alpha = color.alpha();
3573}
3574
3576 double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier, double x1, double y1, double x2, double y2, InterpolationType type, QgsMapBoxGlStyleConversionContext *contextPtr
3577)
3578{
3580 if ( contextPtr )
3581 {
3582 context = *contextPtr;
3583 }
3584
3585 // special case where min = max !
3586 if ( valueMin.canConvert( QMetaType::Double ) && valueMax.canConvert( QMetaType::Double ) )
3587 {
3588 bool minDoubleOk = true;
3589 const double min = valueMin.toDouble( &minDoubleOk );
3590 bool maxDoubleOk = true;
3591 const double max = valueMax.toDouble( &maxDoubleOk );
3592 if ( minDoubleOk && maxDoubleOk && qgsDoubleNear( min, max ) )
3593 {
3594 return QString::number( min * multiplier );
3595 }
3596 }
3597
3598 QString minValueExpr = valueMin.toString();
3599 QString maxValueExpr = valueMax.toString();
3600 if ( valueMin.userType() == QMetaType::Type::QVariantList )
3601 {
3602 minValueExpr = parseExpression( valueMin.toList(), context );
3603 }
3604 if ( valueMax.userType() == QMetaType::Type::QVariantList )
3605 {
3606 maxValueExpr = parseExpression( valueMax.toList(), context );
3607 }
3608
3609 QString expression;
3610 if ( minValueExpr == maxValueExpr )
3611 {
3612 expression = minValueExpr;
3613 }
3614 else
3615 {
3616 switch ( type )
3617 {
3619 expression = u"scale_linear(@vector_tile_zoom,%1,%2,%3,%4)"_s.arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr );
3620 break;
3622 if ( base == 1 )
3623 {
3624 expression = u"scale_linear(@vector_tile_zoom,%1,%2,%3,%4)"_s.arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr );
3625 }
3626 else
3627 {
3628 expression = u"scale_exponential(@vector_tile_zoom,%1,%2,%3,%4,%5)"_s.arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr ).arg( base );
3629 }
3630 break;
3631
3633 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 );
3634 break;
3635 }
3636 }
3637
3638 if ( multiplier != 1 )
3639 return u"(%1) * %2"_s.arg( expression ).arg( multiplier );
3640 else
3641 return expression;
3642}
3643
3644Qt::PenCapStyle QgsMapBoxGlStyleConverter::parseCapStyle( const QString &style )
3645{
3646 if ( style == "round"_L1 )
3647 return Qt::RoundCap;
3648 else if ( style == "square"_L1 )
3649 return Qt::SquareCap;
3650 else
3651 return Qt::FlatCap; // "butt" is default
3652}
3653
3654Qt::PenJoinStyle QgsMapBoxGlStyleConverter::parseJoinStyle( const QString &style )
3655{
3656 if ( style == "bevel"_L1 )
3657 return Qt::BevelJoin;
3658 else if ( style == "round"_L1 )
3659 return Qt::RoundJoin;
3660 else
3661 return Qt::MiterJoin; // "miter" is default
3662}
3663
3664QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3665{
3666 QString op = expression.value( 0 ).toString();
3667 if ( ( op == "%"_L1 || op == "/"_L1 || op == "-"_L1 || op == "^"_L1 ) && expression.size() >= 3 )
3668 {
3669 if ( expression.size() != 3 )
3670 {
3671 context.pushWarning( QObject::tr( "%1: Operator %2 requires exactly two operands, skipping extra operands" ).arg( context.layerId() ).arg( op ) );
3672 }
3673 QString v1 = parseValue( expression.value( 1 ), context, colorExpected );
3674 QString v2 = parseValue( expression.value( 2 ), context, colorExpected );
3675 return u"(%1 %2 %3)"_s.arg( v1, op, v2 );
3676 }
3677 else if ( ( op == "*"_L1 || op == "+"_L1 ) && expression.size() >= 3 )
3678 {
3679 QStringList operands;
3680 std::transform( std::next( expression.begin() ), expression.end(), std::back_inserter( operands ), [&context, colorExpected]( const QVariant &val ) {
3681 return parseValue( val, context, colorExpected );
3682 } );
3683 return u"(%1)"_s.arg( operands.join( u" %1 "_s.arg( op ) ) );
3684 }
3685 else if ( op == "to-number"_L1 )
3686 {
3687 return u"to_real(%1)"_s.arg( parseValue( expression.value( 1 ), context ) );
3688 }
3689 else if ( op == "sqrt"_L1 )
3690 {
3691 return u"sqrt(%1)"_s.arg( parseValue( expression.value( 1 ), context ) );
3692 }
3693 else if ( op == "literal"_L1 )
3694 {
3695 return expression.value( 1 ).toString();
3696 }
3697 else if ( op == "all"_L1 || op == "any"_L1 || op == "none"_L1 )
3698 {
3699 QStringList parts;
3700 for ( int i = 1; i < expression.size(); ++i )
3701 {
3702 const QString part = parseValue( expression.at( i ), context );
3703 if ( part.isEmpty() )
3704 {
3705 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3706 return QString();
3707 }
3708 parts << part;
3709 }
3710
3711 if ( op == "none"_L1 )
3712 return u"NOT (%1)"_s.arg( parts.join( ") AND NOT ("_L1 ) );
3713
3714 QString operatorString;
3715 if ( op == "all"_L1 )
3716 operatorString = u") AND ("_s;
3717 else if ( op == "any"_L1 )
3718 operatorString = u") OR ("_s;
3719
3720 return u"(%1)"_s.arg( parts.join( operatorString ) );
3721 }
3722 else if ( op == '!' )
3723 {
3724 // ! inverts next expression's meaning
3725 QVariantList contraJsonExpr = expression.value( 1 ).toList();
3726 contraJsonExpr[0] = QString( op + contraJsonExpr[0].toString() );
3727 // ['!', ['has', 'level']] -> ['!has', 'level']
3728 return parseKey( contraJsonExpr, context );
3729 }
3730 else if ( op == "=="_L1 || op == "!="_L1 || op == ">="_L1 || op == '>' || op == "<="_L1 || op == '<' )
3731 {
3732 // use IS and NOT IS instead of = and != because they can deal with NULL values
3733 if ( op == "=="_L1 )
3734 op = u"IS"_s;
3735 else if ( op == "!="_L1 )
3736 op = u"IS NOT"_s;
3737 return u"%1 %2 %3"_s.arg( parseKey( expression.value( 1 ), context ), op, parseValue( expression.value( 2 ), context ) );
3738 }
3739 else if ( op == "has"_L1 )
3740 {
3741 return parseKey( expression.value( 1 ), context ) + u" IS NOT NULL"_s;
3742 }
3743 else if ( op == "!has"_L1 )
3744 {
3745 return parseKey( expression.value( 1 ), context ) + u" IS NULL"_s;
3746 }
3747 else if ( op == "in"_L1 || op == "!in"_L1 )
3748 {
3749 const QString key = parseKey( expression.value( 1 ), context );
3750 QStringList parts;
3751
3752 QVariantList values = expression.mid( 2 );
3753 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 )
3754 {
3755 values = expression.at( 2 ).toList().at( 1 ).toList();
3756 }
3757
3758 for ( const QVariant &value : std::as_const( values ) )
3759 {
3760 const QString part = parseValue( value, context );
3761 if ( part.isEmpty() )
3762 {
3763 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3764 return QString();
3765 }
3766 parts << part;
3767 }
3768
3769 if ( parts.size() == 1 )
3770 {
3771 if ( op == "in"_L1 )
3772 return u"%1 IS %2"_s.arg( key, parts.at( 0 ) );
3773 else
3774 return u"(%1 IS NULL OR %1 IS NOT %2)"_s.arg( key, parts.at( 0 ) );
3775 }
3776 else
3777 {
3778 if ( op == "in"_L1 )
3779 return u"%1 IN (%2)"_s.arg( key, parts.join( ", "_L1 ) );
3780 else
3781 return u"(%1 IS NULL OR %1 NOT IN (%2))"_s.arg( key, parts.join( ", "_L1 ) );
3782 }
3783 }
3784 else if ( op == "get"_L1 )
3785 {
3786 return parseKey( expression.value( 1 ), context );
3787 }
3788 else if ( op == "match"_L1 )
3789 {
3790 const QString attribute = expression.value( 1 ).toList().value( 1 ).toString();
3791
3792 if ( expression.size() == 5
3793 && expression.at( 3 ).userType() == QMetaType::Type::Bool
3794 && expression.at( 3 ).toBool() == true
3795 && expression.at( 4 ).userType() == QMetaType::Type::Bool
3796 && expression.at( 4 ).toBool() == false )
3797 {
3798 // simple case, make a nice simple expression instead of a CASE statement
3799 if ( expression.at( 2 ).userType() == QMetaType::Type::QVariantList || expression.at( 2 ).userType() == QMetaType::Type::QStringList )
3800 {
3801 QStringList parts;
3802 for ( const QVariant &p : expression.at( 2 ).toList() )
3803 {
3804 parts << parseValue( p, context );
3805 }
3806
3807 if ( parts.size() > 1 )
3808 return u"%1 IN (%2)"_s.arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3809 else
3810 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ).toList().value( 0 ) );
3811 }
3812 else if ( expression.at( 2 ).userType() == QMetaType::Type::QString
3813 || expression.at( 2 ).userType() == QMetaType::Type::Int
3814 || expression.at( 2 ).userType() == QMetaType::Type::Double
3815 || expression.at( 2 ).userType() == QMetaType::Type::LongLong )
3816 {
3817 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ) );
3818 }
3819 else
3820 {
3821 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3822 return QString();
3823 }
3824 }
3825 else
3826 {
3827 QString caseString = u"CASE "_s;
3828 for ( int i = 2; i < expression.size() - 2; i += 2 )
3829 {
3830 if ( expression.at( i ).userType() == QMetaType::Type::QVariantList || expression.at( i ).userType() == QMetaType::Type::QStringList )
3831 {
3832 QStringList parts;
3833 for ( const QVariant &p : expression.at( i ).toList() )
3834 {
3835 parts << QgsExpression::quotedValue( p );
3836 }
3837
3838 if ( parts.size() > 1 )
3839 caseString += u"WHEN %1 IN (%2) "_s.arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3840 else
3841 caseString += u"WHEN %1 "_s.arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ).toList().value( 0 ) ) );
3842 }
3843 else if ( expression.at( i ).userType() == QMetaType::Type::QString
3844 || expression.at( i ).userType() == QMetaType::Type::Int
3845 || expression.at( i ).userType() == QMetaType::Type::Double
3846 || expression.at( i ).userType() == QMetaType::Type::LongLong )
3847 {
3848 caseString += u"WHEN (%1) "_s.arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ) ) );
3849 }
3850
3851 caseString += u"THEN %1 "_s.arg( parseValue( expression.at( i + 1 ), context, colorExpected ) );
3852 }
3853 caseString += u"ELSE %1 END"_s.arg( parseValue( expression.last(), context, colorExpected ) );
3854 return caseString;
3855 }
3856 }
3857 else if ( op == "to-string"_L1 )
3858 {
3859 return u"to_string(%1)"_s.arg( parseExpression( expression.value( 1 ).toList(), context ) );
3860 }
3861 else if ( op == "to-boolean"_L1 )
3862 {
3863 return u"to_bool(%1)"_s.arg( parseExpression( expression.value( 1 ).toList(), context ) );
3864 }
3865 else if ( op == "case"_L1 )
3866 {
3867 QString caseString = u"CASE"_s;
3868 for ( int i = 1; i < expression.size() - 2; i += 2 )
3869 {
3870 const QString condition = parseExpression( expression.value( i ).toList(), context );
3871 const QString value = parseValue( expression.value( i + 1 ), context, colorExpected );
3872 caseString += u" WHEN (%1) THEN %2"_s.arg( condition, value );
3873 }
3874 const QString value = parseValue( expression.constLast(), context, colorExpected );
3875 caseString += u" ELSE %1 END"_s.arg( value );
3876 return caseString;
3877 }
3878 else if ( op == "zoom"_L1 && expression.count() == 1 )
3879 {
3880 return u"@vector_tile_zoom"_s;
3881 }
3882 else if ( op == "coalesce"_L1 )
3883 {
3884 QString coalesceString = u"coalesce("_s;
3885 for ( int i = 1; i < expression.size(); i++ )
3886 {
3887 if ( i > 1 )
3888 coalesceString += ", "_L1;
3889 coalesceString += parseValue( expression.value( i ), context );
3890 }
3891 coalesceString += ')'_L1;
3892 return coalesceString;
3893 }
3894 else if ( op == "concat"_L1 )
3895 {
3896 QString concatString = u"concat("_s;
3897 for ( int i = 1; i < expression.size(); i++ )
3898 {
3899 if ( i > 1 )
3900 concatString += ", "_L1;
3901 concatString += parseValue( expression.value( i ), context );
3902 }
3903 concatString += ')'_L1;
3904 return concatString;
3905 }
3906 else if ( op == "length"_L1 )
3907 {
3908 return u"length(%1)"_s.arg( parseExpression( expression.value( 1 ).toList(), context ) );
3909 }
3910 else if ( op == "step"_L1 )
3911 {
3912 const QString stepExpression = parseExpression( expression.value( 1 ).toList(), context );
3913 if ( stepExpression.isEmpty() )
3914 {
3915 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3916 return QString();
3917 }
3918
3919 QString caseString = u"CASE "_s;
3920
3921 for ( int i = expression.length() - 2; i > 0; i -= 2 )
3922 {
3923 const QString stepValue = parseValue( expression.value( i + 1 ), context, colorExpected );
3924 if ( i > 1 )
3925 {
3926 const QString stepKey = QgsExpression::quotedValue( expression.value( i ) );
3927 caseString += u" WHEN %1 >= %2 THEN (%3) "_s.arg( stepExpression, stepKey, stepValue );
3928 }
3929 else
3930 {
3931 caseString += u"ELSE (%1) END"_s.arg( stepValue );
3932 }
3933 }
3934 return caseString;
3935 }
3936 else if ( op == "pitch"_L1 )
3937 {
3938 return u"0"_s;
3939 }
3940 else if ( op == "slice"_L1 )
3941 {
3942 // ["slice", input, startIndex, endIndex?] returns a substring/sublist of input.
3943 // MapBox indices are 0-based and endIndex is exclusive, while QGIS substr() is
3944 // 1-based and takes a length
3945 const QString inputExpression = parseValue( expression.value( 1 ), context );
3946 if ( inputExpression.isEmpty() )
3947 {
3948 context.pushWarning( QObject::tr( "%1: Could not interpret slice list" ).arg( context.layerId() ) );
3949 return QString();
3950 }
3951
3952 // When the indices are constant integers
3953 // we can fold the index arithmetic at conversion time
3954 const auto constantInt = []( const QVariant &value, int &result ) -> bool {
3955 switch ( value.userType() )
3956 {
3957 case QMetaType::Int:
3958 case QMetaType::UInt:
3959 case QMetaType::LongLong:
3960 case QMetaType::ULongLong:
3961 {
3962 bool ok = false;
3963 result = value.toInt( &ok );
3964 return ok;
3965 }
3966 default:
3967 return false;
3968 }
3969 };
3970
3971 int startValue = 0;
3972 const bool startIsConstant = constantInt( expression.value( 2 ), startValue );
3973 const QString startExpression = parseValue( expression.value( 2 ), context );
3974 const QString startOffset = startIsConstant ? QString::number( startValue + 1 ) : u"(%1) + 1"_s.arg( startExpression );
3975
3976 if ( expression.size() > 3 )
3977 {
3978 int endValue = 0;
3979 const bool endIsConstant = constantInt( expression.value( 3 ), endValue );
3980 const QString endExpression = parseValue( expression.value( 3 ), context );
3981 const QString length = ( startIsConstant && endIsConstant ) ? QString::number( endValue - startValue ) : u"(%1) - (%2)"_s.arg( endExpression, startExpression );
3982 return u"substr(%1, %2, %3)"_s.arg( inputExpression, startOffset, length );
3983 }
3984 else
3985 {
3986 return u"substr(%1, %2)"_s.arg( inputExpression, startOffset );
3987 }
3988 }
3989 else
3990 {
3991 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression \"%2\"" ).arg( context.layerId(), op ) );
3992 return QString();
3993 }
3994}
3995
3996QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
3997{
3998 QImage spriteImage;
3999
4000 if ( name.isEmpty() )
4001 {
4002 return QImage();
4003 }
4004
4005 QString category;
4006 QString actualName = name;
4007 const int categorySeparator = name.indexOf( ':' );
4008 if ( categorySeparator > 0 )
4009 {
4010 category = name.left( categorySeparator );
4011 if ( context.spriteCategories().contains( category ) )
4012 {
4013 actualName = name.mid( categorySeparator + 1 );
4014 spriteImage = context.spriteImage( category );
4015 }
4016 else
4017 {
4018 category.clear();
4019 }
4020 }
4021
4022 if ( category.isEmpty() )
4023 {
4024 // Images referenced without a category prefix belong to the sprite source with
4025 // the id "default" (if present), otherwise the single unnamed sprite sheet.
4026 if ( context.spriteCategories().contains( "default"_L1 ) )
4027 category = u"default"_s;
4028 spriteImage = context.spriteImage( category );
4029 }
4030
4031 if ( spriteImage.isNull() )
4032 {
4033 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
4034 return QImage();
4035 }
4036
4037 const QVariantMap spriteDefinition = context.spriteDefinitions( category ).value( actualName ).toMap();
4038 if ( spriteDefinition.size() == 0 )
4039 {
4040 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
4041 return QImage();
4042 }
4043
4044 const QImage sprite
4045 = 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() );
4046 if ( sprite.isNull() )
4047 {
4048 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
4049 return QImage();
4050 }
4051
4052 spriteSize = sprite.size() / spriteDefinition.value( u"pixelRatio"_s ).toDouble() * context.pixelSizeConversionFactor();
4053 return sprite;
4054}
4055
4057 const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty
4058)
4059{
4060 QString spritePath;
4061
4062 auto prepareBase64 = []( const QImage &sprite ) {
4063 QString path;
4064 if ( !sprite.isNull() )
4065 {
4066 QByteArray blob;
4067 QBuffer buffer( &blob );
4068 buffer.open( QIODevice::WriteOnly );
4069 sprite.save( &buffer, "PNG" );
4070 buffer.close();
4071 const QByteArray encoded = blob.toBase64();
4072 path = QString( encoded );
4073 path.prepend( "base64:"_L1 );
4074 }
4075 return path;
4076 };
4077
4078 switch ( value.userType() )
4079 {
4080 case QMetaType::Type::QString:
4081 {
4082 QString spriteName = value.toString();
4083 const thread_local QRegularExpression fieldNameMatch( u"{([^}]+)}"_s );
4084 QRegularExpressionMatch match = fieldNameMatch.match( spriteName );
4085 if ( match.hasMatch() )
4086 {
4087 const QString fieldName = match.captured( 1 );
4088 spriteProperty = u"CASE"_s;
4089 spriteSizeProperty = u"CASE"_s;
4090
4091 spriteName.replace( "(", "\\("_L1 );
4092 spriteName.replace( ")", "\\)"_L1 );
4093 spriteName.replace( fieldNameMatch, u"([^\\/\\\\]+)"_s );
4094 const QRegularExpression fieldValueMatch( spriteName );
4095 const QStringList spriteNames = context.spriteDefinitions().keys();
4096 for ( const QString &name : spriteNames )
4097 {
4098 match = fieldValueMatch.match( name );
4099 if ( match.hasMatch() )
4100 {
4101 QSize size;
4102 QString path;
4103 const QString fieldValue = match.captured( 1 );
4104 const QImage sprite = retrieveSprite( name, context, size );
4105 path = prepareBase64( sprite );
4106 if ( spritePath.isEmpty() && !path.isEmpty() )
4107 {
4108 spritePath = path;
4109 spriteSize = size;
4110 }
4111
4112 spriteProperty += u" WHEN \"%1\" = '%2' THEN '%3'"_s.arg( fieldName, fieldValue, path );
4113 spriteSizeProperty += u" WHEN \"%1\" = '%2' THEN %3"_s.arg( fieldName ).arg( fieldValue ).arg( size.width() );
4114 }
4115 }
4116
4117 spriteProperty += " END"_L1;
4118 spriteSizeProperty += " END"_L1;
4119 }
4120 else
4121 {
4122 spriteProperty.clear();
4123 spriteSizeProperty.clear();
4124 const QImage sprite = retrieveSprite( spriteName, context, spriteSize );
4125 spritePath = prepareBase64( sprite );
4126 }
4127 break;
4128 }
4129
4130 case QMetaType::Type::QVariantMap:
4131 {
4132 const QVariantList stops = value.toMap().value( u"stops"_s ).toList();
4133 if ( stops.size() == 0 )
4134 break;
4135
4136 QString path;
4137 QSize size;
4138 QImage sprite;
4139
4140 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
4141 spritePath = prepareBase64( sprite );
4142
4143 spriteProperty = u"CASE WHEN @vector_tile_zoom < %1 THEN '%2'"_s.arg( stops.value( 0 ).toList().value( 0 ).toString() ).arg( spritePath );
4144 spriteSizeProperty = u"CASE WHEN @vector_tile_zoom < %1 THEN %2"_s.arg( stops.value( 0 ).toList().value( 0 ).toString() ).arg( spriteSize.width() );
4145
4146 for ( int i = 0; i < stops.size() - 1; ++i )
4147 {
4148 ;
4149 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
4150 path = prepareBase64( sprite );
4151
4152 spriteProperty += QStringLiteral(
4153 " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
4154 "THEN '%3'"
4155 )
4156 .arg( stops.value( i ).toList().value( 0 ).toString(), stops.value( i + 1 ).toList().value( 0 ).toString(), path );
4157 spriteSizeProperty += QStringLiteral(
4158 " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
4159 "THEN %3"
4160 )
4161 .arg( stops.value( i ).toList().value( 0 ).toString(), stops.value( i + 1 ).toList().value( 0 ).toString() )
4162 .arg( size.width() );
4163 }
4164 sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
4165 path = prepareBase64( sprite );
4166
4167 spriteProperty += QStringLiteral(
4168 " WHEN @vector_tile_zoom >= %1 "
4169 "THEN '%2' END"
4170 )
4171 .arg( stops.last().toList().value( 0 ).toString() )
4172 .arg( path );
4173 spriteSizeProperty += QStringLiteral(
4174 " WHEN @vector_tile_zoom >= %1 "
4175 "THEN %2 END"
4176 )
4177 .arg( stops.last().toList().value( 0 ).toString() )
4178 .arg( size.width() );
4179 break;
4180 }
4181
4182 case QMetaType::Type::QVariantList:
4183 {
4184 const QVariantList json = value.toList();
4185 const QString method = json.value( 0 ).toString();
4186
4187 if ( method == "match"_L1 )
4188 {
4189 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
4190 if ( attribute.isEmpty() )
4191 {
4192 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
4193 break;
4194 }
4195
4196 spriteProperty = u"CASE"_s;
4197 spriteSizeProperty = u"CASE"_s;
4198
4199 for ( int i = 2; i < json.length() - 1; i += 2 )
4200 {
4201 const QVariant matchKey = json.value( i );
4202 const QVariant matchValue = json.value( i + 1 );
4203 QString matchString;
4204 switch ( matchKey.userType() )
4205 {
4206 case QMetaType::Type::QVariantList:
4207 case QMetaType::Type::QStringList:
4208 {
4209 const QVariantList keys = matchKey.toList();
4210 QStringList matchStringList;
4211 for ( const QVariant &key : keys )
4212 {
4213 matchStringList << QgsExpression::quotedValue( key );
4214 }
4215 matchString = matchStringList.join( ',' );
4216 break;
4217 }
4218
4219 case QMetaType::Type::Bool:
4220 case QMetaType::Type::QString:
4221 case QMetaType::Type::Int:
4222 case QMetaType::Type::LongLong:
4223 case QMetaType::Type::Double:
4224 {
4225 matchString = QgsExpression::quotedValue( matchKey );
4226 break;
4227 }
4228
4229 default:
4230 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
4231 break;
4232 }
4233
4234 QString valuePathExpression;
4235 QString valueSizeExpression;
4236 if ( matchValue.userType() == QMetaType::Type::QVariantList || matchValue.userType() == QMetaType::Type::QStringList )
4237 {
4238 // nested expression (e.g. a nested "match"/"step"/"case") -- resolve recursively
4239 QSize nestedSize;
4240 QString nestedProperty;
4241 QString nestedSizeProperty;
4242 const QString nestedPath = retrieveSpriteAsBase64WithProperties( matchValue, context, nestedSize, nestedProperty, nestedSizeProperty );
4243 valuePathExpression = nestedProperty.isEmpty() ? u"'%1'"_s.arg( nestedPath ) : nestedProperty;
4244 valueSizeExpression = nestedSizeProperty.isEmpty() ? QString::number( nestedSize.width() ) : nestedSizeProperty;
4245 spritePath = nestedPath;
4246 spriteSize = nestedSize;
4247 }
4248 else
4249 {
4250 const QImage sprite = retrieveSprite( matchValue.toString(), context, spriteSize );
4251 spritePath = prepareBase64( sprite );
4252 valuePathExpression = u"'%1'"_s.arg( spritePath );
4253 valueSizeExpression = QString::number( spriteSize.width() );
4254 }
4255
4256 spriteProperty += u" WHEN %1 IN (%2) THEN %3"_s.arg( attribute, matchString, valuePathExpression );
4257 spriteSizeProperty += u" WHEN %1 IN (%2) THEN %3"_s.arg( attribute, matchString, valueSizeExpression );
4258 }
4259
4260 const QVariant defaultValue = json.constLast();
4261 if ( defaultValue.userType() == QMetaType::Type::QVariantList || defaultValue.userType() == QMetaType::Type::QStringList )
4262 {
4263 // default is a nested expression (e.g. a nested "match"/"step"/"case") -- resolve recursively
4264 QSize nestedSize;
4265 QString nestedProperty;
4266 QString nestedSizeProperty;
4267 const QString nestedPath = retrieveSpriteAsBase64WithProperties( defaultValue, context, nestedSize, nestedProperty, nestedSizeProperty );
4268 spriteProperty += u" ELSE %1 END"_s.arg( nestedProperty.isEmpty() ? u"'%1'"_s.arg( nestedPath ) : nestedProperty );
4269 spriteSizeProperty += u" ELSE %1 END"_s.arg( nestedSizeProperty.isEmpty() ? QString::number( nestedSize.width() ) : nestedSizeProperty );
4270 spritePath = nestedPath;
4271 spriteSize = nestedSize;
4272 }
4273 else
4274 {
4275 if ( !defaultValue.toString().isEmpty() )
4276 {
4277 const QImage sprite = retrieveSprite( defaultValue.toString(), context, spriteSize );
4278 spritePath = prepareBase64( sprite );
4279 }
4280 else
4281 {
4282 spritePath = QString();
4283 }
4284
4285 spriteProperty += u" ELSE '%1' END"_s.arg( spritePath );
4286 spriteSizeProperty += u" ELSE %3 END"_s.arg( spriteSize.width() );
4287 }
4288 break;
4289 }
4290 else if ( method == "step"_L1 )
4291 {
4292 const QString expression = parseExpression( json.value( 1 ).toList(), context );
4293 if ( expression.isEmpty() )
4294 {
4295 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
4296 break;
4297 }
4298
4299 spriteProperty = u"CASE"_s;
4300 spriteSizeProperty = u"CASE"_s;
4301 for ( int i = json.length() - 2; i > 2; i -= 2 )
4302 {
4303 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
4304 const QString stepValue = json.value( i + 1 ).toString();
4305
4306 const QImage sprite = retrieveSprite( stepValue, context, spriteSize );
4307 spritePath = prepareBase64( sprite );
4308
4309 spriteProperty += u" WHEN %1 >= %2 THEN '%3' "_s.arg( expression, stepKey, spritePath );
4310 spriteSizeProperty += u" WHEN %1 >= %2 THEN %3 "_s.arg( expression ).arg( stepKey ).arg( spriteSize.width() );
4311 }
4312
4313 const QImage sprite = retrieveSprite( json.at( 2 ).toString(), context, spriteSize );
4314 spritePath = prepareBase64( sprite );
4315
4316 spriteProperty += u"ELSE '%1' END"_s.arg( spritePath );
4317 spriteSizeProperty += u"ELSE %3 END"_s.arg( spriteSize.width() );
4318 break;
4319 }
4320 else if ( method == "case"_L1 )
4321 {
4322 spriteProperty = u"CASE"_s;
4323 spriteSizeProperty = u"CASE"_s;
4324 for ( int i = 1; i < json.length() - 2; i += 2 )
4325 {
4326 const QString caseExpression = parseExpression( json.value( i ).toList(), context );
4327 const QString caseValue = json.value( i + 1 ).toString();
4328
4329 const QImage sprite = retrieveSprite( caseValue, context, spriteSize );
4330 spritePath = prepareBase64( sprite );
4331
4332 spriteProperty += u" WHEN %1 THEN '%2' "_s.arg( caseExpression, spritePath );
4333 spriteSizeProperty += u" WHEN %1 THEN %2 "_s.arg( caseExpression ).arg( spriteSize.width() );
4334 }
4335 const QImage sprite = retrieveSprite( json.last().toString(), context, spriteSize );
4336 spritePath = prepareBase64( sprite );
4337
4338 spriteProperty += u"ELSE '%1' END"_s.arg( spritePath );
4339 spriteSizeProperty += u"ELSE %3 END"_s.arg( spriteSize.width() );
4340 break;
4341 }
4342 else
4343 {
4344 context.pushWarning( QObject::tr( "%1: Could not interpret sprite value list with method %2" ).arg( context.layerId(), method ) );
4345 break;
4346 }
4347 }
4348
4349 default:
4350 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
4351 break;
4352 }
4353
4354 return spritePath;
4355}
4356
4357QString QgsMapBoxGlStyleConverter::parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
4358{
4359 QColor c;
4360 switch ( value.userType() )
4361 {
4362 case QMetaType::Type::QVariantList:
4363 case QMetaType::Type::QStringList:
4364 return parseExpression( value.toList(), context, colorExpected );
4365
4366 case QMetaType::Type::Bool:
4367 case QMetaType::Type::QString:
4368 if ( colorExpected )
4369 {
4370 QColor c = parseColor( value, context );
4371 if ( c.isValid() )
4372 {
4373 return parseValue( c, context );
4374 }
4375 }
4376 return QgsExpression::quotedValue( value );
4377
4378 case QMetaType::Type::Int:
4379 case QMetaType::Type::LongLong:
4380 case QMetaType::Type::Double:
4381 return value.toString();
4382
4383 case QMetaType::Type::QColor:
4384 c = value.value<QColor>();
4385 return QString( "color_rgba(%1,%2,%3,%4)" ).arg( c.red() ).arg( c.green() ).arg( c.blue() ).arg( c.alpha() );
4386
4387 default:
4388 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression part" ).arg( context.layerId() ) );
4389 break;
4390 }
4391 return QString();
4392}
4393
4394QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context )
4395{
4396 if ( value.toString() == "$type"_L1 )
4397 {
4398 return u"_geom_type"_s;
4399 }
4400 if ( value.toString() == "level"_L1 )
4401 {
4402 return u"level"_s;
4403 }
4404 else if ( ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() == 1 ) || value.userType() == QMetaType::Type::QStringList )
4405 {
4406 if ( value.toList().size() > 1 )
4407 return value.toList().at( 1 ).toString();
4408 else
4409 {
4410 QString valueString = value.toList().value( 0 ).toString();
4411 if ( valueString == "geometry-type"_L1 )
4412 {
4413 return u"_geom_type"_s;
4414 }
4415 return valueString;
4416 }
4417 }
4418 else if ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() > 1 )
4419 {
4420 return parseExpression( value.toList(), context );
4421 }
4422 return QgsExpression::quotedColumnRef( value.toString() );
4423}
4424
4425QString QgsMapBoxGlStyleConverter::processLabelField( const QString &string, bool &isExpression )
4426{
4427 // {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
4428 // but if single field is covered in {}, return it directly
4429 const thread_local QRegularExpression singleFieldRx( u"^{([^}]+)}$"_s );
4430 const QRegularExpressionMatch match = singleFieldRx.match( string );
4431 if ( match.hasMatch() )
4432 {
4433 isExpression = false;
4434 return match.captured( 1 );
4435 }
4436
4437 const thread_local QRegularExpression multiFieldRx( u"(?={[^}]+})"_s );
4438 const QStringList parts = string.split( multiFieldRx );
4439 if ( parts.size() > 1 )
4440 {
4441 isExpression = true;
4442
4443 QStringList res;
4444 for ( const QString &part : parts )
4445 {
4446 if ( part.isEmpty() )
4447 continue;
4448
4449 if ( !part.contains( '{' ) )
4450 {
4451 res << QgsExpression::quotedValue( part );
4452 continue;
4453 }
4454
4455 // part will start at a {field} reference
4456 const QStringList split = part.split( '}' );
4457 res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
4458 if ( !split.at( 1 ).isEmpty() )
4459 res << QgsExpression::quotedValue( split.at( 1 ) );
4460 }
4461 return u"concat(%1)"_s.arg( res.join( ',' ) );
4462 }
4463 else
4464 {
4465 isExpression = false;
4466 return string;
4467 }
4468}
4469
4470std::unique_ptr<QgsVectorTileRenderer> QgsMapBoxGlStyleConverter::renderer() const
4471{
4472 return mRenderer ? std::unique_ptr<QgsVectorTileRenderer>( mRenderer->clone() ) : nullptr;
4473}
4474
4475std::unique_ptr<QgsVectorTileLabeling> QgsMapBoxGlStyleConverter::labeling() const
4476{
4477 return mLabeling ? std::unique_ptr<QgsVectorTileLabeling>( mLabeling->clone() ) : nullptr;
4478}
4479
4480QList<QgsMapBoxGlStyleAbstractSource *> QgsMapBoxGlStyleConverter::sources()
4481{
4482 return mSources;
4483}
4484
4485QList<QgsMapBoxGlStyleRasterSubLayer> QgsMapBoxGlStyleConverter::rasterSubLayers() const
4486{
4487 return mRasterSubLayers;
4488}
4489
4491{
4492 QList<QgsMapLayer *> subLayers;
4493 for ( const QgsMapBoxGlStyleRasterSubLayer &subLayer : mRasterSubLayers )
4494 {
4495 const QString sourceName = subLayer.source();
4496 std::unique_ptr< QgsRasterLayer > rl;
4497 for ( const QgsMapBoxGlStyleAbstractSource *source : mSources )
4498 {
4499 if ( source->type() == Qgis::MapBoxGlStyleSourceType::Raster && source->name() == sourceName )
4500 {
4501 const QgsMapBoxGlStyleRasterSource *rasterSource = qgis::down_cast< const QgsMapBoxGlStyleRasterSource * >( source );
4502 rl.reset( rasterSource->toRasterLayer() );
4503 rl->pipe()->setDataDefinedProperties( subLayer.dataDefinedProperties() );
4504 break;
4505 }
4506 }
4507
4508 if ( rl )
4509 {
4510 subLayers.append( rl.release() );
4511 }
4512 }
4513 return subLayers;
4514}
4515
4516
4518{
4519 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
4520 if ( !context )
4521 {
4522 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
4523 context = tmpContext.get();
4524 }
4525
4526 auto typeFromString = [context]( const QString &string, const QString &name ) -> Qgis::MapBoxGlStyleSourceType {
4527 if ( string.compare( "vector"_L1, Qt::CaseInsensitive ) == 0 )
4529 else if ( string.compare( "raster"_L1, Qt::CaseInsensitive ) == 0 )
4531 else if ( string.compare( "raster-dem"_L1, Qt::CaseInsensitive ) == 0 )
4533 else if ( string.compare( "geojson"_L1, Qt::CaseInsensitive ) == 0 )
4535 else if ( string.compare( "image"_L1, Qt::CaseInsensitive ) == 0 )
4537 else if ( string.compare( "video"_L1, Qt::CaseInsensitive ) == 0 )
4539 context->pushWarning( QObject::tr( "Invalid source type \"%1\" for source \"%2\"" ).arg( string, name ) );
4541 };
4542
4543 for ( auto it = sources.begin(); it != sources.end(); ++it )
4544 {
4545 const QString name = it.key();
4546 const QVariantMap jsonSource = it.value().toMap();
4547 const QString typeString = jsonSource.value( u"type"_s ).toString();
4548
4549 const Qgis::MapBoxGlStyleSourceType type = typeFromString( typeString, name );
4550
4551 switch ( type )
4552 {
4554 parseRasterSource( jsonSource, name, context );
4555 break;
4562 QgsDebugError( u"Ignoring vector tile style source %1 (%2)"_s.arg( name, qgsEnumValueToKey( type ) ) );
4563 continue;
4564 }
4565 }
4566}
4567
4568void QgsMapBoxGlStyleConverter::parseRasterSource( const QVariantMap &source, const QString &name, QgsMapBoxGlStyleConversionContext *context )
4569{
4570 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
4571 if ( !context )
4572 {
4573 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
4574 context = tmpContext.get();
4575 }
4576
4577 auto raster = std::make_unique< QgsMapBoxGlStyleRasterSource >( name );
4578 if ( raster->setFromJson( source, context ) )
4579 mSources.append( raster.release() );
4580}
4581
4582bool QgsMapBoxGlStyleConverter::numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top )
4583{
4584 if ( bottomVariant.canConvert( QMetaType::Double ) && topVariant.canConvert( QMetaType::Double ) )
4585 {
4586 bool bDoubleOk, tDoubleOk;
4587 bottom = bottomVariant.toDouble( &bDoubleOk );
4588 top = topVariant.toDouble( &tDoubleOk );
4589 return ( bDoubleOk && tDoubleOk );
4590 }
4591 return false;
4592}
4593
4594//
4595// QgsMapBoxGlStyleConversionContext
4596//
4598{
4599 QgsDebugError( warning );
4600 mWarnings << warning;
4601}
4602
4604{
4605 return mTargetUnit;
4606}
4607
4612
4614{
4615 return mSizeConversionFactor;
4616}
4617
4619{
4620 mSizeConversionFactor = sizeConversionFactor;
4621}
4622
4624{
4625 return mSpriteImage.keys();
4626}
4627
4628QImage QgsMapBoxGlStyleConversionContext::spriteImage( const QString &category ) const
4629{
4630 return mSpriteImage.contains( category ) ? mSpriteImage[category] : QImage();
4631}
4632
4633QVariantMap QgsMapBoxGlStyleConversionContext::spriteDefinitions( const QString &category ) const
4634{
4635 return mSpriteDefinitions.contains( category ) ? mSpriteDefinitions[category] : QVariantMap();
4636}
4637
4638void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions, const QString &category )
4639{
4640 mSpriteImage[category] = image;
4641 mSpriteDefinitions[category] = definitions;
4642}
4643
4644void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions, const QString &category )
4645{
4646 setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap(), category );
4647}
4648
4650{
4651 return mLayerId;
4652}
4653
4655{
4656 mLayerId = value;
4657}
4658
4659//
4660// QgsMapBoxGlStyleAbstractSource
4661//
4665
4667{
4668 return mName;
4669}
4670
4672
4673//
4674// QgsMapBoxGlStyleRasterSource
4675//
4676
4680
4685
4687{
4688 mAttribution = json.value( u"attribution"_s ).toString();
4689
4690 const QString scheme = json.value( u"scheme"_s, u"xyz"_s ).toString();
4691 if ( scheme.compare( "xyz"_L1 ) == 0 )
4692 {
4693 // xyz scheme is supported
4694 }
4695 else
4696 {
4697 context->pushWarning( QObject::tr( "%1 scheme is not supported for raster source %2" ).arg( scheme, name() ) );
4698 return false;
4699 }
4700
4701 mMinZoom = json.value( u"minzoom"_s, u"0"_s ).toInt();
4702 mMaxZoom = json.value( u"maxzoom"_s, u"22"_s ).toInt();
4703 mTileSize = json.value( u"tileSize"_s, u"512"_s ).toInt();
4704
4705 const QVariantList tiles = json.value( u"tiles"_s ).toList();
4706 for ( const QVariant &tile : tiles )
4707 {
4708 mTiles.append( tile.toString() );
4709 }
4710
4711 return true;
4712}
4713
4715{
4716 QVariantMap parts;
4717 parts.insert( u"type"_s, u"xyz"_s );
4718 parts.insert( u"url"_s, mTiles.value( 0 ) );
4719
4720 if ( mTileSize == 256 )
4721 parts.insert( u"tilePixelRation"_s, u"1"_s );
4722 else if ( mTileSize == 512 )
4723 parts.insert( u"tilePixelRation"_s, u"2"_s );
4724
4725 parts.insert( u"zmax"_s, QString::number( mMaxZoom ) );
4726 parts.insert( u"zmin"_s, QString::number( mMinZoom ) );
4727
4728 auto rl = std::make_unique< QgsRasterLayer >( QgsProviderRegistry::instance()->encodeUri( u"wms"_s, parts ), name(), u"wms"_s );
4729 return rl.release();
4730}
4731
4732//
4733// QgsMapBoxGlStyleRasterSubLayer
4734//
4736 : mId( id )
4737 , mSource( source )
4738{}
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1398
@ OnLine
Labels can be placed directly over a line feature.
Definition qgis.h:1396
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1397
@ CentralPoint
Place symbols at the mid point of the line.
Definition qgis.h:3352
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
Definition qgis.h:1289
@ Curved
Arranges candidates following the curvature of a line feature. Applies to line layers only.
Definition qgis.h:1291
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature or along a line feature....
Definition qgis.h:1292
@ AboveRight
Above right.
Definition qgis.h:1378
@ BelowLeft
Below left.
Definition qgis.h:1382
@ Above
Above center.
Definition qgis.h:1377
@ BelowRight
Below right.
Definition qgis.h:1384
@ Right
Right middle.
Definition qgis.h:1381
@ AboveLeft
Above left.
Definition qgis.h:1376
@ Below
Below center.
Definition qgis.h:1383
@ Over
Center middle.
Definition qgis.h:1380
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:1460
@ FollowPlacement
Alignment follows placement of label, e.g., labels to the left of a feature will be drawn with right ...
Definition qgis.h:1462
RenderUnit
Rendering size units.
Definition qgis.h:5704
MapBoxGlStyleSourceType
Available MapBox GL style source types.
Definition qgis.h:4770
@ Vector
Vector source.
Definition qgis.h:4771
@ RasterDem
Raster DEM source.
Definition qgis.h:4773
@ Raster
Raster source.
Definition qgis.h:4772
@ Unknown
Other/unknown source type.
Definition qgis.h:4777
@ GeoJson
GeoJSON source.
Definition qgis.h:4774
@ Viewport
Relative to the whole viewport/output device.
Definition qgis.h:3427
@ AllowOverlapAtNoCost
Labels may freely overlap other labels, at no cost.
Definition qgis.h:1251
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...
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.
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...
std::unique_ptr< QgsVectorTileLabeling > labeling() const
Returns a new instance of a vector tile labeling representing the converted style,...
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.
std::unique_ptr< QgsVectorTileRenderer > renderer() const
Returns a new instance of a vector tile renderer representing the converted style,...
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).
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:7531
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:7898
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7624
#define QgsDebugError(str)
Definition qgslogger.h:71
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30