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