Color work, some tests

This commit is contained in:
Nyall Dawson 2020-09-04 12:45:17 +10:00
parent 96c2ddbade
commit c4858b4231
4 changed files with 368 additions and 8 deletions

View File

@ -23,7 +23,7 @@ settings.
%End
public:
QgsMapBoxGlStyleConverter( const QVariantMap &style );
QgsMapBoxGlStyleConverter( const QVariantMap &style, const QString &styleName = QString() );
%Docstring
Constructor for QgsMapBoxGlStyleConverter.
@ -53,7 +53,51 @@ or ``None`` if the style could not be converted successfully.
protected:
void parseLayers( const QVariantList &layers );
void parseLayers( const QVariantList &layers, const QString &styleName );
%Docstring
Parse list of ``layers`` from JSON
%End
static bool parseFillLayer( const QVariantMap &jsonLayer, const QString &styleName, QgsVectorTileBasicRendererStyle &style /Out/ );
%Docstring
Parses a fill layer.
:param jsonLayer: fill layer to parse
:param styleName: style name
:return: - ``True`` if the layer was successfully parsed.
- style: generated QGIS vector tile style
%End
static QgsProperty parseInterpolateColorByZoom( const QVariantMap &json );
static QColor parseColor( const QVariant &color );
%Docstring
Parses a ``color`` in one of these supported formats:
- #fff or #ffffff
- hsl(30, 19%, 90%) or hsla(30, 19%, 90%, 0.4)
- rgb(10, 20, 30) or rgba(10, 20, 30, 0.5)
Returns an invalid color if the color could not be parsed.
%End
static void colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha );
%Docstring
Takes a QColor object and returns HSLA components in required format for QGIS :py:func:`~QgsMapBoxGlStyleConverter.color_hsla` expression function.
:param color: input color
:param hue: an integer value from 0 to 360
:param saturation: an integer value from 0 to 100
:param lightness: an integer value from 0 to 100
:param alpha: an integer value from 0 (completely transparent) to 255 (opaque).
%End
static QString interpolateExpression( int zoomMin, int zoomMax, double valueMin, double valueMax, double base );
%Docstring
Generates an interpolation for values between ``valueMin`` and ``valueMax``, scaled between the
ranges ``zoomMin`` to ``zoomMax``.
%End
private:
QgsMapBoxGlStyleConverter( const QgsMapBoxGlStyleConverter &other );

View File

