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