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