@ -16,13 +16,17 @@
#include "qgsmapboxglstyleconverter.h"
#include "qgsvectortilebasicrenderer.h"
#include "qgsvectortilebasiclabeling.h"
#include "qgssymbollayer.h"
#include "qgssymbollayerutils.h"
#include "qgslogger.h"
QgsMapBoxGlStyleConverter::QgsMapBoxGlStyleConverter( const QVariantMap &style )
QgsMapBoxGlStyleConverter::QgsMapBoxGlStyleConverter( const QVariantMap &style, const QString &styleName )
: mStyle( style )
{
if ( mStyle.contains( QStringLiteral( "layers" ) ) )
{
parseLayers( mStyle.value( QStringLiteral( "layers" ) ).toList() );
parseLayers( mStyle.value( QStringLiteral( "layers" ) ).toList(), styleName );
}
else
{
@ -32,9 +36,254 @@ QgsMapBoxGlStyleConverter::QgsMapBoxGlStyleConverter( const QVariantMap &style )
QgsMapBoxGlStyleConverter::~QgsMapBoxGlStyleConverter() = default;
void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers )
void QgsMapBoxGlStyleConverter::parseLayers( const QVariantList &layers, const QString &styleName )
{
QList<QgsVectorTileBasicRendererStyle> rendererStyles;
QList<QgsVectorTileBasicLabelingStyle> labelingStyles;
for ( const QVariant &layer : layers )
{
const QVariantMap jsonLayer = layer.toMap();
const QString layerType = jsonLayer.value( QStringLiteral( "type" ) ).toString();
if ( layerType == QLatin1String( "background" ) )
continue;
const QString styleId = jsonLayer.value( QStringLiteral( "id" ) ).toString();
const QString layerName = jsonLayer.value( QStringLiteral( "source-layer" ) ).toString();
const int minZoom = jsonLayer.value( QStringLiteral( "minzoom" ), QStringLiteral( "-1" ) ).toInt();
const int maxZoom = jsonLayer.value( QStringLiteral( "maxzoom" ), QStringLiteral( "-1" ) ).toInt();
const bool enabled = jsonLayer.value( QStringLiteral( "visibility" ) ).toString() != QLatin1String( "none" );
QString filterExpression;
if ( jsonLayer.contains( QStringLiteral( "filter" ) ) )
{
// filterExpression = parseExpression( jsonLayer.value( QStringLiteral( "filter" ) ) );
}
QgsVectorTileBasicRendererStyle rendererStyle;
QgsVectorTileBasicLabelingStyle labelingStyle;
bool hasRendererStyle = false;
bool hasLabelingStyle = false;
if ( layerType == QLatin1String( "fill" ) )
{
hasRendererStyle = parseFillLayer( jsonLayer, styleName, rendererStyle );
}
else if ( layerType == QLatin1String( "line" ) )
{
// hasRendererStyle = parseLineLayer( jsonLayer, styleName );
}
else if ( layerType == QLatin1String( "symbol" ) )
{
// hasLabelingStyle = parseSymbolLayer( jsonLayer, styleName );
}
else
{
QgsDebugMsg( QStringLiteral( "Skipping unknown layer type: %1" ).arg( layerType ) );
continue;
}
if ( hasRendererStyle )
{
rendererStyle.setStyleName( styleId );
rendererStyle.setLayerName( layerName );
rendererStyle.setFilterExpression( filterExpression );
rendererStyle.setMinZoomLevel( minZoom );
rendererStyle.setMaxZoomLevel( maxZoom );
rendererStyle.setEnabled( enabled );
rendererStyles.append( rendererStyle );
}
if ( hasLabelingStyle )
{
labelingStyle.setStyleName( styleId );
labelingStyle.setLayerName( layerName );
labelingStyle.setFilterExpression( filterExpression );
labelingStyle.setMinZoomLevel( minZoom );
labelingStyle.setMaxZoomLevel( maxZoom );
labelingStyle.setEnabled( enabled );
labelingStyles.append( labelingStyle );
}
}
mRenderer = qgis::make_unique< QgsVectorTileBasicRenderer >();
QgsVectorTileBasicRenderer *renderer = dynamic_cast< QgsVectorTileBasicRenderer *>( mRenderer.get() );
renderer->setStyles( rendererStyles );
mLabeling = qgis::make_unique< QgsVectorTileBasicLabeling >();
QgsVectorTileBasicLabeling *labeling = dynamic_cast< QgsVectorTileBasicLabeling * >( mLabeling.get() );
labeling->setStyles( labelingStyles );
}
bool QgsMapBoxGlStyleConverter::parseFillLayer( const QVariantMap &jsonLayer, const QString &styleName, QgsVectorTileBasicRendererStyle &style )
{
if ( !jsonLayer.contains( QStringLiteral( "paint" ) ) )
{
QgsDebugMsg( QStringLiteral( "Style layer %1 has no paint property, skipping" ).arg( jsonLayer.value( QStringLiteral( "id" ) ).toString() ) );
return false;
}
const QVariantMap jsonPaint = jsonLayer.value( QStringLiteral( "paint" ) ).toMap();
QgsPropertyCollection ddProperties;
// fill color
QColor fillColor;
if ( jsonLayer.contains( QStringLiteral( "fill-color" ) ) )
{
const QVariant jsonFillColor = jsonPaint.value( QStringLiteral( "fill-color" ) );
switch ( jsonFillColor.type() )
{
case QVariant::Map:
ddProperties.setProperty( QgsSymbolLayer::PropertyFillColor, parseInterpolateColorByZoom( jsonFillColor.toMap() ) );
break;
case QVariant::List:
case QVariant::StringList:
break;
case QVariant::String:
break;
default:
break;
}
}
}
QgsProperty QgsMapBoxGlStyleConverter::parseInterpolateColorByZoom( const QVariantMap &json )
{
const double base = json.value( QStringLiteral( "base" ), QStringLiteral( "1" ) ).toDouble();
const QVariantList stops = json.value( QStringLiteral( "stops" ) ).toList();
if ( stops.empty() )
return QgsProperty();
QString caseString = QStringLiteral( "CASE " );
if ( base == 1 )
{
// base = 1 -> scale linear
for ( int i = 0; i < stops.length() - 1; ++i )
{
// step bottom zoom
const QString bz = stops.at( i ).toList().value( 0 ).toString();
// step top zoom
const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
const QColor bottomColor = parseColor( stops.at( i ).toList().value( 1 ) );
const QColor topColor = parseColor( stops.at( i + 1 ).toList().value( 1 ) );
int bcHue;
int bcSat;
int bcLight;
int bcAlpha;
colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
int tcHue;
int tcSat;
int tcLight;
int tcAlpha;
colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
caseString += QStringLiteral( "WHEN @zoom_level >= %1 AND @zoom_level < %2 THEN color_hsla("
"scale_linear(@zoom_level, %1, %2, %3, %4), "
"scale_linear(@zoom_level, %1, %2, %5, %6), "
"scale_linear(@zoom_level, %1, %2, %7, %8), "
"scale_linear(@zoom_level, %1, %2, %9, %10)) "
).arg( bz, tz )
.arg( bcHue )
.arg( tcHue )
.arg( bcSat )
.arg( tcSat )
.arg( bcLight )
.arg( tcLight )
.arg( bcAlpha )
.arg( tcAlpha );
}
}
else
{
// Base != 1 -> scale_exp()
for ( int i = 0; i < stops.length() - 1; ++i )
{
// step bottom zoom
const QString bz = stops.at( i ).toList().value( 0 ).toString();
// step top zoom
const QString tz = stops.at( i + 1 ).toList().value( 0 ).toString();
const QColor bottomColor = parseColor( stops.at( i ).toList().value( 1 ) );
const QColor topColor = parseColor( stops.at( i + 1 ).toList().value( 1 ) );
int bcHue;
int bcSat;
int bcLight;
int bcAlpha;
colorAsHslaComponents( bottomColor, bcHue, bcSat, bcLight, bcAlpha );
int tcHue;
int tcSat;
int tcLight;
int tcAlpha;
colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
caseString += QStringLiteral( "WHEN @zoom_level >= %1 AND @zoom_level < %2 THEN color_hsla("
"%3, %4, %5, %6) " ).arg( bz, tz,
interpolateExpression( bz.toInt(), tz.toInt(), bcHue, tcHue, base ),
interpolateExpression( bz.toInt(), tz.toInt(), bcSat, tcSat, base ),
interpolateExpression( bz.toInt(), tz.toInt(), bcLight, tcLight, base ),
interpolateExpression( bz.toInt(), tz.toInt(), bcAlpha, tcAlpha, base ) );
}
}
// top color
const QString tz = stops.last().toList().value( 0 ).toString();
const QColor topColor = parseColor( stops.last().toList().value( 1 ) );
int tcHue;
int tcSat;
int tcLight;
int tcAlpha;
colorAsHslaComponents( topColor, tcHue, tcSat, tcLight, tcAlpha );
caseString += QStringLiteral( "WHEN @zoom_level >= %1 THEN color_hsla(%2, %3, %4, %5) "
"ELSE color_hsla(%2, %3, %4, %5) END" ).arg( tz )
.arg( tcHue ).arg( tcSat ).arg( tcLight ).arg( tcAlpha );
return QgsProperty::fromExpression( caseString );
}
QColor QgsMapBoxGlStyleConverter::parseColor( const QVariant &color )
{
if ( color.type() != QVariant::String )
{
QgsDebugMsg( QStringLiteral( "Could not parse non-string color %1, skipping" ).arg( color.toString() ) );
return QColor();
}
return QgsSymbolLayerUtils::parseColor( color.toString() );
}
void QgsMapBoxGlStyleConverter::colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha )
{
hue = std::max( 0, color.hslHue() );
saturation = color.hslSaturation() / 255.0 * 100;
lightness = color.lightness() / 255.0 * 100;
alpha = color.alpha();
}
QString QgsMapBoxGlStyleConverter::interpolateExpression( int zoomMin, int zoomMax, double valueMin, double valueMax, double base )
{
return QStringLiteral( "%1 + %2 * (%3^(@zoom_level-%4)-1)/(%3^(%5-%4)-1)" ).arg( valueMin )
.arg( valueMax - valueMin )
.arg( base )
.arg( zoomMin )
.arg( zoomMax );
}
QgsVectorTileRenderer *QgsMapBoxGlStyleConverter::renderer() const

