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