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