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