View File

@ -18,11 +18,13 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsproperty.h"
#include <QVariantMap>
#include <memory>
class QgsVectorTileRenderer;
class QgsVectorTileLabeling;
class QgsVectorTileBasicRendererStyle;
/**
* \ingroup core
@ -40,7 +42,7 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
*
* The specified MapBox GL \a style configuration will be converted.
*/
QgsMapBoxGlStyleConverter( const QVariantMap &style );
QgsMapBoxGlStyleConverter( const QVariantMap &style, const QString &styleName = QString() );
//! QgsMapBoxGlStyleConverter cannot be copied
QgsMapBoxGlStyleConverter( const QgsMapBoxGlStyleConverter &other ) = delete;
@ -69,7 +71,49 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
protected:
void parseLayers( const QVariantList &layers );
/**
* Parse list of \a layers from JSON
*/
void parseLayers( const QVariantList &layers, const QString &styleName );
/**
* Parses a fill layer.
*
* \param jsonLayer fill layer to parse
* \param styleName style name
* \param style generated QGIS vector tile style
* \returns TRUE if the layer was successfully parsed.
*/
static bool parseFillLayer( const QVariantMap &jsonLayer, const QString &styleName, QgsVectorTileBasicRendererStyle &style SIP_OUT );
static QgsProperty parseInterpolateColorByZoom( const QVariantMap &json );
/**
* Parses a \a color in one of these supported formats:
*
* - #fff or #ffffff
* - hsl(30, 19%, 90%) or hsla(30, 19%, 90%, 0.4)
* - rgb(10, 20, 30) or rgba(10, 20, 30, 0.5)
*
* Returns an invalid color if the color could not be parsed.
*/
static QColor parseColor( const QVariant &color );
/**
* Takes a QColor object and returns HSLA components in required format for QGIS color_hsla() expression function.
* \param color input color
* \param hue an integer value from 0 to 360
* \param saturation an integer value from 0 to 100
* \param lightness an integer value from 0 to 100
* \param alpha an integer value from 0 (completely transparent) to 255 (opaque).
*/
static void colorAsHslaComponents( const QColor &color, int &hue, int &saturation, int &lightness, int &alpha );
/**
* Generates an interpolation for values between \a valueMin and \a valueMax, scaled between the
* ranges \a zoomMin to \a zoomMax.
*/
static QString interpolateExpression( int zoomMin, int zoomMax, double valueMin, double valueMax, double base );
private:
@ -78,7 +122,6 @@ class CORE_EXPORT QgsMapBoxGlStyleConverter
#endif
QVariantMap mStyle;
QString mError;

