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