QGIS API Documentation 3.41.0-Master (af5edcb665c)
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 default:
1717 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextRotate.userType() ) ) ) );
1718 break;
1719 }
1720 }
1721
1722 if ( jsonLayout.contains( QStringLiteral( "text-transform" ) ) )
1723 {
1724 const QString textTransform = jsonLayout.value( QStringLiteral( "text-transform" ) ).toString();
1725 if ( textTransform == QLatin1String( "uppercase" ) )
1726 {
1727 labelSettings.fieldName = QStringLiteral( "upper(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1728 }
1729 else if ( textTransform == QLatin1String( "lowercase" ) )
1730 {
1731 labelSettings.fieldName = QStringLiteral( "lower(%1)" ).arg( labelSettings.isExpression ? labelSettings.fieldName : QgsExpression::quotedColumnRef( labelSettings.fieldName ) );
1732 }
1733 labelSettings.isExpression = true;
1734 }
1735
1738 if ( jsonLayout.contains( QStringLiteral( "symbol-placement" ) ) )
1739 {
1740 const QString symbolPlacement = jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString();
1741 if ( symbolPlacement == QLatin1String( "line" ) )
1742 {
1745 geometryType = Qgis::GeometryType::Line;
1746
1747 if ( jsonLayout.contains( QStringLiteral( "text-rotation-alignment" ) ) )
1748 {
1749 const QString textRotationAlignment = jsonLayout.value( QStringLiteral( "text-rotation-alignment" ) ).toString();
1750 if ( textRotationAlignment == QLatin1String( "viewport" ) )
1751 {
1753 }
1754 }
1755
1756 if ( labelSettings.placement == Qgis::LabelPlacement::Curved )
1757 {
1758 QPointF textOffset;
1759 QgsProperty textOffsetProperty;
1760 if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1761 {
1762 const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1763
1764 // units are ems!
1765 switch ( jsonTextOffset.userType() )
1766 {
1767 case QMetaType::Type::QVariantMap:
1768 textOffsetProperty = parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, !textSizeProperty ? textSize : 1.0, &textOffset );
1769 if ( !textSizeProperty )
1770 {
1771 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LabelDistance, QStringLiteral( "abs(array_get(%1,1))-%2" ).arg( textOffsetProperty.asExpression() ).arg( textSize ) );
1772 }
1773 else
1774 {
1775 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() ) );
1776 }
1777 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::LinePlacementOptions, QStringLiteral( "if(array_get(%1,1)>0,'BL','AL')" ).arg( textOffsetProperty.asExpression() ) );
1778 break;
1779
1780 case QMetaType::Type::QVariantList:
1781 case QMetaType::Type::QStringList:
1782 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1783 jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1784 break;
1785
1786 default:
1787 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) ) );
1788 break;
1789 }
1790
1791 if ( !textOffset.isNull() )
1792 {
1793 labelSettings.distUnits = context.targetUnit();
1794 labelSettings.dist = std::abs( textOffset.y() ) - textSize;
1796 if ( textSizeProperty && !textOffsetProperty )
1797 {
1798 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() ) );
1799 }
1800 }
1801 }
1802
1803 if ( textOffset.isNull() )
1804 {
1806 }
1807 }
1808 }
1809 }
1810
1811 if ( jsonLayout.contains( QStringLiteral( "text-justify" ) ) )
1812 {
1813 const QVariant jsonTextJustify = jsonLayout.value( QStringLiteral( "text-justify" ) );
1814
1815 // default is center
1816 QString textAlign = QStringLiteral( "center" );
1817
1818 const QVariantMap conversionMap
1819 {
1820 { QStringLiteral( "left" ), QStringLiteral( "left" ) },
1821 { QStringLiteral( "center" ), QStringLiteral( "center" ) },
1822 { QStringLiteral( "right" ), QStringLiteral( "right" ) },
1823 { QStringLiteral( "auto" ), QStringLiteral( "follow" ) }
1824 };
1825
1826 switch ( jsonTextJustify.userType() )
1827 {
1828 case QMetaType::Type::QString:
1829 textAlign = jsonTextJustify.toString();
1830 break;
1831
1832 case QMetaType::Type::QVariantList:
1833 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextJustify.toList(), context, conversionMap, &textAlign ) ) );
1834 break;
1835
1836 case QMetaType::Type::QVariantMap:
1837 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextJustify.toMap(), context, conversionMap, &textAlign ) );
1838 break;
1839
1840 default:
1841 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-justify type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextJustify.userType() ) ) ) );
1842 break;
1843 }
1844
1845 if ( textAlign == QLatin1String( "left" ) )
1847 else if ( textAlign == QLatin1String( "right" ) )
1849 else if ( textAlign == QLatin1String( "center" ) )
1851 else if ( textAlign == QLatin1String( "follow" ) )
1853 }
1854 else
1855 {
1857 }
1858
1859 if ( labelSettings.placement == Qgis::LabelPlacement::OverPoint )
1860 {
1861 if ( jsonLayout.contains( QStringLiteral( "text-anchor" ) ) )
1862 {
1863 const QVariant jsonTextAnchor = jsonLayout.value( QStringLiteral( "text-anchor" ) );
1864 QString textAnchor;
1865
1866 const QVariantMap conversionMap
1867 {
1868 { QStringLiteral( "center" ), 4 },
1869 { QStringLiteral( "left" ), 5 },
1870 { QStringLiteral( "right" ), 3 },
1871 { QStringLiteral( "top" ), 7 },
1872 { QStringLiteral( "bottom" ), 1 },
1873 { QStringLiteral( "top-left" ), 8 },
1874 { QStringLiteral( "top-right" ), 6 },
1875 { QStringLiteral( "bottom-left" ), 2 },
1876 { QStringLiteral( "bottom-right" ), 0 },
1877 };
1878
1879 switch ( jsonTextAnchor.userType() )
1880 {
1881 case QMetaType::Type::QString:
1882 textAnchor = jsonTextAnchor.toString();
1883 break;
1884
1885 case QMetaType::Type::QVariantList:
1886 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, QgsProperty::fromExpression( parseStringStops( jsonTextAnchor.toList(), context, conversionMap, &textAnchor ) ) );
1887 break;
1888
1889 case QMetaType::Type::QVariantMap:
1890 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetQuad, parseInterpolateStringByZoom( jsonTextAnchor.toMap(), context, conversionMap, &textAnchor ) );
1891 break;
1892
1893 default:
1894 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-anchor type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextAnchor.userType() ) ) ) );
1895 break;
1896 }
1897
1898 if ( textAnchor == QLatin1String( "center" ) )
1900 else if ( textAnchor == QLatin1String( "left" ) )
1902 else if ( textAnchor == QLatin1String( "right" ) )
1904 else if ( textAnchor == QLatin1String( "top" ) )
1906 else if ( textAnchor == QLatin1String( "bottom" ) )
1908 else if ( textAnchor == QLatin1String( "top-left" ) )
1910 else if ( textAnchor == QLatin1String( "top-right" ) )
1912 else if ( textAnchor == QLatin1String( "bottom-left" ) )
1914 else if ( textAnchor == QLatin1String( "bottom-right" ) )
1916 }
1917
1918 QPointF textOffset;
1919 if ( jsonLayout.contains( QStringLiteral( "text-offset" ) ) )
1920 {
1921 const QVariant jsonTextOffset = jsonLayout.value( QStringLiteral( "text-offset" ) );
1922
1923 // units are ems!
1924 switch ( jsonTextOffset.userType() )
1925 {
1926 case QMetaType::Type::QVariantMap:
1927 ddLabelProperties.setProperty( QgsPalLayerSettings::Property::OffsetXY, parseInterpolatePointByZoom( jsonTextOffset.toMap(), context, textSize, &textOffset ) );
1928 break;
1929
1930 case QMetaType::Type::QVariantList:
1931 case QMetaType::Type::QStringList:
1932 textOffset = QPointF( jsonTextOffset.toList().value( 0 ).toDouble() * textSize,
1933 jsonTextOffset.toList().value( 1 ).toDouble() * textSize );
1934 break;
1935
1936 default:
1937 context.pushWarning( QObject::tr( "%1: Skipping unsupported text-offset type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonTextOffset.userType() ) ) ) );
1938 break;
1939 }
1940
1941 if ( !textOffset.isNull() )
1942 {
1943 labelSettings.offsetUnits = context.targetUnit();
1944 labelSettings.xOffset = textOffset.x();
1945 labelSettings.yOffset = textOffset.y();
1946 }
1947 }
1948 }
1949
1950 if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) &&
1951 ( labelSettings.placement == Qgis::LabelPlacement::Horizontal || labelSettings.placement == Qgis::LabelPlacement::Curved ) )
1952 {
1953 QSize spriteSize;
1954 QString spriteProperty, spriteSizeProperty;
1955 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
1956 if ( !sprite.isEmpty() )
1957 {
1958 double size = 1.0;
1959 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
1960 {
1961 QgsProperty property;
1962 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
1963 switch ( jsonIconSize.userType() )
1964 {
1965 case QMetaType::Type::Int:
1966 case QMetaType::Type::LongLong:
1967 case QMetaType::Type::Double:
1968 {
1969 size = jsonIconSize.toDouble();
1970 if ( !spriteSizeProperty.isEmpty() )
1971 {
1973 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
1974 }
1975 break;
1976 }
1977
1978 case QMetaType::Type::QVariantMap:
1979 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
1980 break;
1981
1982 case QMetaType::Type::QVariantList:
1983 case QMetaType::Type::QStringList:
1984 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
1985 break;
1986 default:
1987 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
1988 break;
1989 }
1990
1991 if ( !property.expressionString().isEmpty() )
1992 {
1993 if ( !spriteSizeProperty.isEmpty() )
1994 {
1996 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
1997 }
1998 else
1999 {
2001 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2002 }
2003 }
2004 }
2005
2007 markerLayer->setPath( sprite );
2008 markerLayer->setSize( spriteSize.width() );
2009 markerLayer->setSizeUnit( context.targetUnit() );
2010
2011 if ( !spriteProperty.isEmpty() )
2012 {
2013 QgsPropertyCollection markerDdProperties;
2014 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2015 markerLayer->setDataDefinedProperties( markerDdProperties );
2016 }
2017
2018 QgsTextBackgroundSettings backgroundSettings;
2019 backgroundSettings.setEnabled( true );
2021 backgroundSettings.setSize( spriteSize * size );
2022 backgroundSettings.setSizeUnit( context.targetUnit() );
2024 backgroundSettings.setMarkerSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2025 format.setBackground( backgroundSettings );
2026 }
2027 }
2028
2029 if ( textSize >= 0 )
2030 {
2031 // TODO -- this probably needs revisiting -- it was copied from the MapTiler code, but may be wrong...
2032 labelSettings.priority = std::min( textSize / ( context.pixelSizeConversionFactor() * 3 ), 10.0 );
2033 }
2034
2035 labelSettings.setFormat( format );
2036
2037 // use a low obstacle weight for layers by default -- we'd rather have more labels for these layers, even if placement isn't ideal
2038 labelSettings.obstacleSettings().setFactor( 0.1 );
2039
2040 labelSettings.setDataDefinedProperties( ddLabelProperties );
2041
2042 labelingStyle.setGeometryType( geometryType );
2043 labelingStyle.setLabelSettings( labelSettings );
2044
2045 hasLabeling = true;
2046
2047 hasRenderer = parseSymbolLayerAsRenderer( jsonLayer, renderer, context );
2048}
2049
2051{
2052 if ( !jsonLayer.contains( QStringLiteral( "layout" ) ) )
2053 {
2054 context.pushWarning( QObject::tr( "%1: Style layer has no layout property, skipping" ).arg( context.layerId() ) );
2055 return false;
2056 }
2057 const QVariantMap jsonLayout = jsonLayer.value( QStringLiteral( "layout" ) ).toMap();
2058
2059 if ( jsonLayout.value( QStringLiteral( "symbol-placement" ) ).toString() == QLatin1String( "line" ) && !jsonLayout.contains( QStringLiteral( "text-field" ) ) )
2060 {
2061 QgsPropertyCollection ddProperties;
2062
2063 double spacing = -1.0;
2064 if ( jsonLayout.contains( QStringLiteral( "symbol-spacing" ) ) )
2065 {
2066 const QVariant jsonSpacing = jsonLayout.value( QStringLiteral( "symbol-spacing" ) );
2067 switch ( jsonSpacing.userType() )
2068 {
2069 case QMetaType::Type::Int:
2070 case QMetaType::Type::LongLong:
2071 case QMetaType::Type::Double:
2072 spacing = jsonSpacing.toDouble() * context.pixelSizeConversionFactor();
2073 break;
2074
2075 case QMetaType::Type::QVariantMap:
2076 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseInterpolateByZoom( jsonSpacing.toMap(), context, context.pixelSizeConversionFactor(), &spacing ) );
2077 break;
2078
2079 case QMetaType::Type::QVariantList:
2080 case QMetaType::Type::QStringList:
2081 ddProperties.setProperty( QgsSymbolLayer::Property::Interval, parseValueList( jsonSpacing.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &spacing ) );
2082 break;
2083
2084 default:
2085 context.pushWarning( QObject::tr( "%1: Skipping unsupported symbol-spacing type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonSpacing.userType() ) ) ) );
2086 break;
2087 }
2088 }
2089 else
2090 {
2091 // defaults to 250
2092 spacing = 250 * context.pixelSizeConversionFactor();
2093 }
2094
2095 bool rotateMarkers = true;
2096 if ( jsonLayout.contains( QStringLiteral( "icon-rotation-alignment" ) ) )
2097 {
2098 const QString alignment = jsonLayout.value( QStringLiteral( "icon-rotation-alignment" ) ).toString();
2099 if ( alignment == QLatin1String( "map" ) || alignment == QLatin1String( "auto" ) )
2100 {
2101 rotateMarkers = true;
2102 }
2103 else if ( alignment == QLatin1String( "viewport" ) )
2104 {
2105 rotateMarkers = false;
2106 }
2107 }
2108
2109 QgsPropertyCollection markerDdProperties;
2110 double rotation = 0.0;
2111 if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
2112 {
2113 const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
2114 switch ( jsonIconRotate.userType() )
2115 {
2116 case QMetaType::Type::Int:
2117 case QMetaType::Type::LongLong:
2118 case QMetaType::Type::Double:
2119 rotation = jsonIconRotate.toDouble();
2120 break;
2121
2122 case QMetaType::Type::QVariantMap:
2123 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2124 break;
2125
2126 case QMetaType::Type::QVariantList:
2127 case QMetaType::Type::QStringList:
2128 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2129 break;
2130
2131 default:
2132 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) ) );
2133 break;
2134 }
2135 }
2136
2137 QgsMarkerLineSymbolLayer *lineSymbol = new QgsMarkerLineSymbolLayer( rotateMarkers, spacing > 0 ? spacing : 1 );
2138 lineSymbol->setOutputUnit( context.targetUnit() );
2139 lineSymbol->setDataDefinedProperties( ddProperties );
2140 if ( spacing < 1 )
2141 {
2142 // if spacing isn't specified, it's a central point marker only
2144 }
2145
2147 QSize spriteSize;
2148 QString spriteProperty, spriteSizeProperty;
2149 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
2150 if ( !sprite.isNull() )
2151 {
2152 markerLayer->setPath( sprite );
2153 markerLayer->setSize( spriteSize.width() );
2154 markerLayer->setSizeUnit( context.targetUnit() );
2155
2156 if ( !spriteProperty.isEmpty() )
2157 {
2158 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2159 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2160 }
2161 }
2162
2163 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
2164 {
2165 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
2166 double size = 1.0;
2167 QgsProperty property;
2168 switch ( jsonIconSize.userType() )
2169 {
2170 case QMetaType::Type::Int:
2171 case QMetaType::Type::LongLong:
2172 case QMetaType::Type::Double:
2173 {
2174 size = jsonIconSize.toDouble();
2175 if ( !spriteSizeProperty.isEmpty() )
2176 {
2177 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2178 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
2179 }
2180 break;
2181 }
2182
2183 case QMetaType::Type::QVariantMap:
2184 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2185 break;
2186
2187 case QMetaType::Type::QVariantList:
2188 case QMetaType::Type::QStringList:
2189 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2190 break;
2191 default:
2192 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
2193 break;
2194 }
2195 markerLayer->setSize( size * spriteSize.width() );
2196 if ( !property.expressionString().isEmpty() )
2197 {
2198 if ( !spriteSizeProperty.isEmpty() )
2199 {
2200 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2201 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2202 }
2203 else
2204 {
2205 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2206 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2207 }
2208 }
2209 }
2210
2211 markerLayer->setDataDefinedProperties( markerDdProperties );
2212 markerLayer->setAngle( rotation );
2213 lineSymbol->setSubSymbol( new QgsMarkerSymbol( QgsSymbolLayerList() << markerLayer ) );
2214
2215 std::unique_ptr< QgsSymbol > symbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << lineSymbol );
2216
2217 // set render units
2218 symbol->setOutputUnit( context.targetUnit() );
2219 lineSymbol->setOutputUnit( context.targetUnit() );
2220
2222 rendererStyle.setSymbol( symbol.release() );
2223 return true;
2224 }
2225 else if ( jsonLayout.contains( QStringLiteral( "icon-image" ) ) )
2226 {
2227 const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
2228
2229 QSize spriteSize;
2230 QString spriteProperty, spriteSizeProperty;
2231 const QString sprite = retrieveSpriteAsBase64WithProperties( jsonLayout.value( QStringLiteral( "icon-image" ) ), context, spriteSize, spriteProperty, spriteSizeProperty );
2232 if ( !sprite.isEmpty() || !spriteProperty.isEmpty() )
2233 {
2235 rasterMarker->setPath( sprite );
2236 rasterMarker->setSize( spriteSize.width() );
2237 rasterMarker->setSizeUnit( context.targetUnit() );
2238
2239 QgsPropertyCollection markerDdProperties;
2240 if ( !spriteProperty.isEmpty() )
2241 {
2242 markerDdProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( spriteProperty ) );
2243 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width, QgsProperty::fromExpression( spriteSizeProperty ) );
2244 }
2245
2246 if ( jsonLayout.contains( QStringLiteral( "icon-size" ) ) )
2247 {
2248 const QVariant jsonIconSize = jsonLayout.value( QStringLiteral( "icon-size" ) );
2249 double size = 1.0;
2250 QgsProperty property;
2251 switch ( jsonIconSize.userType() )
2252 {
2253 case QMetaType::Type::Int:
2254 case QMetaType::Type::LongLong:
2255 case QMetaType::Type::Double:
2256 {
2257 size = jsonIconSize.toDouble();
2258 if ( !spriteSizeProperty.isEmpty() )
2259 {
2260 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2261 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,%2*@marker_size)" ).arg( spriteSizeProperty ).arg( size ) ) );
2262 }
2263 break;
2264 }
2265
2266 case QMetaType::Type::QVariantMap:
2267 property = parseInterpolateByZoom( jsonIconSize.toMap(), context, 1, &size );
2268 break;
2269
2270 case QMetaType::Type::QVariantList:
2271 case QMetaType::Type::QStringList:
2272 property = parseValueList( jsonIconSize.toList(), PropertyType::Numeric, context );
2273 break;
2274 default:
2275 context.pushWarning( QObject::tr( "%1: Skipping non-implemented icon-size type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconSize.userType() ) ) ) );
2276 break;
2277 }
2278 rasterMarker->setSize( size * spriteSize.width() );
2279 if ( !property.expressionString().isEmpty() )
2280 {
2281 if ( !spriteSizeProperty.isEmpty() )
2282 {
2283 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2284 QgsProperty::fromExpression( QStringLiteral( "with_variable('marker_size',%1,(%2)*@marker_size)" ).arg( spriteSizeProperty ).arg( property.expressionString() ) ) );
2285 }
2286 else
2287 {
2288 markerDdProperties.setProperty( QgsSymbolLayer::Property::Width,
2289 QgsProperty::fromExpression( QStringLiteral( "(%2)*%1" ).arg( spriteSize.width() ).arg( property.expressionString() ) ) );
2290 }
2291 }
2292 }
2293
2294 double rotation = 0.0;
2295 if ( jsonLayout.contains( QStringLiteral( "icon-rotate" ) ) )
2296 {
2297 const QVariant jsonIconRotate = jsonLayout.value( QStringLiteral( "icon-rotate" ) );
2298 switch ( jsonIconRotate.userType() )
2299 {
2300 case QMetaType::Type::Int:
2301 case QMetaType::Type::LongLong:
2302 case QMetaType::Type::Double:
2303 rotation = jsonIconRotate.toDouble();
2304 break;
2305
2306 case QMetaType::Type::QVariantMap:
2307 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseInterpolateByZoom( jsonIconRotate.toMap(), context, context.pixelSizeConversionFactor(), &rotation ) );
2308 break;
2309
2310 case QMetaType::Type::QVariantList:
2311 case QMetaType::Type::QStringList:
2312 markerDdProperties.setProperty( QgsSymbolLayer::Property::Angle, parseValueList( jsonIconRotate.toList(), PropertyType::Numeric, context, context.pixelSizeConversionFactor(), 255, nullptr, &rotation ) );
2313 break;
2314
2315 default:
2316 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-rotate type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconRotate.userType() ) ) ) );
2317 break;
2318 }
2319 }
2320
2321 double iconOpacity = -1.0;
2322 if ( jsonPaint.contains( QStringLiteral( "icon-opacity" ) ) )
2323 {
2324 const QVariant jsonIconOpacity = jsonPaint.value( QStringLiteral( "icon-opacity" ) );
2325 switch ( jsonIconOpacity.userType() )
2326 {
2327 case QMetaType::Type::Int:
2328 case QMetaType::Type::LongLong:
2329 case QMetaType::Type::Double:
2330 iconOpacity = jsonIconOpacity.toDouble();
2331 break;
2332
2333 case QMetaType::Type::QVariantMap:
2334 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseInterpolateByZoom( jsonIconOpacity.toMap(), context, 100, &iconOpacity ) );
2335 break;
2336
2337 case QMetaType::Type::QVariantList:
2338 case QMetaType::Type::QStringList:
2339 markerDdProperties.setProperty( QgsSymbolLayer::Property::Opacity, parseValueList( jsonIconOpacity.toList(), PropertyType::Numeric, context, 100, 255, nullptr, &iconOpacity ) );
2340 break;
2341
2342 default:
2343 context.pushWarning( QObject::tr( "%1: Skipping unsupported icon-opacity type (%2)" ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( jsonIconOpacity.userType() ) ) ) );
2344 break;
2345 }
2346 }
2347
2348 rasterMarker->setDataDefinedProperties( markerDdProperties );
2349 rasterMarker->setAngle( rotation );
2350 if ( iconOpacity >= 0 )
2351 rasterMarker->setOpacity( iconOpacity );
2352
2353 QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << rasterMarker );
2354 rendererStyle.setSymbol( markerSymbol );
2356 return true;
2357 }
2358 }
2359
2360 return false;
2361}
2362
2364{
2365 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2366 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2367 if ( stops.empty() )
2368 return QgsProperty();
2369
2370 QString caseString = QStringLiteral( "CASE " );
2371 const QString colorComponent( "color_part(%1,'%2')" );
2372
2373 for ( int i = 0; i < stops.length() - 1; ++i )
2374 {
2375 // step bottom zoom
2376 const QString bz = stops.at( i ).toList().value( 0 ).toString();
2377 // step top zoom
2378 const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
2379
2380 const QVariant bcVariant = stops.at( i ).toList().value( 1 );
2381 const QVariant tcVariant = stops.at( i + 1 ).toList().value( 1 );
2382
2383 const QColor bottomColor = parseColor( bcVariant.toString(), context );
2384 const QColor topColor = parseColor( tcVariant.toString(), context );
2385
2386 if ( i == 0 && bottomColor.isValid() )
2387 {
2388 int bcHue;
2389 int bcSat;
2390 int bcLight;
2391 int bcAlpha;
2392 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2393 caseString += QStringLiteral( "WHEN @vector_tile_zoom < %1 THEN color_hsla(%2, %3, %4, %5) " )
2394 .arg( bz ).arg( bcHue ).arg( bcSat ).arg( bcLight ).arg( bcAlpha );
2395 }
2396
2397 if ( bottomColor.isValid() && topColor.isValid() )
2398 {
2399 int bcHue;
2400 int bcSat;
2401 int bcLight;
2402 int bcAlpha;
2403 colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
2404 int tcHue;
2405 int tcSat;
2406 int tcLight;
2407 int tcAlpha;
2408 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2409 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2410 "%3, %4, %5, %6) " ).arg( bz, tz,
2411 interpolateExpression( bz.toDouble(), tz.toDouble(), bcHue, tcHue, base, 1, &context ),
2412 interpolateExpression( bz.toDouble(), tz.toDouble(), bcSat, tcSat, base, 1, &context ),
2413 interpolateExpression( bz.toDouble(), tz.toDouble(), bcLight, tcLight, base, 1, &context ),
2414 interpolateExpression( bz.toDouble(), tz.toDouble(), bcAlpha, tcAlpha, base, 1, &context ) );
2415 }
2416 else
2417 {
2418 const QString bottomColorExpr = parseColorExpression( bcVariant, context );
2419 const QString topColorExpr = parseColorExpression( tcVariant, context );
2420
2421 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 THEN color_hsla("
2422 "%3, %4, %5, %6) " ).arg( bz, tz,
2423 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_hue" ), colorComponent.arg( topColorExpr ).arg( "hsl_hue" ), base, 1, &context ),
2424 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "hsl_saturation" ), colorComponent.arg( topColorExpr ).arg( "hsl_saturation" ), base, 1, &context ),
2425 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "lightness" ), colorComponent.arg( topColorExpr ).arg( "lightness" ), base, 1, &context ),
2426 interpolateExpression( bz.toDouble(), tz.toDouble(), colorComponent.arg( bottomColorExpr ).arg( "alpha" ), colorComponent.arg( topColorExpr ).arg( "alpha" ), base, 1, &context ) );
2427 }
2428 }
2429
2430 // top color
2431 const QString tz = stops.last().toList().value( 0 ).toString();
2432 const QVariant tcVariant = stops.last().toList().value( 1 );
2433 QColor topColor;
2434 if ( tcVariant.userType() == QMetaType::Type::QString )
2435 {
2436 topColor = parseColor( tcVariant, context );
2437 if ( topColor.isValid() )
2438 {
2439 int tcHue;
2440 int tcSat;
2441 int tcLight;
2442 int tcAlpha;
2443 colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
2444 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2445 "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz ).arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );
2446 }
2447 }
2448 else if ( tcVariant.userType() == QMetaType::QVariantList )
2449 {
2450 const QString topColorExpr = parseColorExpression( tcVariant, context );
2451
2452 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 THEN color_hsla(%2, %3, %4, %5) "
2453 "ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
2454 .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" ) );
2455 }
2456
2457 if ( !stops.empty() && defaultColor )
2458 *defaultColor = parseColor( stops.value( 0 ).toList().value( 1 ).toString(), context );
2459
2460 return QgsProperty::fromExpression( caseString );
2461}
2462
2463QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, double *defaultNumber )
2464{
2465 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2466 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2467 if ( stops.empty() )
2468 return QgsProperty();
2469
2470 QString scaleExpression;
2471 if ( stops.size() <= 2 )
2472 {
2473 scaleExpression = interpolateExpression(
2474 stops.value( 0 ).toList().value( 0 ).toDouble(), // zoomMin
2475 stops.last().toList().value( 0 ).toDouble(), // zoomMax
2476 stops.value( 0 ).toList().value( 1 ), // valueMin
2477 stops.last().toList().value( 1 ), // valueMax
2478 base, multiplier, &context );
2479 }
2480 else
2481 {
2482 scaleExpression = parseStops( base, stops, multiplier, context );
2483 }
2484
2485 if ( !stops.empty() && defaultNumber )
2486 *defaultNumber = stops.value( 0 ).toList().value( 1 ).toDouble() * multiplier;
2487
2488 return QgsProperty::fromExpression( scaleExpression );
2489}
2490
2492{
2494 if ( contextPtr )
2495 {
2496 context = *contextPtr;
2497 }
2498 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2499 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2500 if ( stops.empty() )
2501 return QgsProperty();
2502
2503 QString scaleExpression;
2504 if ( stops.length() <= 2 )
2505 {
2506 const QVariant bv = stops.value( 0 ).toList().value( 1 );
2507 const QVariant tv = stops.last().toList().value( 1 );
2508 double bottom = 0.0;
2509 double top = 0.0;
2510 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2511 scaleExpression = QStringLiteral( "set_color_part(@symbol_color, 'alpha', %1)" )
2513 stops.value( 0 ).toList().value( 0 ).toDouble(),
2514 stops.last().toList().value( 0 ).toDouble(),
2515 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2516 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ), base, 1, &context ) );
2517 }
2518 else
2519 {
2520 scaleExpression = parseOpacityStops( base, stops, maxOpacity, context );
2521 }
2522 return QgsProperty::fromExpression( scaleExpression );
2523}
2524
2525QString QgsMapBoxGlStyleConverter::parseOpacityStops( double base, const QVariantList &stops, int maxOpacity, QgsMapBoxGlStyleConversionContext &context )
2526{
2527 QString caseString = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN set_color_part(@symbol_color, 'alpha', %2)" )
2528 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
2529 .arg( stops.value( 0 ).toList().value( 1 ).toDouble() * maxOpacity );
2530
2531 for ( int i = 0; i < stops.size() - 1; ++i )
2532 {
2533 const QVariant bv = stops.value( i ).toList().value( 1 );
2534 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2535 double bottom = 0.0;
2536 double top = 0.0;
2537 const bool numeric = numericArgumentsOnly( bv, tv, bottom, top );
2538
2539 caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
2540 "THEN set_color_part(@symbol_color, 'alpha', %3)" )
2541 .arg( stops.value( i ).toList().value( 0 ).toString(),
2542 stops.value( i + 1 ).toList().value( 0 ).toString(),
2544 stops.value( i ).toList().value( 0 ).toDouble(),
2545 stops.value( i + 1 ).toList().value( 0 ).toDouble(),
2546 numeric ? QString::number( bottom * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( bv, context ) ).arg( maxOpacity ),
2547 numeric ? QString::number( top * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( tv, context ) ).arg( maxOpacity ),
2548 base, 1, &context ) );
2549 }
2550
2551
2552 bool numeric = false;
2553 const QVariant vv = stops.last().toList().value( 1 );
2554 double dv = vv.toDouble( &numeric );
2555
2556 caseString += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
2557 "THEN set_color_part(@symbol_color, 'alpha', %2) END" ).arg(
2558 stops.last().toList().value( 0 ).toString(),
2559 numeric ? QString::number( dv * maxOpacity ) : QString( "(%1) * %2" ).arg( parseValue( vv, context ) ).arg( maxOpacity )
2560 );
2561 return caseString;
2562}
2563
2564QgsProperty QgsMapBoxGlStyleConverter::parseInterpolatePointByZoom( const QVariantMap &json, QgsMapBoxGlStyleConversionContext &context, double multiplier, QPointF *defaultPoint )
2565{
2566 const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
2567 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2568 if ( stops.empty() )
2569 return QgsProperty();
2570
2571 QString scaleExpression;
2572 if ( stops.size() <= 2 )
2573 {
2574 scaleExpression = QStringLiteral( "array(%1,%2)" ).arg( interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2575 stops.last().toList().value( 0 ).toDouble(),
2576 stops.value( 0 ).toList().value( 1 ).toList().value( 0 ),
2577 stops.last().toList().value( 1 ).toList().value( 0 ), base, multiplier, &context ),
2578 interpolateExpression( stops.value( 0 ).toList().value( 0 ).toDouble(),
2579 stops.last().toList().value( 0 ).toDouble(),
2580 stops.value( 0 ).toList().value( 1 ).toList().value( 1 ),
2581 stops.last().toList().value( 1 ).toList().value( 1 ), base, multiplier, &context )
2582 );
2583 }
2584 else
2585 {
2586 scaleExpression = parsePointStops( base, stops, context, multiplier );
2587 }
2588
2589 if ( !stops.empty() && defaultPoint )
2590 *defaultPoint = QPointF( stops.value( 0 ).toList().value( 1 ).toList().value( 0 ).toDouble() * multiplier,
2591 stops.value( 0 ).toList().value( 1 ).toList().value( 1 ).toDouble() * multiplier );
2592
2593 return QgsProperty::fromExpression( scaleExpression );
2594}
2595
2597 const QVariantMap &conversionMap, QString *defaultString )
2598{
2599 const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
2600 if ( stops.empty() )
2601 return QgsProperty();
2602
2603 const QString scaleExpression = parseStringStops( stops, context, conversionMap, defaultString );
2604
2605 return QgsProperty::fromExpression( scaleExpression );
2606}
2607
2608QString QgsMapBoxGlStyleConverter::parsePointStops( double base, const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, double multiplier )
2609{
2610 QString caseString = QStringLiteral( "CASE " );
2611
2612 for ( int i = 0; i < stops.length() - 1; ++i )
2613 {
2614 // bottom zoom and value
2615 const QVariant bz = stops.value( i ).toList().value( 0 );
2616 const QVariant bv = stops.value( i ).toList().value( 1 );
2617 if ( bv.userType() != QMetaType::Type::QVariantList && bv.userType() != QMetaType::Type::QStringList )
2618 {
2619 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( bz.userType() ) ) ) );
2620 return QString();
2621 }
2622
2623 // top zoom and value
2624 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2625 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2626 if ( tv.userType() != QMetaType::Type::QVariantList && tv.userType() != QMetaType::Type::QStringList )
2627 {
2628 context.pushWarning( QObject::tr( "%1: Skipping unsupported offset interpolation type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( tz.userType() ) ) ) );
2629 return QString();
2630 }
2631
2632 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2633 "THEN array(%3,%4)" ).arg( bz.toString(),
2634 tz.toString(),
2635 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 0 ), tv.toList().value( 0 ), base, multiplier, &context ),
2636 interpolateExpression( bz.toDouble(), tz.toDouble(), bv.toList().value( 1 ), tv.toList().value( 1 ), base, multiplier, &context ) );
2637 }
2638 caseString += QLatin1String( "END" );
2639 return caseString;
2640}
2641
2642QString QgsMapBoxGlStyleConverter::parseArrayStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &, double multiplier )
2643{
2644 if ( stops.length() < 2 )
2645 return QString();
2646
2647 QString caseString = QStringLiteral( "CASE" );
2648
2649 for ( int i = 0; i < stops.length(); ++i )
2650 {
2651 caseString += QLatin1String( " WHEN " );
2652 QStringList conditions;
2653 if ( i > 0 )
2654 {
2655 const QVariant bottomZoom = stops.value( i ).toList().value( 0 );
2656 conditions << QStringLiteral( "@vector_tile_zoom > %1" ).arg( bottomZoom.toString() );
2657 }
2658 if ( i < stops.length() - 1 )
2659 {
2660 const QVariant topZoom = stops.value( i + 1 ).toList().value( 0 );
2661 conditions << QStringLiteral( "@vector_tile_zoom <= %1" ).arg( topZoom.toString() );
2662 }
2663
2664 const QVariantList values = stops.value( i ).toList().value( 1 ).toList();
2665 QStringList valuesFixed;
2666 bool ok = false;
2667 for ( const QVariant &value : values )
2668 {
2669 const double number = value.toDouble( &ok );
2670 if ( ok )
2671 valuesFixed << QString::number( number * multiplier );
2672 }
2673
2674 // top zoom and value
2675 caseString += QStringLiteral( "%1 THEN array(%3)" ).arg(
2676 conditions.join( QLatin1String( " AND " ) ),
2677 valuesFixed.join( ',' )
2678 );
2679 }
2680 caseString += QLatin1String( " END" );
2681 return caseString;
2682}
2683
2684QString QgsMapBoxGlStyleConverter::parseStops( double base, const QVariantList &stops, double multiplier, QgsMapBoxGlStyleConversionContext &context )
2685{
2686 QString caseString = QStringLiteral( "CASE " );
2687
2688 for ( int i = 0; i < stops.length() - 1; ++i )
2689 {
2690 // bottom zoom and value
2691 const QVariant bz = stops.value( i ).toList().value( 0 );
2692 const QVariant bv = stops.value( i ).toList().value( 1 );
2693 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2694 {
2695 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2696 return QString();
2697 }
2698
2699 // top zoom and value
2700 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2701 const QVariant tv = stops.value( i + 1 ).toList().value( 1 );
2702 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2703 {
2704 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2705 return QString();
2706 }
2707
2708 const QString lowerComparator = i == 0 ? QStringLiteral( ">=" ) : QStringLiteral( ">" );
2709
2710 caseString += QStringLiteral( "WHEN @vector_tile_zoom %1 %2 AND @vector_tile_zoom <= %3 "
2711 "THEN %4 " ).arg( lowerComparator,
2712 bz.toString(),
2713 tz.toString(),
2714 interpolateExpression( bz.toDouble(), tz.toDouble(), bv, tv, base, multiplier, &context ) );
2715 }
2716
2717 const QVariant z = stops.last().toList().value( 0 );
2718 const QVariant v = stops.last().toList().value( 1 );
2719 QString vStr = v.toString();
2720 if ( ( QMetaType::Type )v.userType() == QMetaType::QVariantList )
2721 {
2722 vStr = parseExpression( v.toList(), context );
2723 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2724 "THEN ( ( %2 ) * %3 ) END" ).arg( z.toString() ).arg( vStr ).arg( multiplier );
2725 }
2726 else
2727 {
2728 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 "
2729 "THEN %2 END" ).arg( z.toString() ).arg( v.toDouble() * multiplier );
2730 }
2731
2732 return caseString;
2733}
2734
2735QString QgsMapBoxGlStyleConverter::parseStringStops( const QVariantList &stops, QgsMapBoxGlStyleConversionContext &context, const QVariantMap &conversionMap, QString *defaultString )
2736{
2737 QString caseString = QStringLiteral( "CASE " );
2738
2739 for ( int i = 0; i < stops.length() - 1; ++i )
2740 {
2741 // bottom zoom and value
2742 const QVariant bz = stops.value( i ).toList().value( 0 );
2743 const QString bv = stops.value( i ).toList().value( 1 ).toString();
2744 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2745 {
2746 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2747 return QString();
2748 }
2749
2750 // top zoom
2751 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2752 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2753 {
2754 context.pushWarning( QObject::tr( "%1: Expressions in interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2755 return QString();
2756 }
2757
2758 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom <= %2 "
2759 "THEN %3 " ).arg( bz.toString(),
2760 tz.toString(),
2761 QgsExpression::quotedValue( conversionMap.value( bv, bv ) ) );
2762 }
2763 caseString += QStringLiteral( "ELSE %1 END" ).arg( QgsExpression::quotedValue( conversionMap.value( stops.constLast().toList().value( 1 ).toString(),
2764 stops.constLast().toList().value( 1 ) ) ) );
2765 if ( defaultString )
2766 *defaultString = stops.constLast().toList().value( 1 ).toString();
2767 return caseString;
2768}
2769
2771{
2772 QString caseString = QStringLiteral( "CASE " );
2773
2774 bool isExpression = false;
2775 for ( int i = 0; i < stops.length() - 1; ++i )
2776 {
2777 // bottom zoom and value
2778 const QVariant bz = stops.value( i ).toList().value( 0 );
2779 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2780 {
2781 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2782 return QString();
2783 }
2784
2785 // top zoom
2786 const QVariant tz = stops.value( i + 1 ).toList().value( 0 );
2787 if ( tz.userType() == QMetaType::Type::QVariantList || tz.userType() == QMetaType::Type::QStringList )
2788 {
2789 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2790 return QString();
2791 }
2792
2793 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
2794 if ( fieldPart.isEmpty() )
2795 fieldPart = QStringLiteral( "''" );
2796 else if ( !isExpression )
2797 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
2798
2799 caseString += QStringLiteral( "WHEN @vector_tile_zoom > %1 AND @vector_tile_zoom < %2 "
2800 "THEN %3 " ).arg( bz.toString(),
2801 tz.toString(),
2802 fieldPart ) ;
2803 }
2804
2805 {
2806 const QVariant bz = stops.constLast().toList().value( 0 );
2807 if ( bz.userType() == QMetaType::Type::QVariantList || bz.userType() == QMetaType::Type::QStringList )
2808 {
2809 context.pushWarning( QObject::tr( "%1: Lists in label interpolation function are not supported, skipping." ).arg( context.layerId() ) );
2810 return QString();
2811 }
2812
2813 QString fieldPart = processLabelField( stops.constLast().toList().value( 1 ).toString(), isExpression );
2814 if ( fieldPart.isEmpty() )
2815 fieldPart = QStringLiteral( "''" );
2816 else if ( !isExpression )
2817 fieldPart = QgsExpression::quotedColumnRef( fieldPart );
2818
2819 caseString += QStringLiteral( "WHEN @vector_tile_zoom >= %1 "
2820 "THEN %3 " ).arg( bz.toString(),
2821 fieldPart ) ;
2822 }
2823
2824 QString defaultPart = processLabelField( stops.constFirst().toList().value( 1 ).toString(), isExpression );
2825 if ( defaultPart.isEmpty() )
2826 defaultPart = QStringLiteral( "''" );
2827 else if ( !isExpression )
2828 defaultPart = QgsExpression::quotedColumnRef( defaultPart );
2829 caseString += QStringLiteral( "ELSE %1 END" ).arg( defaultPart );
2830
2831 return caseString;
2832}
2833
2834QgsProperty QgsMapBoxGlStyleConverter::parseValueList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2835{
2836 const QString method = json.value( 0 ).toString();
2837 if ( method == QLatin1String( "interpolate" ) )
2838 {
2839 return parseInterpolateListByZoom( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2840 }
2841 else if ( method == QLatin1String( "match" ) )
2842 {
2843 return parseMatchList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2844 }
2845 else if ( method == QLatin1String( "step" ) )
2846 {
2847 return parseStepList( json, type, context, multiplier, maxOpacity, defaultColor, defaultNumber );
2848 }
2849 else
2850 {
2851 return QgsProperty::fromExpression( parseExpression( json, context ) );
2852 }
2853}
2854
2855QgsProperty QgsMapBoxGlStyleConverter::parseMatchList( const QVariantList &json, QgsMapBoxGlStyleConverter::PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
2856{
2857 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
2858 if ( attribute.isEmpty() )
2859 {
2860 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
2861 return QgsProperty();
2862 }
2863
2864 QString caseString = QStringLiteral( "CASE " );
2865
2866 for ( int i = 2; i < json.length() - 1; i += 2 )
2867 {
2868 QVariantList keys;
2869 QVariant variantKeys = json.value( i );
2870 if ( variantKeys.userType() == QMetaType::Type::QVariantList || variantKeys.userType() == QMetaType::Type::QStringList )
2871 keys = variantKeys.toList();
2872 else
2873 keys = {variantKeys};
2874
2875 QStringList matchString;
2876 for ( const QVariant &key : keys )
2877 {
2878 matchString << QgsExpression::quotedValue( key );
2879 }
2880
2881 const QVariant value = json.value( i + 1 );
2882
2883 QString valueString;
2884 switch ( type )
2885 {
2887 {
2888 if ( value.userType() == QMetaType::Type::QVariantList || value.userType() == QMetaType::Type::QStringList )
2889 {
2890 valueString = parseMatchList( value.toList(), PropertyType::Color, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
2891 }
2892 else
2893 {
2894 const QColor color = parseColor( value, context );
2895 valueString = QgsExpression::quotedString( color.name() );
2896 }
2897 break;
2898 }
2899
2901 {
2902 const double v = value.toDouble() * multiplier;
2903 valueString = QString::number( v );
2904 break;
2905 }
2906
2908 {
2909 const double v = value.toDouble() * maxOpacity;
2910 valueString = QString::number( v );
2911 break;
2912 }
2913
2915 {
2916 valueString = QStringLiteral( "array(%1,%2)" ).arg( value.toList().value( 0 ).toDouble() * multiplier,
2917 value.toList().value( 0 ).toDouble() * multiplier );
2918 break;
2919 }
2920
2922 {
2923 if ( value.toList().count() == 2 && value.toList().first().toString() == QLatin1String( "literal" ) )
2924 {
2925 valueString = QStringLiteral( "array(%1)" ).arg( value.toList().at( 1 ).toStringList().join( ',' ) );
2926 }
2927 else
2928 {
2929 valueString = QStringLiteral( "array(%1)" ).arg( value.toStringList().join( ',' ) );
2930 }
2931 break;
2932 }
2933 }
2934
2935 if ( matchString.count() == 1 )
2936 {
2937 caseString += QStringLiteral( "WHEN %1 IS %2 THEN %3 " ).arg( attribute, matchString.at( 0 ), valueString );
2938 }
2939 else
2940 {
2941 caseString += QStringLiteral( "WHEN %1 IN (%2) THEN %3 " ).arg( attribute, matchString.join( ',' ), valueString );
2942 }
2943 }
2944
2945 QVariant lastValue = json.constLast();
2946 QString elseValue;
2947
2948 switch ( lastValue.userType() )
2949 {
2950 case QMetaType::Type::QVariantList:
2951 case QMetaType::Type::QStringList:
2952 elseValue = parseValueList( lastValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).asExpression();
2953 break;
2954
2955 default:
2956 {
2957 switch ( type )
2958 {
2960 {
2961 const QColor color = parseColor( lastValue, context );
2962 if ( defaultColor )
2963 *defaultColor = color;
2964
2965 elseValue = QgsExpression::quotedString( color.name() );
2966 break;
2967 }
2968
2970 {
2971 const double v = json.constLast().toDouble() * multiplier;
2972 if ( defaultNumber )
2973 *defaultNumber = v;
2974 elseValue = QString::number( v );
2975 break;
2976 }
2977
2979 {
2980 const double v = json.constLast().toDouble() * maxOpacity;
2981 if ( defaultNumber )
2982 *defaultNumber = v;
2983 elseValue = QString::number( v );
2984 break;
2985 }
2986
2988 {
2989 elseValue = QStringLiteral( "array(%1,%2)" )
2990 .arg( json.constLast().toList().value( 0 ).toDouble() * multiplier )
2991 .arg( json.constLast().toList().value( 0 ).toDouble() * multiplier );
2992 break;
2993 }
2994
2996 {
2997 if ( json.constLast().toList().count() == 2 && json.constLast().toList().first().toString() == QLatin1String( "literal" ) )
2998 {
2999 elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toList().at( 1 ).toStringList().join( ',' ) );
3000 }
3001 else
3002 {
3003 elseValue = QStringLiteral( "array(%1)" ).arg( json.constLast().toStringList().join( ',' ) );
3004 }
3005 break;
3006 }
3007
3008 }
3009 break;
3010 }
3011 }
3012
3013 caseString += QStringLiteral( "ELSE %1 END" ).arg( elseValue );
3014 return QgsProperty::fromExpression( caseString );
3015}
3016
3017QgsProperty QgsMapBoxGlStyleConverter::parseStepList( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
3018{
3019 const QString expression = parseExpression( json.value( 1 ).toList(), context );
3020 if ( expression.isEmpty() )
3021 {
3022 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3023 return QgsProperty();
3024 }
3025
3026 QString caseString = QStringLiteral( "CASE " );
3027
3028
3029 for ( int i = json.length() - 2; i > 0; i -= 2 )
3030 {
3031 const QVariant stepValue = json.value( i + 1 );
3032
3033 QString valueString;
3034 if ( stepValue.canConvert<QVariantList>()
3035 && ( stepValue.toList().count() != 2 || type != PropertyType::Point )
3036 && type != PropertyType::NumericArray )
3037 {
3038 valueString = parseValueList( stepValue.toList(), type, context, multiplier, maxOpacity, defaultColor, defaultNumber ).expressionString();
3039 }
3040 else
3041 {
3042 switch ( type )
3043 {
3045 {
3046 const QColor color = parseColor( stepValue, context );
3047 valueString = QgsExpression::quotedString( color.name() );
3048 break;
3049 }
3050
3052 {
3053 const double v = stepValue.toDouble() * multiplier;
3054 valueString = QString::number( v );
3055 break;
3056 }
3057
3059 {
3060 const double v = stepValue.toDouble() * maxOpacity;
3061 valueString = QString::number( v );
3062 break;
3063 }
3064
3066 {
3067 valueString = QStringLiteral( "array(%1,%2)" ).arg(
3068 stepValue.toList().value( 0 ).toDouble() * multiplier ).arg(
3069 stepValue.toList().value( 0 ).toDouble() * multiplier
3070 );
3071 break;
3072 }
3073
3075 {
3076 if ( stepValue.toList().count() == 2 && stepValue.toList().first().toString() == QLatin1String( "literal" ) )
3077 {
3078 valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toList().at( 1 ).toStringList().join( ',' ) );
3079 }
3080 else
3081 {
3082 valueString = QStringLiteral( "array(%1)" ).arg( stepValue.toStringList().join( ',' ) );
3083 }
3084 break;
3085 }
3086 }
3087 }
3088
3089 if ( i > 1 )
3090 {
3091 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
3092 caseString += QStringLiteral( " WHEN %1 >= %2 THEN (%3) " ).arg( expression, stepKey, valueString );
3093 }
3094 else
3095 {
3096 caseString += QStringLiteral( "ELSE (%1) END" ).arg( valueString );
3097 }
3098 }
3099 return QgsProperty::fromExpression( caseString );
3100}
3101
3102QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateListByZoom( const QVariantList &json, PropertyType type, QgsMapBoxGlStyleConversionContext &context, double multiplier, int maxOpacity, QColor *defaultColor, double *defaultNumber )
3103{
3104 if ( json.value( 0 ).toString() != QLatin1String( "interpolate" ) )
3105 {
3106 context.pushWarning( QObject::tr( "%1: Could not interpret value list" ).arg( context.layerId() ) );
3107 return QgsProperty();
3108 }
3109
3110 double base = 1;
3111 const QString technique = json.value( 1 ).toList().value( 0 ).toString();
3112 if ( technique == QLatin1String( "linear" ) )
3113 base = 1;
3114 else if ( technique == QLatin1String( "exponential" ) )
3115 base = json.value( 1 ).toList(). value( 1 ).toDouble();
3116 else if ( technique == QLatin1String( "cubic-bezier" ) )
3117 {
3118 context.pushWarning( QObject::tr( "%1: Cubic-bezier interpolation is not supported, linear used instead." ).arg( context.layerId() ) );
3119 base = 1;
3120 }
3121 else
3122 {
3123 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation method %2" ).arg( context.layerId(), technique ) );
3124 return QgsProperty();
3125 }
3126
3127 if ( json.value( 2 ).toList().value( 0 ).toString() != QLatin1String( "zoom" ) )
3128 {
3129 context.pushWarning( QObject::tr( "%1: Skipping not implemented interpolation input %2" ).arg( context.layerId(), json.value( 2 ).toString() ) );
3130 return QgsProperty();
3131 }
3132
3133 // Convert stops into list of lists
3134 QVariantList stops;
3135 for ( int i = 3; i < json.length(); i += 2 )
3136 {
3137 stops.push_back( QVariantList() << json.value( i ).toString() << json.value( i + 1 ) );
3138 }
3139
3140 QVariantMap props;
3141 props.insert( QStringLiteral( "stops" ), stops );
3142 props.insert( QStringLiteral( "base" ), base );
3143 switch ( type )
3144 {
3146 return parseInterpolateColorByZoom( props, context, defaultColor );
3147
3149 return parseInterpolateByZoom( props, context, multiplier, defaultNumber );
3150
3152 return parseInterpolateOpacityByZoom( props, maxOpacity, &context );
3153
3155 return parseInterpolatePointByZoom( props, context, multiplier );
3156
3158 context.pushWarning( QObject::tr( "%1: Skipping unsupported numeric array in interpolate" ).arg( context.layerId() ) );
3159 return QgsProperty();
3160
3161 }
3162 return QgsProperty();
3163}
3164
3166{
3167 if ( ( QMetaType::Type )colorExpression.userType() == QMetaType::QVariantList )
3168 {
3169 return parseExpression( colorExpression.toList(), context, true );
3170 }
3171 return parseValue( colorExpression, context, true );
3172}
3173
3175{
3176 if ( color.userType() != QMetaType::Type::QString )
3177 {
3178 context.pushWarning( QObject::tr( "%1: Could not parse non-string color %2, skipping" ).arg( context.layerId(), color.toString() ) );
3179 return QColor();
3180 }
3181
3182 return QgsSymbolLayerUtils::parseColor( color.toString() );
3183}
3184
3185void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
3186{
3187 hue = std::max( 0, color.hslHue() );
3188 saturation = color.hslSaturation() / 255.0 * 100;
3189 lightness = color.lightness() / 255.0 * 100;
3190 alpha = color.alpha();
3191}
3192
3193QString QgsMapBoxGlStyleConverter::interpolateExpression( double zoomMin, double zoomMax, QVariant valueMin, QVariant valueMax, double base, double multiplier, QgsMapBoxGlStyleConversionContext *contextPtr )
3194{
3196 if ( contextPtr )
3197 {
3198 context = *contextPtr;
3199 }
3200
3201 // special case where min = max !
3202 if ( valueMin.canConvert( QMetaType::Double ) && valueMax.canConvert( QMetaType::Double ) )
3203 {
3204 bool minDoubleOk = true;
3205 const double min = valueMin.toDouble( &minDoubleOk );
3206 bool maxDoubleOk = true;
3207 const double max = valueMax.toDouble( &maxDoubleOk );
3208 if ( minDoubleOk && maxDoubleOk && qgsDoubleNear( min, max ) )
3209 {
3210 return QString::number( min * multiplier );
3211 }
3212 }
3213
3214 QString minValueExpr = valueMin.toString();
3215 QString maxValueExpr = valueMax.toString();
3216 if ( valueMin.userType() == QMetaType::Type::QVariantList )
3217 {
3218 minValueExpr = parseExpression( valueMin.toList(), context );
3219 }
3220 if ( valueMax.userType() == QMetaType::Type::QVariantList )
3221 {
3222 maxValueExpr = parseExpression( valueMax.toList(), context );
3223 }
3224
3225 QString expression;
3226 if ( minValueExpr == maxValueExpr )
3227 {
3228 expression = minValueExpr;
3229 }
3230 else
3231 {
3232 if ( base == 1 )
3233 {
3234 expression = QStringLiteral( "scale_linear(@vector_tile_zoom,%1,%2,%3,%4)" ).arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr );
3235 }
3236 else
3237 {
3238 expression = QStringLiteral( "scale_exponential(@vector_tile_zoom,%1,%2,%3,%4,%5)" ).arg( zoomMin ).arg( zoomMax ).arg( minValueExpr ).arg( maxValueExpr ).arg( base );
3239 }
3240 }
3241
3242 if ( multiplier != 1 )
3243 return QStringLiteral( "(%1) * %2" ).arg( expression ).arg( multiplier );
3244 else
3245 return expression;
3246}
3247
3248Qt::PenCapStyle QgsMapBoxGlStyleConverter::parseCapStyle( const QString &style )
3249{
3250 if ( style == QLatin1String( "round" ) )
3251 return Qt::RoundCap;
3252 else if ( style == QLatin1String( "square" ) )
3253 return Qt::SquareCap;
3254 else
3255 return Qt::FlatCap; // "butt" is default
3256}
3257
3258Qt::PenJoinStyle QgsMapBoxGlStyleConverter::parseJoinStyle( const QString &style )
3259{
3260 if ( style == QLatin1String( "bevel" ) )
3261 return Qt::BevelJoin;
3262 else if ( style == QLatin1String( "round" ) )
3263 return Qt::RoundJoin;
3264 else
3265 return Qt::MiterJoin; // "miter" is default
3266}
3267
3268QString QgsMapBoxGlStyleConverter::parseExpression( const QVariantList &expression, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3269{
3270 QString op = expression.value( 0 ).toString();
3271 if ( op == QLatin1String( "%" ) && expression.size() >= 3 )
3272 {
3273 return QStringLiteral( "%1 %2 %3" ).arg( parseValue( expression.value( 1 ), context ),
3274 op,
3275 parseValue( expression.value( 2 ), context ) );
3276 }
3277 else if ( op == QLatin1String( "to-number" ) )
3278 {
3279 return QStringLiteral( "to_real(%1)" ).arg( parseValue( expression.value( 1 ), context ) );
3280 }
3281 if ( op == QLatin1String( "literal" ) )
3282 {
3283 return expression.value( 1 ).toString();
3284 }
3285 else if ( op == QLatin1String( "all" )
3286 || op == QLatin1String( "any" )
3287 || op == QLatin1String( "none" ) )
3288 {
3289 QStringList parts;
3290 for ( int i = 1; i < expression.size(); ++i )
3291 {
3292 const QString part = parseValue( expression.at( i ), context );
3293 if ( part.isEmpty() )
3294 {
3295 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3296 return QString();
3297 }
3298 parts << part;
3299 }
3300
3301 if ( op == QLatin1String( "none" ) )
3302 return QStringLiteral( "NOT (%1)" ).arg( parts.join( QLatin1String( ") AND NOT (" ) ) );
3303
3304 QString operatorString;
3305 if ( op == QLatin1String( "all" ) )
3306 operatorString = QStringLiteral( ") AND (" );
3307 else if ( op == QLatin1String( "any" ) )
3308 operatorString = QStringLiteral( ") OR (" );
3309
3310 return QStringLiteral( "(%1)" ).arg( parts.join( operatorString ) );
3311 }
3312 else if ( op == '!' )
3313 {
3314 // ! inverts next expression's meaning
3315 QVariantList contraJsonExpr = expression.value( 1 ).toList();
3316 contraJsonExpr[0] = QString( op + contraJsonExpr[0].toString() );
3317 // ['!', ['has', 'level']] -> ['!has', 'level']
3318 return parseKey( contraJsonExpr, context );
3319 }
3320 else if ( op == QLatin1String( "==" )
3321 || op == QLatin1String( "!=" )
3322 || op == QLatin1String( ">=" )
3323 || op == '>'
3324 || op == QLatin1String( "<=" )
3325 || op == '<' )
3326 {
3327 // use IS and NOT IS instead of = and != because they can deal with NULL values
3328 if ( op == QLatin1String( "==" ) )
3329 op = QStringLiteral( "IS" );
3330 else if ( op == QLatin1String( "!=" ) )
3331 op = QStringLiteral( "IS NOT" );
3332 return QStringLiteral( "%1 %2 %3" ).arg( parseKey( expression.value( 1 ), context ),
3333 op, parseValue( expression.value( 2 ), context ) );
3334 }
3335 else if ( op == QLatin1String( "has" ) )
3336 {
3337 return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NOT NULL" );
3338 }
3339 else if ( op == QLatin1String( "!has" ) )
3340 {
3341 return parseKey( expression.value( 1 ), context ) + QStringLiteral( " IS NULL" );
3342 }
3343 else if ( op == QLatin1String( "in" ) || op == QLatin1String( "!in" ) )
3344 {
3345 const QString key = parseKey( expression.value( 1 ), context );
3346 QStringList parts;
3347
3348 QVariantList values = expression.mid( 2 );
3349 if ( expression.size() == 3
3350 && expression.at( 2 ).userType() == QMetaType::Type::QVariantList && expression.at( 2 ).toList().count() > 1
3351 && expression.at( 2 ).toList().at( 0 ).toString() == QLatin1String( "literal" ) )
3352 {
3353 values = expression.at( 2 ).toList().at( 1 ).toList();
3354 }
3355
3356 for ( const QVariant &value : std::as_const( values ) )
3357 {
3358 const QString part = parseValue( value, context );
3359 if ( part.isEmpty() )
3360 {
3361 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3362 return QString();
3363 }
3364 parts << part;
3365 }
3366 if ( op == QLatin1String( "in" ) )
3367 return QStringLiteral( "%1 IN (%2)" ).arg( key, parts.join( QLatin1String( ", " ) ) );
3368 else
3369 return QStringLiteral( "(%1 IS NULL OR %1 NOT IN (%2))" ).arg( key, parts.join( QLatin1String( ", " ) ) );
3370 }
3371 else if ( op == QLatin1String( "get" ) )
3372 {
3373 return parseKey( expression.value( 1 ), context );
3374 }
3375 else if ( op == QLatin1String( "match" ) )
3376 {
3377 const QString attribute = expression.value( 1 ).toList().value( 1 ).toString();
3378
3379 if ( expression.size() == 5
3380 && expression.at( 3 ).userType() == QMetaType::Type::Bool && expression.at( 3 ).toBool() == true
3381 && expression.at( 4 ).userType() == QMetaType::Type::Bool && expression.at( 4 ).toBool() == false )
3382 {
3383 // simple case, make a nice simple expression instead of a CASE statement
3384 if ( expression.at( 2 ).userType() == QMetaType::Type::QVariantList || expression.at( 2 ).userType() == QMetaType::Type::QStringList )
3385 {
3386 QStringList parts;
3387 for ( const QVariant &p : expression.at( 2 ).toList() )
3388 {
3389 parts << parseValue( p, context );
3390 }
3391
3392 if ( parts.size() > 1 )
3393 return QStringLiteral( "%1 IN (%2)" ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3394 else
3395 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ).toList().value( 0 ) );
3396 }
3397 else if ( expression.at( 2 ).userType() == QMetaType::Type::QString || expression.at( 2 ).userType() == QMetaType::Type::Int
3398 || expression.at( 2 ).userType() == QMetaType::Type::Double || expression.at( 2 ).userType() == QMetaType::Type::LongLong )
3399 {
3400 return QgsExpression::createFieldEqualityExpression( attribute, expression.at( 2 ) );
3401 }
3402 else
3403 {
3404 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression" ).arg( context.layerId() ) );
3405 return QString();
3406 }
3407 }
3408 else
3409 {
3410 QString caseString = QStringLiteral( "CASE " );
3411 for ( int i = 2; i < expression.size() - 2; i += 2 )
3412 {
3413 if ( expression.at( i ).userType() == QMetaType::Type::QVariantList || expression.at( i ).userType() == QMetaType::Type::QStringList )
3414 {
3415 QStringList parts;
3416 for ( const QVariant &p : expression.at( i ).toList() )
3417 {
3418 parts << QgsExpression::quotedValue( p );
3419 }
3420
3421 if ( parts.size() > 1 )
3422 caseString += QStringLiteral( "WHEN %1 IN (%2) " ).arg( QgsExpression::quotedColumnRef( attribute ), parts.join( ", " ) );
3423 else
3424 caseString += QStringLiteral( "WHEN %1 " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ).toList().value( 0 ) ) );
3425 }
3426 else if ( expression.at( i ).userType() == QMetaType::Type::QString || expression.at( i ).userType() == QMetaType::Type::Int
3427 || expression.at( i ).userType() == QMetaType::Type::Double || expression.at( i ).userType() == QMetaType::Type::LongLong )
3428 {
3429 caseString += QStringLiteral( "WHEN (%1) " ).arg( QgsExpression::createFieldEqualityExpression( attribute, expression.at( i ) ) );
3430 }
3431
3432 caseString += QStringLiteral( "THEN %1 " ).arg( parseValue( expression.at( i + 1 ), context, colorExpected ) );
3433 }
3434 caseString += QStringLiteral( "ELSE %1 END" ).arg( parseValue( expression.last(), context, colorExpected ) );
3435 return caseString;
3436 }
3437 }
3438 else if ( op == QLatin1String( "to-string" ) )
3439 {
3440 return QStringLiteral( "to_string(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
3441 }
3442 else if ( op == QLatin1String( "to-boolean" ) )
3443 {
3444 return QStringLiteral( "to_bool(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
3445 }
3446 else if ( op == QLatin1String( "case" ) )
3447 {
3448 QString caseString = QStringLiteral( "CASE" );
3449 for ( int i = 1; i < expression.size() - 2; i += 2 )
3450 {
3451 const QString condition = parseExpression( expression.value( i ).toList(), context );
3452 const QString value = parseValue( expression.value( i + 1 ), context );
3453 caseString += QStringLiteral( " WHEN (%1) THEN %2" ).arg( condition, value );
3454 }
3455 const QString value = parseValue( expression.constLast(), context );
3456 caseString += QStringLiteral( " ELSE %1 END" ).arg( value );
3457 return caseString;
3458 }
3459 else if ( op == QLatin1String( "zoom" ) && expression.count() == 1 )
3460 {
3461 return QStringLiteral( "@vector_tile_zoom" );
3462 }
3463 else if ( op == QLatin1String( "concat" ) )
3464 {
3465 QString concatString = QStringLiteral( "concat(" );
3466 for ( int i = 1; i < expression.size(); i++ )
3467 {
3468 if ( i > 1 )
3469 concatString += QLatin1String( ", " );
3470 concatString += parseValue( expression.value( i ), context );
3471 }
3472 concatString += QLatin1Char( ')' );
3473 return concatString;
3474 }
3475 else if ( op == QLatin1String( "length" ) )
3476 {
3477 return QStringLiteral( "length(%1)" ).arg( parseExpression( expression.value( 1 ).toList(), context ) );
3478 }
3479 else if ( op == QLatin1String( "step" ) )
3480 {
3481 const QString stepExpression = parseExpression( expression.value( 1 ).toList(), context );
3482 if ( stepExpression.isEmpty() )
3483 {
3484 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3485 return QString();
3486 }
3487
3488 QString caseString = QStringLiteral( "CASE " );
3489
3490 for ( int i = expression.length() - 2; i > 0; i -= 2 )
3491 {
3492 const QString stepValue = parseValue( expression.value( i + 1 ), context, colorExpected );
3493 if ( i > 1 )
3494 {
3495 const QString stepKey = QgsExpression::quotedValue( expression.value( i ) );
3496 caseString += QStringLiteral( " WHEN %1 >= %2 THEN (%3) " ).arg( stepExpression, stepKey, stepValue );
3497 }
3498 else
3499 {
3500 caseString += QStringLiteral( "ELSE (%1) END" ).arg( stepValue );
3501 }
3502 }
3503 return caseString;
3504 }
3505 else
3506 {
3507 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression \"%2\"" ).arg( context.layerId(), op ) );
3508 return QString();
3509 }
3510}
3511
3512QImage QgsMapBoxGlStyleConverter::retrieveSprite( const QString &name, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize )
3513{
3514 if ( context.spriteImage().isNull() )
3515 {
3516 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3517 return QImage();
3518 }
3519
3520 const QVariantMap spriteDefinition = context.spriteDefinitions().value( name ).toMap();
3521 if ( spriteDefinition.size() == 0 )
3522 {
3523 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3524 return QImage();
3525 }
3526
3527 const QImage sprite = context.spriteImage().copy( spriteDefinition.value( QStringLiteral( "x" ) ).toInt(),
3528 spriteDefinition.value( QStringLiteral( "y" ) ).toInt(),
3529 spriteDefinition.value( QStringLiteral( "width" ) ).toInt(),
3530 spriteDefinition.value( QStringLiteral( "height" ) ).toInt() );
3531 if ( sprite.isNull() )
3532 {
3533 context.pushWarning( QObject::tr( "%1: Could not retrieve sprite '%2'" ).arg( context.layerId(), name ) );
3534 return QImage();
3535 }
3536
3537 spriteSize = sprite.size() / spriteDefinition.value( QStringLiteral( "pixelRatio" ) ).toDouble() * context.pixelSizeConversionFactor();
3538 return sprite;
3539}
3540
3541QString QgsMapBoxGlStyleConverter::retrieveSpriteAsBase64WithProperties( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, QSize &spriteSize, QString &spriteProperty, QString &spriteSizeProperty )
3542{
3543 QString spritePath;
3544
3545 auto prepareBase64 = []( const QImage & sprite )
3546 {
3547 QString path;
3548 if ( !sprite.isNull() )
3549 {
3550 QByteArray blob;
3551 QBuffer buffer( &blob );
3552 buffer.open( QIODevice::WriteOnly );
3553 sprite.save( &buffer, "PNG" );
3554 buffer.close();
3555 const QByteArray encoded = blob.toBase64();
3556 path = QString( encoded );
3557 path.prepend( QLatin1String( "base64:" ) );
3558 }
3559 return path;
3560 };
3561
3562 switch ( value.userType() )
3563 {
3564 case QMetaType::Type::QString:
3565 {
3566 QString spriteName = value.toString();
3567 const thread_local QRegularExpression fieldNameMatch( QStringLiteral( "{([^}]+)}" ) );
3568 QRegularExpressionMatch match = fieldNameMatch.match( spriteName );
3569 if ( match.hasMatch() )
3570 {
3571 const QString fieldName = match.captured( 1 );
3572 spriteProperty = QStringLiteral( "CASE" );
3573 spriteSizeProperty = QStringLiteral( "CASE" );
3574
3575 spriteName.replace( "(", QLatin1String( "\\(" ) );
3576 spriteName.replace( ")", QLatin1String( "\\)" ) );
3577 spriteName.replace( fieldNameMatch, QStringLiteral( "([^\\/\\\\]+)" ) );
3578 const QRegularExpression fieldValueMatch( spriteName );
3579 const QStringList spriteNames = context.spriteDefinitions().keys();
3580 for ( const QString &name : spriteNames )
3581 {
3582 match = fieldValueMatch.match( name );
3583 if ( match.hasMatch() )
3584 {
3585 QSize size;
3586 QString path;
3587 const QString fieldValue = match.captured( 1 );
3588 const QImage sprite = retrieveSprite( name, context, size );
3589 path = prepareBase64( sprite );
3590 if ( spritePath.isEmpty() && !path.isEmpty() )
3591 {
3592 spritePath = path;
3593 spriteSize = size;
3594 }
3595
3596 spriteProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN '%3'" )
3597 .arg( fieldName, fieldValue, path );
3598 spriteSizeProperty += QStringLiteral( " WHEN \"%1\" = '%2' THEN %3" )
3599 .arg( fieldName ).arg( fieldValue ).arg( size.width() );
3600 }
3601 }
3602
3603 spriteProperty += QLatin1String( " END" );
3604 spriteSizeProperty += QLatin1String( " END" );
3605 }
3606 else
3607 {
3608 spriteProperty.clear();
3609 spriteSizeProperty.clear();
3610 const QImage sprite = retrieveSprite( spriteName, context, spriteSize );
3611 spritePath = prepareBase64( sprite );
3612 }
3613 break;
3614 }
3615
3616 case QMetaType::Type::QVariantMap:
3617 {
3618 const QVariantList stops = value.toMap().value( QStringLiteral( "stops" ) ).toList();
3619 if ( stops.size() == 0 )
3620 break;
3621
3622 QString path;
3623 QSize size;
3624 QImage sprite;
3625
3626 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, spriteSize );
3627 spritePath = prepareBase64( sprite );
3628
3629 spriteProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN '%2'" )
3630 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3631 .arg( spritePath );
3632 spriteSizeProperty = QStringLiteral( "CASE WHEN @vector_tile_zoom < %1 THEN %2" )
3633 .arg( stops.value( 0 ).toList().value( 0 ).toString() )
3634 .arg( spriteSize.width() );
3635
3636 for ( int i = 0; i < stops.size() - 1; ++i )
3637 {
3638 ;
3639 sprite = retrieveSprite( stops.value( 0 ).toList().value( 1 ).toString(), context, size );
3640 path = prepareBase64( sprite );
3641
3642 spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3643 "THEN '%3'" )
3644 .arg( stops.value( i ).toList().value( 0 ).toString(),
3645 stops.value( i + 1 ).toList().value( 0 ).toString(),
3646 path );
3647 spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 AND @vector_tile_zoom < %2 "
3648 "THEN %3" )
3649 .arg( stops.value( i ).toList().value( 0 ).toString(),
3650 stops.value( i + 1 ).toList().value( 0 ).toString() )
3651 .arg( size.width() );
3652 }
3653 sprite = retrieveSprite( stops.last().toList().value( 1 ).toString(), context, size );
3654 path = prepareBase64( sprite );
3655
3656 spriteProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3657 "THEN '%2' END" )
3658 .arg( stops.last().toList().value( 0 ).toString() )
3659 .arg( path );
3660 spriteSizeProperty += QStringLiteral( " WHEN @vector_tile_zoom >= %1 "
3661 "THEN %2 END" )
3662 .arg( stops.last().toList().value( 0 ).toString() )
3663 .arg( size.width() );
3664 break;
3665 }
3666
3667 case QMetaType::Type::QVariantList:
3668 {
3669 const QVariantList json = value.toList();
3670 const QString method = json.value( 0 ).toString();
3671
3672 if ( method == QLatin1String( "match" ) )
3673 {
3674 const QString attribute = parseExpression( json.value( 1 ).toList(), context );
3675 if ( attribute.isEmpty() )
3676 {
3677 context.pushWarning( QObject::tr( "%1: Could not interpret match list" ).arg( context.layerId() ) );
3678 break;
3679 }
3680
3681 spriteProperty = QStringLiteral( "CASE" );
3682 spriteSizeProperty = QStringLiteral( "CASE" );
3683
3684 for ( int i = 2; i < json.length() - 1; i += 2 )
3685 {
3686 const QVariant matchKey = json.value( i );
3687 const QVariant matchValue = json.value( i + 1 );
3688 QString matchString;
3689 switch ( matchKey.userType() )
3690 {
3691 case QMetaType::Type::QVariantList:
3692 case QMetaType::Type::QStringList:
3693 {
3694 const QVariantList keys = matchKey.toList();
3695 QStringList matchStringList;
3696 for ( const QVariant &key : keys )
3697 {
3698 matchStringList << QgsExpression::quotedValue( key );
3699 }
3700 matchString = matchStringList.join( ',' );
3701 break;
3702 }
3703
3704 case QMetaType::Type::Bool:
3705 case QMetaType::Type::QString:
3706 case QMetaType::Type::Int:
3707 case QMetaType::Type::LongLong:
3708 case QMetaType::Type::Double:
3709 {
3710 matchString = QgsExpression::quotedValue( matchKey );
3711 break;
3712 }
3713
3714 default:
3715 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
3716 break;
3717
3718 }
3719
3720 const QImage sprite = retrieveSprite( matchValue.toString(), context, spriteSize );
3721 spritePath = prepareBase64( sprite );
3722
3723 spriteProperty += QStringLiteral( " WHEN %1 IN (%2) "
3724 "THEN '%3'" ).arg( attribute,
3725 matchString,
3726 spritePath );
3727
3728 spriteSizeProperty += QStringLiteral( " WHEN %1 IN (%2) "
3729 "THEN %3" ).arg( attribute,
3730 matchString ).arg( spriteSize.width() );
3731 }
3732
3733 if ( !json.constLast().toString().isEmpty() )
3734 {
3735 const QImage sprite = retrieveSprite( json.constLast().toString(), context, spriteSize );
3736 spritePath = prepareBase64( sprite );
3737 }
3738 else
3739 {
3740 spritePath = QString();
3741 }
3742
3743 spriteProperty += QStringLiteral( " ELSE '%1' END" ).arg( spritePath );
3744 spriteSizeProperty += QStringLiteral( " ELSE %3 END" ).arg( spriteSize.width() );
3745 break;
3746 }
3747 else if ( method == QLatin1String( "step" ) )
3748 {
3749 const QString expression = parseExpression( json.value( 1 ).toList(), context );
3750 if ( expression.isEmpty() )
3751 {
3752 context.pushWarning( QObject::tr( "%1: Could not interpret step list" ).arg( context.layerId() ) );
3753 break;
3754 }
3755
3756 spriteProperty = QStringLiteral( "CASE" );
3757 spriteSizeProperty = QStringLiteral( "CASE" );
3758 for ( int i = json.length() - 2; i > 2; i -= 2 )
3759 {
3760 const QString stepKey = QgsExpression::quotedValue( json.value( i ) );
3761 const QString stepValue = json.value( i + 1 ).toString();
3762
3763 const QImage sprite = retrieveSprite( stepValue, context, spriteSize );
3764 spritePath = prepareBase64( sprite );
3765
3766 spriteProperty += QStringLiteral( " WHEN %1 >= %2 THEN '%3' " ).arg( expression, stepKey, spritePath );
3767 spriteSizeProperty += QStringLiteral( " WHEN %1 >= %2 THEN %3 " ).arg( expression ).arg( stepKey ).arg( spriteSize.width() );
3768 }
3769
3770 const QImage sprite = retrieveSprite( json.at( 2 ).toString(), context, spriteSize );
3771 spritePath = prepareBase64( sprite );
3772
3773 spriteProperty += QStringLiteral( "ELSE '%1' END" ).arg( spritePath );
3774 spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3775 break;
3776 }
3777 else if ( method == QLatin1String( "case" ) )
3778 {
3779 spriteProperty = QStringLiteral( "CASE" );
3780 spriteSizeProperty = QStringLiteral( "CASE" );
3781 for ( int i = 1; i < json.length() - 2; i += 2 )
3782 {
3783 const QString caseExpression = parseExpression( json.value( i ).toList(), context );
3784 const QString caseValue = json.value( i + 1 ).toString();
3785
3786 const QImage sprite = retrieveSprite( caseValue, context, spriteSize );
3787 spritePath = prepareBase64( sprite );
3788
3789 spriteProperty += QStringLiteral( " WHEN %1 THEN '%2' " ).arg( caseExpression, spritePath );
3790 spriteSizeProperty += QStringLiteral( " WHEN %1 THEN %2 " ).arg( caseExpression ).arg( spriteSize.width() );
3791 }
3792 const QImage sprite = retrieveSprite( json.last().toString(), context, spriteSize );
3793 spritePath = prepareBase64( sprite );
3794
3795 spriteProperty += QStringLiteral( "ELSE '%1' END" ).arg( spritePath );
3796 spriteSizeProperty += QStringLiteral( "ELSE %3 END" ).arg( spriteSize.width() );
3797 break;
3798 }
3799 else
3800 {
3801 context.pushWarning( QObject::tr( "%1: Could not interpret sprite value list with method %2" ).arg( context.layerId(), method ) );
3802 break;
3803 }
3804 }
3805
3806 default:
3807 context.pushWarning( QObject::tr( "%1: Skipping unsupported sprite type (%2)." ).arg( context.layerId(), QMetaType::typeName( static_cast<QMetaType::Type>( value.userType() ) ) ) );
3808 break;
3809 }
3810
3811 return spritePath;
3812}
3813
3814QString QgsMapBoxGlStyleConverter::parseValue( const QVariant &value, QgsMapBoxGlStyleConversionContext &context, bool colorExpected )
3815{
3816 QColor c;
3817 switch ( value.userType() )
3818 {
3819 case QMetaType::Type::QVariantList:
3820 case QMetaType::Type::QStringList:
3821 return parseExpression( value.toList(), context, colorExpected );
3822
3823 case QMetaType::Type::Bool:
3824 case QMetaType::Type::QString:
3825 if ( colorExpected )
3826 {
3827 QColor c = parseColor( value, context );
3828 if ( c.isValid() )
3829 {
3830 return parseValue( c, context );
3831 }
3832 }
3833 return QgsExpression::quotedValue( value );
3834
3835 case QMetaType::Type::Int:
3836 case QMetaType::Type::LongLong:
3837 case QMetaType::Type::Double:
3838 return value.toString();
3839
3840 case QMetaType::Type::QColor:
3841 c = value.value<QColor>();
3842 return QString( "color_rgba(%1,%2,%3,%4)" ).arg( c.red() ).arg( c.green() ).arg( c.blue() ).arg( c.alpha() );
3843
3844 default:
3845 context.pushWarning( QObject::tr( "%1: Skipping unsupported expression part" ).arg( context.layerId() ) );
3846 break;
3847 }
3848 return QString();
3849}
3850
3851QString QgsMapBoxGlStyleConverter::parseKey( const QVariant &value, QgsMapBoxGlStyleConversionContext &context )
3852{
3853 if ( value.toString() == QLatin1String( "$type" ) )
3854 {
3855 return QStringLiteral( "_geom_type" );
3856 }
3857 if ( value.toString() == QLatin1String( "level" ) )
3858 {
3859 return QStringLiteral( "level" );
3860 }
3861 else if ( ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() == 1 ) || value.userType() == QMetaType::Type::QStringList )
3862 {
3863 if ( value.toList().size() > 1 )
3864 return value.toList().at( 1 ).toString();
3865 else
3866 {
3867 QString valueString = value.toList().value( 0 ).toString();
3868 if ( valueString == QLatin1String( "geometry-type" ) )
3869 {
3870 return QStringLiteral( "_geom_type" );
3871 }
3872 return valueString;
3873 }
3874 }
3875 else if ( value.userType() == QMetaType::Type::QVariantList && value.toList().size() > 1 )
3876 {
3877 return parseExpression( value.toList(), context );
3878 }
3879 return QgsExpression::quotedColumnRef( value.toString() );
3880}
3881
3882QString QgsMapBoxGlStyleConverter::processLabelField( const QString &string, bool &isExpression )
3883{
3884 // {field_name} is permitted in string -- if multiple fields are present, convert them to an expression
3885 // but if single field is covered in {}, return it directly
3886 const thread_local QRegularExpression singleFieldRx( QStringLiteral( "^{([^}]+)}$" ) );
3887 const QRegularExpressionMatch match = singleFieldRx.match( string );
3888 if ( match.hasMatch() )
3889 {
3890 isExpression = false;
3891 return match.captured( 1 );
3892 }
3893
3894 const thread_local QRegularExpression multiFieldRx( QStringLiteral( "(?={[^}]+})" ) );
3895 const QStringList parts = string.split( multiFieldRx );
3896 if ( parts.size() > 1 )
3897 {
3898 isExpression = true;
3899
3900 QStringList res;
3901 for ( const QString &part : parts )
3902 {
3903 if ( part.isEmpty() )
3904 continue;
3905
3906 if ( !part.contains( '{' ) )
3907 {
3908 res << QgsExpression::quotedValue( part );
3909 continue;
3910 }
3911
3912 // part will start at a {field} reference
3913 const QStringList split = part.split( '}' );
3914 res << QgsExpression::quotedColumnRef( split.at( 0 ).mid( 1 ) );
3915 if ( !split.at( 1 ).isEmpty() )
3916 res << QgsExpression::quotedValue( split.at( 1 ) );
3917 }
3918 return QStringLiteral( "concat(%1)" ).arg( res.join( ',' ) );
3919 }
3920 else
3921 {
3922 isExpression = false;
3923 return string;
3924 }
3925}
3926
3928{
3929 return mRenderer ? mRenderer->clone() : nullptr;
3930}
3931
3933{
3934 return mLabeling ? mLabeling->clone() : nullptr;
3935}
3936
3937QList<QgsMapBoxGlStyleAbstractSource *> QgsMapBoxGlStyleConverter::sources()
3938{
3939 return mSources;
3940}
3941
3942QList<QgsMapBoxGlStyleRasterSubLayer> QgsMapBoxGlStyleConverter::rasterSubLayers() const
3943{
3944 return mRasterSubLayers;
3945}
3946
3948{
3949 QList<QgsMapLayer *> subLayers;
3950 for ( const QgsMapBoxGlStyleRasterSubLayer &subLayer : mRasterSubLayers )
3951 {
3952 const QString sourceName = subLayer.source();
3953 std::unique_ptr< QgsRasterLayer > rl;
3954 for ( const QgsMapBoxGlStyleAbstractSource *source : mSources )
3955 {
3956 if ( source->type() == Qgis::MapBoxGlStyleSourceType::Raster && source->name() == sourceName )
3957 {
3958 const QgsMapBoxGlStyleRasterSource *rasterSource = qgis::down_cast< const QgsMapBoxGlStyleRasterSource * >( source );
3959 rl.reset( rasterSource->toRasterLayer() );
3960 rl->pipe()->setDataDefinedProperties( subLayer.dataDefinedProperties() );
3961 break;
3962 }
3963 }
3964
3965 if ( rl )
3966 {
3967 subLayers.append( rl.release() );
3968 }
3969 }
3970 return subLayers;
3971}
3972
3973
3975{
3976 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
3977 if ( !context )
3978 {
3979 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
3980 context = tmpContext.get();
3981 }
3982
3983 auto typeFromString = [context]( const QString & string, const QString & name )->Qgis::MapBoxGlStyleSourceType
3984 {
3985 if ( string.compare( QLatin1String( "vector" ), Qt::CaseInsensitive ) == 0 )
3987 else if ( string.compare( QLatin1String( "raster" ), Qt::CaseInsensitive ) == 0 )
3989 else if ( string.compare( QLatin1String( "raster-dem" ), Qt::CaseInsensitive ) == 0 )
3991 else if ( string.compare( QLatin1String( "geojson" ), Qt::CaseInsensitive ) == 0 )
3993 else if ( string.compare( QLatin1String( "image" ), Qt::CaseInsensitive ) == 0 )
3995 else if ( string.compare( QLatin1String( "video" ), Qt::CaseInsensitive ) == 0 )
3997 context->pushWarning( QObject::tr( "Invalid source type \"%1\" for source \"%2\"" ).arg( string, name ) );
3999 };
4000
4001 for ( auto it = sources.begin(); it != sources.end(); ++it )
4002 {
4003 const QString name = it.key();
4004 const QVariantMap jsonSource = it.value().toMap();
4005 const QString typeString = jsonSource.value( QStringLiteral( "type" ) ).toString();
4006
4007 const Qgis::MapBoxGlStyleSourceType type = typeFromString( typeString, name );
4008
4009 switch ( type )
4010 {
4012 parseRasterSource( jsonSource, name, context );
4013 break;
4020 QgsDebugError( QStringLiteral( "Ignoring vector tile style source %1 (%2)" ).arg( name, qgsEnumValueToKey( type ) ) );
4021 continue;
4022 }
4023 }
4024}
4025
4026void QgsMapBoxGlStyleConverter::parseRasterSource( const QVariantMap &source, const QString &name, QgsMapBoxGlStyleConversionContext *context )
4027{
4028 std::unique_ptr< QgsMapBoxGlStyleConversionContext > tmpContext;
4029 if ( !context )
4030 {
4031 tmpContext = std::make_unique< QgsMapBoxGlStyleConversionContext >();
4032 context = tmpContext.get();
4033 }
4034
4035 std::unique_ptr< QgsMapBoxGlStyleRasterSource > raster = std::make_unique< QgsMapBoxGlStyleRasterSource >( name );
4036 if ( raster->setFromJson( source, context ) )
4037 mSources.append( raster.release() );
4038}
4039
4040bool QgsMapBoxGlStyleConverter::numericArgumentsOnly( const QVariant &bottomVariant, const QVariant &topVariant, double &bottom, double &top )
4041{
4042 if ( bottomVariant.canConvert( QMetaType::Double ) && topVariant.canConvert( QMetaType::Double ) )
4043 {
4044 bool bDoubleOk, tDoubleOk;
4045 bottom = bottomVariant.toDouble( &bDoubleOk );
4046 top = topVariant.toDouble( &tDoubleOk );
4047 return ( bDoubleOk && tDoubleOk );
4048 }
4049 return false;
4050}
4051
4052//
4053// QgsMapBoxGlStyleConversionContext
4054//
4056{
4057 QgsDebugError( warning );
4058 mWarnings << warning;
4059}
4060
4062{
4063 return mTargetUnit;
4064}
4065
4067{
4068 mTargetUnit = targetUnit;
4069}
4070
4072{
4073 return mSizeConversionFactor;
4074}
4075
4077{
4078 mSizeConversionFactor = sizeConversionFactor;
4079}
4080
4082{
4083 return mSpriteImage;
4084}
4085
4087{
4088 return mSpriteDefinitions;
4089}
4090
4091void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QVariantMap &definitions )
4092{
4093 mSpriteImage = image;
4094 mSpriteDefinitions = definitions;
4095}
4096
4097void QgsMapBoxGlStyleConversionContext::setSprites( const QImage &image, const QString &definitions )
4098{
4099 setSprites( image, QgsJsonUtils::parseJson( definitions ).toMap() );
4100}
4101
4103{
4104 return mLayerId;
4105}
4106
4108{
4109 mLayerId = value;
4110}
4111
4112//
4113// QgsMapBoxGlStyleAbstractSource
4114//
4116 : mName( name )
4117{
4118}
4119
4121{
4122 return mName;
4123}
4124
4126
4127//
4128// QgsMapBoxGlStyleRasterSource
4129//
4130
4136
4141
4143{
4144 mAttribution = json.value( QStringLiteral( "attribution" ) ).toString();
4145
4146 const QString scheme = json.value( QStringLiteral( "scheme" ), QStringLiteral( "xyz" ) ).toString();
4147 if ( scheme.compare( QLatin1String( "xyz" ) ) == 0 )
4148 {
4149 // xyz scheme is supported
4150 }
4151 else
4152 {
4153 context->pushWarning( QObject::tr( "%1 scheme is not supported for raster source %2" ).arg( scheme, name() ) );
4154 return false;
4155 }
4156
4157 mMinZoom = json.value( QStringLiteral( "minzoom" ), QStringLiteral( "0" ) ).toInt();
4158 mMaxZoom = json.value( QStringLiteral( "maxzoom" ), QStringLiteral( "22" ) ).toInt();
4159 mTileSize = json.value( QStringLiteral( "tileSize" ), QStringLiteral( "512" ) ).toInt();
4160
4161 const QVariantList tiles = json.value( QStringLiteral( "tiles" ) ).toList();
4162 for ( const QVariant &tile : tiles )
4163 {
4164 mTiles.append( tile.toString() );
4165 }
4166
4167 return true;
4168}
4169
4171{
4172 QVariantMap parts;
4173 parts.insert( QStringLiteral( "type" ), QStringLiteral( "xyz" ) );
4174 parts.insert( QStringLiteral( "url" ), mTiles.value( 0 ) );
4175
4176 if ( mTileSize == 256 )
4177 parts.insert( QStringLiteral( "tilePixelRation" ), QStringLiteral( "1" ) );
4178 else if ( mTileSize == 512 )
4179 parts.insert( QStringLiteral( "tilePixelRation" ), QStringLiteral( "2" ) );
4180
4181 parts.insert( QStringLiteral( "zmax" ), QString::number( mMaxZoom ) );
4182 parts.insert( QStringLiteral( "zmin" ), QString::number( mMinZoom ) );
4183
4184 std::unique_ptr< QgsRasterLayer > rl = std::make_unique< QgsRasterLayer >( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "wms" ), parts ), name(), QStringLiteral( "wms" ) );
4185 return rl.release();
4186}
4187
4188//
4189// QgsMapBoxGlStyleRasterSubLayer
4190//
4192 : mId( id )
4193 , mSource( source )
4194{
4195
4196}
@ 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:5983
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:6257
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:6066
#define QgsDebugError(str)
Definition qgslogger.h:38
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30