View File

@ -44,12 +44,36 @@ TEST_DATA_DIR = unitTestDataPath()
class TestQgsMapBoxGlStyleConverter(unittest.TestCase):
maxDiff = 100000
def testNoLayer(self):
c = QgsMapBoxGlStyleConverter({'x': 'y'})
self.assertEqual(c.errorMessage(), 'Could not find layers list in JSON')
self.assertIsNone(c.renderer())
self.assertIsNone(c.labeling())
def testInterpolateExpression(self):
self.assertEqual(QgsMapBoxGlStyleConverter.interpolateExpression(5, 13, 27, 29, 1),
'27 + 2 * (1^(@zoom_level-5)-1)/(1^(13-5)-1)')
self.assertEqual(QgsMapBoxGlStyleConverter.interpolateExpression(5, 13, 27, 29, 1.5),
'27 + 2 * (1.5^(@zoom_level-5)-1)/(1.5^(13-5)-1)')
def testColorAsHslaComponents(self):
self.assertEqual(QgsMapBoxGlStyleConverter.colorAsHslaComponents(QColor.fromHsl(30, 50, 70)), (30, 19, 27, 255))
def testParseInterpolateColorByZoom(self):
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({}).isActive(), False)
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({'base': 1,
'stops': [[0, '#f1f075'],
[150, '#b52e3e'],
[250, '#e55e5e']]
}).expressionString(), 'CASE WHEN @zoom_level >= 0 AND @zoom_level < 150 THEN color_hsla(scale_linear(@zoom_level, 0, 150, 59, 352), scale_linear(@zoom_level, 0, 150, 81, 59), scale_linear(@zoom_level, 0, 150, 70, 44), scale_linear(@zoom_level, 0, 150, 255, 255)) WHEN @zoom_level >= 150 AND @zoom_level < 250 THEN color_hsla(scale_linear(@zoom_level, 150, 250, 352, 0), scale_linear(@zoom_level, 150, 250, 59, 72), scale_linear(@zoom_level, 150, 250, 44, 63), scale_linear(@zoom_level, 150, 250, 255, 255)) WHEN @zoom_level >= 250 THEN color_hsla(0, 72, 63, 255) ELSE color_hsla(0, 72, 63, 255) END')
self.assertEqual(QgsMapBoxGlStyleConverter.parseInterpolateColorByZoom({'base': 2,
'stops': [[0, '#f1f075'],
[150, '#b52e3e'],
[250, '#e55e5e']]
}).expressionString(), 'CASE WHEN @zoom_level >= 0 AND @zoom_level < 150 THEN color_hsla(59 + 293 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 81 + -22 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 70 + -26 * (2^(@zoom_level-0)-1)/(2^(150-0)-1), 255 + 0 * (2^(@zoom_level-0)-1)/(2^(150-0)-1)) WHEN @zoom_level >= 150 AND @zoom_level < 250 THEN color_hsla(352 + -352 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 59 + 13 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 44 + 19 * (2^(@zoom_level-150)-1)/(2^(250-150)-1), 255 + 0 * (2^(@zoom_level-150)-1)/(2^(250-150)-1)) WHEN @zoom_level >= 250 THEN color_hsla(0, 72, 63, 255) ELSE color_hsla(0, 72, 63, 255) END')
if __name__ == '__main__':
unittest.main()