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