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