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