From 52159851bf1d0a5dd2152244e2d9336f391dec90 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 17 Nov 2014 23:30:00 +1100 Subject: [PATCH] [FEATURE] Raster image symbol fill type Allows for filling polygons with a tiled raster image. Options include (data defined) file name, opacity, image size (in pixels, mm or map units), coordinate mode (feature or view) and rotation. --- .../symbology-ng/qgsfillsymbollayerv2.sip | 169 ++++++++ python/core/symbology-ng/qgssymbolv2.sip | 3 +- .../symbology-ng/qgsfillsymbollayerv2.cpp | 285 ++++++++++++++ src/core/symbology-ng/qgsfillsymbollayerv2.h | 185 +++++++++ .../symbology-ng/qgssymbollayerv2registry.cpp | 2 + .../symbology-ng/qgssymbollayerv2utils.cpp | 10 + src/core/symbology-ng/qgssymbolv2.h | 3 +- .../symbology-ng/qgslayerpropertieswidget.cpp | 1 + .../symbology-ng/qgssymbollayerv2widget.cpp | 330 ++++++++++++++++ src/gui/symbology-ng/qgssymbollayerv2widget.h | 37 ++ src/ui/symbollayer/widget_rasterfill.ui | 372 ++++++++++++++++++ tests/src/core/CMakeLists.txt | 1 + tests/src/core/testqgsrasterfill.cpp | 165 ++++++++ .../default/expected_rasterfill.png | Bin 0 -> 641536 bytes 14 files changed, 1561 insertions(+), 2 deletions(-) create mode 100755 src/ui/symbollayer/widget_rasterfill.ui create mode 100644 tests/src/core/testqgsrasterfill.cpp create mode 100644 tests/testdata/control_images/expected_rasterfill/default/expected_rasterfill.png diff --git a/python/core/symbology-ng/qgsfillsymbollayerv2.sip b/python/core/symbology-ng/qgsfillsymbollayerv2.sip index 6ec210d265b7..f1829149f8b3 100644 --- a/python/core/symbology-ng/qgsfillsymbollayerv2.sip +++ b/python/core/symbology-ng/qgsfillsymbollayerv2.sip @@ -461,6 +461,175 @@ class QgsImageFillSymbolLayer: QgsFillSymbolLayerV2 virtual Qt::PenStyle dxfPenStyle() const; }; +/** \ingroup core + * \class QgsRasterFillSymbolLayer + * \brief A class for filling symbols with a repeated raster image. + * \note Added in version 2.7 + */ +class QgsRasterFillSymbolLayer: QgsImageFillSymbolLayer +{ +%TypeHeaderCode +#include +%End + public: + + enum FillCoordinateMode + { + Feature, + Viewport + }; + + QgsRasterFillSymbolLayer( const QString& imageFilePath = QString() ); + ~QgsRasterFillSymbolLayer(); + + static QgsSymbolLayerV2* create( const QgsStringMap& properties = QgsStringMap() ); + + // implemented from base classes + QString layerType() const; + void renderPolygon( const QPolygonF& points, QList* rings, QgsSymbolV2RenderContext& context ); + void startRender( QgsSymbolV2RenderContext& context ); + void stopRender( QgsSymbolV2RenderContext& context ); + QgsStringMap properties() const; + QgsSymbolLayerV2* clone() const; + virtual double estimateMaxBleed() const; + + //override QgsImageFillSymbolLayer's support for sub symbols + virtual QgsSymbolV2* subSymbol(); + virtual bool setSubSymbol( QgsSymbolV2* symbol ); + + /**Sets the path to the raster image used for the fill. + * @param imagePath path to image file + * @see imageFilePath + */ + void setImageFilePath( const QString& imagePath ); + /**The path to the raster image used for the fill. + * @returns path to image file + * @see setImageFilePath + */ + QString imageFilePath() const; + + /**Set the coordinate mode for fill. Controls how the top left corner of the image + * fill is positioned relative to the feature. + * @param mode coordinate mode + * @see coordinateMode + */ + void setCoordinateMode( const FillCoordinateMode mode ); + /**Coordinate mode for fill. Controls how the top left corner of the image + * fill is positioned relative to the feature. + * @returns coordinate mode + * @see setCoordinateMode + */ + FillCoordinateMode coordinateMode() const; + + /**Sets the opacity for the raster image used in the fill. + * @param alpha opacity value between 0 (fully transparent) and 1 (fully opaque) + * @see alpha + */ + void setAlpha( const double alpha ); + /**The opacity for the raster image used in the fill. + * @returns opacity value between 0 (fully transparent) and 1 (fully opaque) + * @see setAlpha + */ + double alpha() const; + + /**Sets the offset for the fill. + * @param offset offset for fill + * @see offset + * @see setOffsetUnit + * @see setOffsetMapUnitScale + */ + void setOffset( const QPointF& offset ); + /**Returns the offset for the fill. + * @returns offset for fill + * @see setOffset + * @see offsetUnit + * @see offsetMapUnitScale + */ + QPointF offset() const; + + /**Sets the units for the fill's offset. + * @param unit units for offset + * @see offsetUnit + * @see setOffset + * @see setOffsetMapUnitScale + */ + void setOffsetUnit( const QgsSymbolV2::OutputUnit unit ); + /**Returns the units for the fill's offset. + * @returns units for offset + * @see setOffsetUnit + * @see offset + * @see offsetMapUnitScale + */ + QgsSymbolV2::OutputUnit offsetUnit() const; + + /**Sets the map unit scale for the fill's offset. + * @param scale map unit scale for offset + * @see offsetMapUnitScale + * @see setOffset + * @see setOffsetUnit + */ + void setOffsetMapUnitScale( const QgsMapUnitScale& scale ); + /**Returns the map unit scale for the fill's offset. + * @returns map unit scale for offset + * @see setOffsetMapUnitScale + * @see offset + * @see offsetUnit + */ + const QgsMapUnitScale& offsetMapUnitScale() const; + + /**Sets the width for scaling the image used in the fill. The image's height will also be + * scaled to maintain the image's aspect ratio. + * @param width width for scaling the image + * @see width + * @see setWidthUnit + * @see setWidthMapUnitScale + */ + void setWidth( const double width ); + /**Returns the width used for scaling the image used in the fill. The image's height is + * scaled to maintain the image's aspect ratio. + * @returns width used for scaling the image + * @see setWidth + * @see widthUnit + * @see widthMapUnitScale + */ + double width() const; + + /**Sets the units for the image's width. + * @param unit units for width + * @see widthUnit + * @see setWidth + * @see setWidthMapUnitScale + */ + void setWidthUnit( const QgsSymbolV2::OutputUnit unit ); + /**Returns the units for the image's width. + * @returns units for width + * @see setWidthUnit + * @see width + * @see widthMapUnitScale + */ + QgsSymbolV2::OutputUnit widthUnit() const; + + /**Sets the map unit scale for the image's width. + * @param scale map unit scale for width + * @see widthMapUnitScale + * @see setWidth + * @see setWidthUnit + */ + void setWidthMapUnitScale( const QgsMapUnitScale& scale ); + /**Returns the map unit scale for the image's width. + * @returns map unit scale for width + * @see setWidthMapUnitScale + * @see width + * @see widthUnit + */ + const QgsMapUnitScale& widthMapUnitScale() const; + + protected: + + void applyDataDefinedSettings( const QgsSymbolV2RenderContext& context ); + +}; + /**A class for svg fill patterns. The class automatically scales the pattern to the appropriate pixel dimensions of the output device*/ class QgsSVGFillSymbolLayer: QgsImageFillSymbolLayer diff --git a/python/core/symbology-ng/qgssymbolv2.sip b/python/core/symbology-ng/qgssymbolv2.sip index 06cfdfaf93da..e1a2de13e075 100644 --- a/python/core/symbology-ng/qgssymbolv2.sip +++ b/python/core/symbology-ng/qgssymbolv2.sip @@ -23,7 +23,8 @@ class QgsSymbolV2 { MM, MapUnit, - Mixed //mixed units in symbol layers + Mixed, //mixed units in symbol layers + Pixel }; enum SymbolType diff --git a/src/core/symbology-ng/qgsfillsymbollayerv2.cpp b/src/core/symbology-ng/qgsfillsymbollayerv2.cpp index 92fc99cbc6b5..c1ea94b88980 100644 --- a/src/core/symbology-ng/qgsfillsymbollayerv2.cpp +++ b/src/core/symbology-ng/qgsfillsymbollayerv2.cpp @@ -3445,3 +3445,288 @@ QgsMapUnitScale QgsCentroidFillSymbolLayerV2::mapUnitScale() const } + + +QgsRasterFillSymbolLayer::QgsRasterFillSymbolLayer( const QString &imageFilePath ) + : QgsImageFillSymbolLayer() + , mImageFilePath( imageFilePath ) + , mCoordinateMode( QgsRasterFillSymbolLayer::Feature ) + , mAlpha( 1.0 ) + , mOffsetUnit( QgsSymbolV2::MM ) + , mWidth( 0.0 ) + , mWidthUnit( QgsSymbolV2::Pixel ) +{ + QgsImageFillSymbolLayer::setSubSymbol( 0 ); //disable sub symbol +} + +QgsRasterFillSymbolLayer::~QgsRasterFillSymbolLayer() +{ + +} + +QgsSymbolLayerV2 *QgsRasterFillSymbolLayer::create( const QgsStringMap &properties ) +{ + FillCoordinateMode mode = QgsRasterFillSymbolLayer::Feature; + double alpha = 1.0; + QPointF offset; + double angle = 0.0; + double width = 0.0; + + QString imagePath; + if ( properties.contains( "imageFile" ) ) + { + imagePath = properties["imageFile"]; + } + if ( properties.contains( "coordinate_mode" ) ) + { + mode = ( FillCoordinateMode )properties["coordinate_mode"].toInt(); + } + if ( properties.contains( "alpha" ) ) + { + alpha = properties["alpha"].toDouble(); + } + if ( properties.contains( "offset" ) ) + { + offset = QgsSymbolLayerV2Utils::decodePoint( properties["offset"] ); + } + if ( properties.contains( "angle" ) ) + { + angle = properties["angle"].toDouble(); + } + if ( properties.contains( "width" ) ) + { + width = properties["width"].toDouble(); + } + QgsRasterFillSymbolLayer* symbolLayer = new QgsRasterFillSymbolLayer( imagePath ); + symbolLayer->setCoordinateMode( mode ); + symbolLayer->setAlpha( alpha ); + symbolLayer->setOffset( offset ); + symbolLayer->setAngle( angle ); + symbolLayer->setWidth( width ); + if ( properties.contains( "offset_unit" ) ) + { + symbolLayer->setOffsetUnit( QgsSymbolLayerV2Utils::decodeOutputUnit( properties["offset_unit"] ) ); + } + if ( properties.contains( "offset_map_unit_scale" ) ) + { + symbolLayer->setOffsetMapUnitScale( QgsSymbolLayerV2Utils::decodeMapUnitScale( properties["offset_map_unit_scale"] ) ); + } + if ( properties.contains( "width_unit" ) ) + { + symbolLayer->setWidthUnit( QgsSymbolLayerV2Utils::decodeOutputUnit( properties["width_unit"] ) ); + } + if ( properties.contains( "width_map_unit_scale" ) ) + { + symbolLayer->setWidthMapUnitScale( QgsSymbolLayerV2Utils::decodeMapUnitScale( properties["width_map_unit_scale"] ) ); + } + + //data defined + if ( properties.contains( "file_expression" ) ) + { + symbolLayer->setDataDefinedProperty( "file", properties["file_expression"] ); + } + if ( properties.contains( "alpha_expression" ) ) + { + symbolLayer->setDataDefinedProperty( "alpha", properties["alpha_expression"] ); + } + if ( properties.contains( "angle_expression" ) ) + { + symbolLayer->setDataDefinedProperty( "angle", properties["angle_expression"] ); + } + if ( properties.contains( "width_expression" ) ) + { + symbolLayer->setDataDefinedProperty( "width", properties["width_expression"] ); + } + return symbolLayer; +} + +bool QgsRasterFillSymbolLayer::setSubSymbol( QgsSymbolV2 *symbol ) +{ + Q_UNUSED( symbol ); + return true; +} + +QString QgsRasterFillSymbolLayer::layerType() const +{ + return "RasterFill"; +} + +void QgsRasterFillSymbolLayer::renderPolygon( const QPolygonF &points, QList *rings, QgsSymbolV2RenderContext &context ) +{ + QPainter* p = context.renderContext().painter(); + if ( !p ) + { + return; + } + + QPointF offset; + if ( !mOffset.isNull() ) + { + offset.setX( mOffset.x() * QgsSymbolLayerV2Utils::lineWidthScaleFactor( context.renderContext(), mOffsetUnit, mOffsetMapUnitScale ) ); + offset.setY( mOffset.y() * QgsSymbolLayerV2Utils::lineWidthScaleFactor( context.renderContext(), mOffsetUnit, mOffsetMapUnitScale ) ); + p->translate( offset ); + } + if ( mCoordinateMode == Feature ) + { + QRectF boundingRect = points.boundingRect(); + mBrush.setTransform( mBrush.transform().translate( boundingRect.left() - mBrush.transform().dx(), + boundingRect.top() - mBrush.transform().dy() ) ); + } + + QgsImageFillSymbolLayer::renderPolygon( points, rings, context ); + if ( !mOffset.isNull() ) + { + p->translate( -offset ); + } +} + +void QgsRasterFillSymbolLayer::startRender( QgsSymbolV2RenderContext &context ) +{ + prepareExpressions( context.fields(), context.renderContext().rendererScale() ); + applyPattern( mBrush, mImageFilePath, mWidth, mAlpha, context ); +} + +void QgsRasterFillSymbolLayer::stopRender( QgsSymbolV2RenderContext &context ) +{ + Q_UNUSED( context ); +} + +QgsStringMap QgsRasterFillSymbolLayer::properties() const +{ + QgsStringMap map; + map["imageFile"] = mImageFilePath; + map["coordinate_mode"] = QString::number( mCoordinateMode ); + map["alpha"] = QString::number( mAlpha ); + map["offset"] = QgsSymbolLayerV2Utils::encodePoint( mOffset ); + map["offset_unit"] = QgsSymbolLayerV2Utils::encodeOutputUnit( mOffsetUnit ); + map["offset_map_unit_scale"] = QgsSymbolLayerV2Utils::encodeMapUnitScale( mOffsetMapUnitScale ); + map["angle"] = QString::number( mAngle ); + map["width"] = QString::number( mWidth ); + map["width_unit"] = QgsSymbolLayerV2Utils::encodeOutputUnit( mWidthUnit ); + map["width_map_unit_scale"] = QgsSymbolLayerV2Utils::encodeMapUnitScale( mWidthMapUnitScale ); + + saveDataDefinedProperties( map ); + return map; +} + +QgsSymbolLayerV2 *QgsRasterFillSymbolLayer::clone() const +{ + QgsRasterFillSymbolLayer* sl = new QgsRasterFillSymbolLayer( mImageFilePath ); + sl->setCoordinateMode( mCoordinateMode ); + sl->setAlpha( mAlpha ); + sl->setOffset( mOffset ); + sl->setOffsetUnit( mOffsetUnit ); + sl->setOffsetMapUnitScale( mOffsetMapUnitScale ); + sl->setAngle( mAngle ); + sl->setWidth( mWidth ); + sl->setWidthUnit( mWidthUnit ); + sl->setWidthMapUnitScale( mWidthMapUnitScale ); + copyDataDefinedProperties( sl ); + return sl; +} + +double QgsRasterFillSymbolLayer::estimateMaxBleed() const +{ + return mOffset.x() > mOffset.y() ? mOffset.x() : mOffset.y(); +} + +void QgsRasterFillSymbolLayer::setImageFilePath( const QString &imagePath ) +{ + mImageFilePath = imagePath; +} + +void QgsRasterFillSymbolLayer::setCoordinateMode( const QgsRasterFillSymbolLayer::FillCoordinateMode mode ) +{ + mCoordinateMode = mode; +} + +void QgsRasterFillSymbolLayer::setAlpha( const double alpha ) +{ + mAlpha = alpha; +} + +void QgsRasterFillSymbolLayer::applyDataDefinedSettings( const QgsSymbolV2RenderContext &context ) +{ + if ( mDataDefinedProperties.isEmpty() ) + return; // shortcut + + QgsExpression* widthExpression = expression( "width" ); + QgsExpression* fileExpression = expression( "file" ); + QgsExpression* alphaExpression = expression( "alpha" ); + QgsExpression* angleExpression = expression( "angle" ); + + if ( !widthExpression && !angleExpression && !alphaExpression && !fileExpression ) + { + return; //no data defined settings + } + + if ( angleExpression ) + { + mNextAngle = angleExpression->evaluate( const_cast( context.feature() ) ).toDouble(); + } + + if ( !widthExpression && !alphaExpression && !fileExpression ) + { + return; //nothing further to do + } + + double width = mWidth; + if ( widthExpression ) + { + width = widthExpression->evaluate( const_cast( context.feature() ) ).toDouble(); + } + double alpha = mAlpha; + if ( alphaExpression ) + { + alpha = alphaExpression->evaluate( const_cast( context.feature() ) ).toDouble(); + } + QString file = mImageFilePath; + if ( fileExpression ) + { + file = fileExpression->evaluate( const_cast( context.feature() ) ).toString(); + } + applyPattern( mBrush, file, width, alpha, context ); +} + +void QgsRasterFillSymbolLayer::applyPattern( QBrush &brush, const QString &imageFilePath, const double width, const double alpha, const QgsSymbolV2RenderContext &context ) +{ + QImage image( imageFilePath ); + if ( image.isNull() ) + { + return; + } + if ( !image.hasAlphaChannel() ) + { + image = image.convertToFormat( QImage::Format_ARGB32 ); + } + + double pixelWidth; + if ( width > 0 ) + { + pixelWidth = width * QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context.renderContext(), mWidthUnit, mWidthMapUnitScale ); + } + else + { + pixelWidth = image.width(); + } + + //reduce alpha of image + if ( alpha < 1.0 ) + { + QPainter p; + p.begin( &image ); + p.setCompositionMode( QPainter::CompositionMode_DestinationIn ); + QColor alphaColor( 0, 0, 0 ); + alphaColor.setAlphaF( alpha ); + p.fillRect( image.rect(), alphaColor ); + p.end(); + } + + //resize image if required + if ( !qgsDoubleNear( pixelWidth, image.width() ) ) + { + image = image.scaledToWidth( pixelWidth, Qt::SmoothTransformation ); + } + + brush.setTextureImage( image ); +} diff --git a/src/core/symbology-ng/qgsfillsymbollayerv2.h b/src/core/symbology-ng/qgsfillsymbollayerv2.h index 0f381c648f6e..9add31c1444b 100644 --- a/src/core/symbology-ng/qgsfillsymbollayerv2.h +++ b/src/core/symbology-ng/qgsfillsymbollayerv2.h @@ -550,6 +550,7 @@ class CORE_EXPORT QgsShapeburstFillSymbolLayerV2 : public QgsFillSymbolLayerV2 class CORE_EXPORT QgsImageFillSymbolLayer: public QgsFillSymbolLayerV2 { public: + QgsImageFillSymbolLayer(); virtual ~QgsImageFillSymbolLayer(); void renderPolygon( const QPolygonF& points, QList* rings, QgsSymbolV2RenderContext& context ); @@ -590,6 +591,190 @@ class CORE_EXPORT QgsImageFillSymbolLayer: public QgsFillSymbolLayerV2 virtual void applyDataDefinedSettings( const QgsSymbolV2RenderContext& context ) { Q_UNUSED( context ); } }; +/** \ingroup core + * \class QgsRasterFillSymbolLayer + * \brief A class for filling symbols with a repeated raster image. + * \note Added in version 2.7 + */ +class CORE_EXPORT QgsRasterFillSymbolLayer: public QgsImageFillSymbolLayer +{ + public: + + enum FillCoordinateMode + { + Feature, + Viewport + }; + + QgsRasterFillSymbolLayer( const QString& imageFilePath = QString() ); + ~QgsRasterFillSymbolLayer(); + + static QgsSymbolLayerV2* create( const QgsStringMap& properties = QgsStringMap() ); + + // implemented from base classes + QString layerType() const; + void renderPolygon( const QPolygonF& points, QList* rings, QgsSymbolV2RenderContext& context ); + void startRender( QgsSymbolV2RenderContext& context ); + void stopRender( QgsSymbolV2RenderContext& context ); + QgsStringMap properties() const; + QgsSymbolLayerV2* clone() const; + virtual double estimateMaxBleed() const; + + //override QgsImageFillSymbolLayer's support for sub symbols + virtual QgsSymbolV2* subSymbol() { return 0; } + virtual bool setSubSymbol( QgsSymbolV2* symbol ); + + /**Sets the path to the raster image used for the fill. + * @param imagePath path to image file + * @see imageFilePath + */ + void setImageFilePath( const QString& imagePath ); + /**The path to the raster image used for the fill. + * @returns path to image file + * @see setImageFilePath + */ + QString imageFilePath() const { return mImageFilePath; } + + /**Set the coordinate mode for fill. Controls how the top left corner of the image + * fill is positioned relative to the feature. + * @param mode coordinate mode + * @see coordinateMode + */ + void setCoordinateMode( const FillCoordinateMode mode ); + /**Coordinate mode for fill. Controls how the top left corner of the image + * fill is positioned relative to the feature. + * @returns coordinate mode + * @see setCoordinateMode + */ + FillCoordinateMode coordinateMode() const { return mCoordinateMode; } + + /**Sets the opacity for the raster image used in the fill. + * @param alpha opacity value between 0 (fully transparent) and 1 (fully opaque) + * @see alpha + */ + void setAlpha( const double alpha ); + /**The opacity for the raster image used in the fill. + * @returns opacity value between 0 (fully transparent) and 1 (fully opaque) + * @see setAlpha + */ + double alpha() const { return mAlpha; } + + /**Sets the offset for the fill. + * @param offset offset for fill + * @see offset + * @see setOffsetUnit + * @see setOffsetMapUnitScale + */ + void setOffset( const QPointF& offset ) { mOffset = offset; } + /**Returns the offset for the fill. + * @returns offset for fill + * @see setOffset + * @see offsetUnit + * @see offsetMapUnitScale + */ + QPointF offset() const { return mOffset; } + + /**Sets the units for the fill's offset. + * @param unit units for offset + * @see offsetUnit + * @see setOffset + * @see setOffsetMapUnitScale + */ + void setOffsetUnit( const QgsSymbolV2::OutputUnit unit ) { mOffsetUnit = unit; } + /**Returns the units for the fill's offset. + * @returns units for offset + * @see setOffsetUnit + * @see offset + * @see offsetMapUnitScale + */ + QgsSymbolV2::OutputUnit offsetUnit() const { return mOffsetUnit; } + + /**Sets the map unit scale for the fill's offset. + * @param scale map unit scale for offset + * @see offsetMapUnitScale + * @see setOffset + * @see setOffsetUnit + */ + void setOffsetMapUnitScale( const QgsMapUnitScale& scale ) { mOffsetMapUnitScale = scale; } + /**Returns the map unit scale for the fill's offset. + * @returns map unit scale for offset + * @see setOffsetMapUnitScale + * @see offset + * @see offsetUnit + */ + const QgsMapUnitScale& offsetMapUnitScale() const { return mOffsetMapUnitScale; } + + /**Sets the width for scaling the image used in the fill. The image's height will also be + * scaled to maintain the image's aspect ratio. + * @param width width for scaling the image + * @see width + * @see setWidthUnit + * @see setWidthMapUnitScale + */ + void setWidth( const double width ) { mWidth = width; } + /**Returns the width used for scaling the image used in the fill. The image's height is + * scaled to maintain the image's aspect ratio. + * @returns width used for scaling the image + * @see setWidth + * @see widthUnit + * @see widthMapUnitScale + */ + double width() const { return mWidth; } + + /**Sets the units for the image's width. + * @param unit units for width + * @see widthUnit + * @see setWidth + * @see setWidthMapUnitScale + */ + void setWidthUnit( const QgsSymbolV2::OutputUnit unit ) { mWidthUnit = unit; } + /**Returns the units for the image's width. + * @returns units for width + * @see setWidthUnit + * @see width + * @see widthMapUnitScale + */ + QgsSymbolV2::OutputUnit widthUnit() const { return mWidthUnit; } + + /**Sets the map unit scale for the image's width. + * @param scale map unit scale for width + * @see widthMapUnitScale + * @see setWidth + * @see setWidthUnit + */ + void setWidthMapUnitScale( const QgsMapUnitScale& scale ) { mWidthMapUnitScale = scale; } + /**Returns the map unit scale for the image's width. + * @returns map unit scale for width + * @see setWidthMapUnitScale + * @see width + * @see widthUnit + */ + const QgsMapUnitScale& widthMapUnitScale() const { return mWidthMapUnitScale; } + + protected: + + /**Path to the image file*/ + QString mImageFilePath; + FillCoordinateMode mCoordinateMode; + double mAlpha; + + QPointF mOffset; + QgsSymbolV2::OutputUnit mOffsetUnit; + QgsMapUnitScale mOffsetMapUnitScale; + + double mWidth; + QgsSymbolV2::OutputUnit mWidthUnit; + QgsMapUnitScale mWidthMapUnitScale; + + void applyDataDefinedSettings( const QgsSymbolV2RenderContext& context ); + + private: + + /**Applies the image pattern to the brush*/ + void applyPattern( QBrush& brush, const QString& imageFilePath, const double width, const double alpha, + const QgsSymbolV2RenderContext& context ); +}; + /**A class for svg fill patterns. The class automatically scales the pattern to the appropriate pixel dimensions of the output device*/ class CORE_EXPORT QgsSVGFillSymbolLayer: public QgsImageFillSymbolLayer diff --git a/src/core/symbology-ng/qgssymbollayerv2registry.cpp b/src/core/symbology-ng/qgssymbollayerv2registry.cpp index 3594c37abd74..7f0e56592360 100644 --- a/src/core/symbology-ng/qgssymbollayerv2registry.cpp +++ b/src/core/symbology-ng/qgssymbollayerv2registry.cpp @@ -46,6 +46,8 @@ QgsSymbolLayerV2Registry::QgsSymbolLayerV2Registry() QgsGradientFillSymbolLayerV2::create ) ); addSymbolLayerType( new QgsSymbolLayerV2Metadata( "ShapeburstFill", QObject::tr( "Shapeburst fill" ), QgsSymbolV2::Fill, QgsShapeburstFillSymbolLayerV2::create ) ); + addSymbolLayerType( new QgsSymbolLayerV2Metadata( "RasterFill", QObject::tr( "Raster image fill" ), QgsSymbolV2::Fill, + QgsRasterFillSymbolLayer::create ) ); addSymbolLayerType( new QgsSymbolLayerV2Metadata( "SVGFill", QObject::tr( "SVG fill" ), QgsSymbolV2::Fill, QgsSVGFillSymbolLayer::create, QgsSVGFillSymbolLayer::createFromSld ) ); addSymbolLayerType( new QgsSymbolLayerV2Metadata( "CentroidFill", QObject::tr( "Centroid fill" ), QgsSymbolV2::Fill, diff --git a/src/core/symbology-ng/qgssymbollayerv2utils.cpp b/src/core/symbology-ng/qgssymbollayerv2utils.cpp index 1f24263fdf9c..ddda51b6016f 100644 --- a/src/core/symbology-ng/qgssymbollayerv2utils.cpp +++ b/src/core/symbology-ng/qgssymbollayerv2utils.cpp @@ -347,6 +347,8 @@ QString QgsSymbolLayerV2Utils::encodeOutputUnit( QgsSymbolV2::OutputUnit unit ) return "MM"; case QgsSymbolV2::MapUnit: return "MapUnit"; + case QgsSymbolV2::Pixel: + return "Pixel"; default: return "MM"; } @@ -362,6 +364,10 @@ QgsSymbolV2::OutputUnit QgsSymbolLayerV2Utils::decodeOutputUnit( QString str ) { return QgsSymbolV2::MapUnit; } + else if ( str == "Pixel" ) + { + return QgsSymbolV2::Pixel; + } // millimeters are default return QgsSymbolV2::MM; @@ -3261,6 +3267,10 @@ double QgsSymbolLayerV2Utils::pixelSizeScaleFactor( const QgsRenderContext& c, Q { return ( c.scaleFactor() * c.rasterScaleFactor() ); } + else if ( u == QgsSymbolV2::Pixel ) + { + return 1.0; + } else //QgsSymbol::MapUnit { double mup = scale.computeMapUnitsPerPixel( c ); diff --git a/src/core/symbology-ng/qgssymbolv2.h b/src/core/symbology-ng/qgssymbolv2.h index c6bafe2b0e1f..dedbc6f412ec 100644 --- a/src/core/symbology-ng/qgssymbolv2.h +++ b/src/core/symbology-ng/qgssymbolv2.h @@ -48,7 +48,8 @@ class CORE_EXPORT QgsSymbolV2 { MM = 0, MapUnit, - Mixed //mixed units in symbol layers + Mixed, //mixed units in symbol layers + Pixel }; enum SymbolType diff --git a/src/gui/symbology-ng/qgslayerpropertieswidget.cpp b/src/gui/symbology-ng/qgslayerpropertieswidget.cpp index 423ca185145c..eead8b4b7ddb 100644 --- a/src/gui/symbology-ng/qgslayerpropertieswidget.cpp +++ b/src/gui/symbology-ng/qgslayerpropertieswidget.cpp @@ -69,6 +69,7 @@ static void _initWidgetFunctions() _initWidgetFunction( "SimpleFill", QgsSimpleFillSymbolLayerV2Widget::create ); _initWidgetFunction( "GradientFill", QgsGradientFillSymbolLayerV2Widget::create ); _initWidgetFunction( "ShapeburstFill", QgsShapeburstFillSymbolLayerV2Widget::create ); + _initWidgetFunction( "RasterFill", QgsRasterFillSymbolLayerWidget::create ); _initWidgetFunction( "SVGFill", QgsSVGFillSymbolLayerWidget::create ); _initWidgetFunction( "CentroidFill", QgsCentroidFillSymbolLayerV2Widget::create ); _initWidgetFunction( "LinePatternFill", QgsLinePatternFillSymbolLayerWidget::create ); diff --git a/src/gui/symbology-ng/qgssymbollayerv2widget.cpp b/src/gui/symbology-ng/qgssymbollayerv2widget.cpp index 73e0b1e4fa2c..12e4b29eafc1 100644 --- a/src/gui/symbology-ng/qgssymbollayerv2widget.cpp +++ b/src/gui/symbology-ng/qgssymbollayerv2widget.cpp @@ -44,6 +44,7 @@ #include #include #include +#include QString QgsSymbolLayerV2Widget::dataDefinedPropertyLabel( const QString &entryName ) { @@ -2912,3 +2913,332 @@ void QgsCentroidFillSymbolLayerV2Widget::on_mDrawInsideCheckBox_stateChanged( in mLayer->setPointOnSurface( state == Qt::Checked ); emit changed(); } + +/////////////// + +QgsRasterFillSymbolLayerWidget::QgsRasterFillSymbolLayerWidget( const QgsVectorLayer *vl, QWidget *parent ) + : QgsSymbolLayerV2Widget( parent, vl ) +{ + mLayer = 0; + setupUi( this ); + + mWidthUnitWidget->setUnits( QStringList() << tr( "Pixels" ) << tr( "Millimeter" ) << tr( "Map unit" ), 1 ); + mOffsetUnitWidget->setUnits( QStringList() << tr( "Millimeter" ) << tr( "Map unit" ), 1 ); + + connect( cboCoordinateMode, SIGNAL( currentIndexChanged( int ) ), this, SLOT( setCoordinateMode( int ) ) ); + connect( mSpinOffsetX, SIGNAL( valueChanged( double ) ), this, SLOT( offsetChanged() ) ); + connect( mSpinOffsetY, SIGNAL( valueChanged( double ) ), this, SLOT( offsetChanged() ) ); +} + +void QgsRasterFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayerV2 *layer ) +{ + if ( !layer ) + { + return; + } + + if ( layer->layerType() != "RasterFill" ) + { + return; + } + + mLayer = dynamic_cast( layer ); + if ( !mLayer ) + { + return; + } + + mImageLineEdit->blockSignals( true ); + mImageLineEdit->setText( mLayer->imageFilePath() ); + mImageLineEdit->blockSignals( false ); + + cboCoordinateMode->blockSignals( true ); + switch ( mLayer->coordinateMode() ) + { + case QgsRasterFillSymbolLayer::Viewport: + cboCoordinateMode->setCurrentIndex( 1 ); + break; + case QgsRasterFillSymbolLayer::Feature: + default: + cboCoordinateMode->setCurrentIndex( 0 ); + break; + } + cboCoordinateMode->blockSignals( false ); + mSpinTransparency->blockSignals( true ); + mSpinTransparency->setValue( mLayer->alpha() * 100.0 ); + mSpinTransparency->blockSignals( false ); + mSliderTransparency->blockSignals( true ); + mSliderTransparency->setValue( mLayer->alpha() * 100.0 ); + mSliderTransparency->blockSignals( false ); + mRotationSpinBox->blockSignals( true ); + mRotationSpinBox->setValue( mLayer->angle() ); + mRotationSpinBox->blockSignals( false ); + + mSpinOffsetX->blockSignals( true ); + mSpinOffsetX->setValue( mLayer->offset().x() ); + mSpinOffsetX->blockSignals( false ); + mSpinOffsetY->blockSignals( true ); + mSpinOffsetY->setValue( mLayer->offset().y() ); + mSpinOffsetY->blockSignals( false ); + mOffsetUnitWidget->blockSignals( true ); + mOffsetUnitWidget->setUnit( mLayer->offsetUnit() ); + mOffsetUnitWidget->setMapUnitScale( mLayer->offsetMapUnitScale() ); + mOffsetUnitWidget->blockSignals( false ); + + mWidthSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( mLayer->width() ); + mWidthSpinBox->blockSignals( false ); + mWidthUnitWidget->blockSignals( true ); + switch ( mLayer->widthUnit() ) + { + case QgsSymbolV2::MM: + mWidthUnitWidget->setUnit( 1 ); + break; + case QgsSymbolV2::MapUnit: + mWidthUnitWidget->setUnit( 2 ); + break; + case QgsSymbolV2::Pixel: + default: + mWidthUnitWidget->setUnit( 0 ); + break; + } + mWidthUnitWidget->setMapUnitScale( mLayer->widthMapUnitScale() ); + mWidthUnitWidget->blockSignals( false ); + updatePreviewImage(); +} + +QgsSymbolLayerV2 *QgsRasterFillSymbolLayerWidget::symbolLayer() +{ + return mLayer; +} + +void QgsRasterFillSymbolLayerWidget::on_mBrowseToolButton_clicked() +{ + QSettings s; + QString openDir; + QString lineEditText = mImageLineEdit->text(); + if ( !lineEditText.isEmpty() ) + { + QFileInfo openDirFileInfo( lineEditText ); + openDir = openDirFileInfo.path(); + } + + if ( openDir.isEmpty() ) + { + openDir = s.value( "/UI/lastRasterFillImageDir", "" ).toString(); + } + + //show file dialog + QString filePath = QFileDialog::getOpenFileName( 0, tr( "Select image file" ), openDir ); + if ( !filePath.isNull() ) + { + //check if file exists + QFileInfo fileInfo( filePath ); + if ( !fileInfo.exists() || !fileInfo.isReadable() ) + { + QMessageBox::critical( 0, "Invalid file", "Error, file does not exist or is not readable" ); + return; + } + + s.setValue( "/UI/lastRasterFillImageDir", fileInfo.absolutePath() ); + mImageLineEdit->setText( filePath ); + on_mImageLineEdit_editingFinished(); + } +} + +void QgsRasterFillSymbolLayerWidget::on_mImageLineEdit_editingFinished() +{ + if ( !mLayer ) + { + return; + } + + QFileInfo fi( mImageLineEdit->text() ); + if ( !fi.exists() ) + { + QUrl url( mImageLineEdit->text() ); + if ( !url.isValid() ) + { + return; + } + } + + QApplication::setOverrideCursor( QCursor( Qt::WaitCursor ) ); + mLayer->setImageFilePath( mImageLineEdit->text() ); + updatePreviewImage(); + QApplication::restoreOverrideCursor(); + + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::setCoordinateMode( int index ) +{ + switch ( index ) + { + case 0: + //feature coordinate mode + mLayer->setCoordinateMode( QgsRasterFillSymbolLayer::Feature ); + break; + case 1: + //viewport coordinate mode + mLayer->setCoordinateMode( QgsRasterFillSymbolLayer::Viewport ); + break; + } + + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::on_mSpinTransparency_valueChanged( int value ) +{ + if ( !mLayer ) + { + return; + } + + mLayer->setAlpha( value / 100.0 ); + emit changed(); + updatePreviewImage(); +} + +void QgsRasterFillSymbolLayerWidget::offsetChanged() +{ + mLayer->setOffset( QPointF( mSpinOffsetX->value(), mSpinOffsetY->value() ) ); + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::on_mOffsetUnitWidget_changed() +{ + if ( !mLayer ) + { + return; + } + QgsSymbolV2::OutputUnit unit = static_cast( mOffsetUnitWidget->getUnit() ); + mLayer->setOffsetUnit( unit ); + mLayer->setOffsetMapUnitScale( mOffsetUnitWidget->getMapUnitScale() ); + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::on_mRotationSpinBox_valueChanged( double d ) +{ + if ( mLayer ) + { + mLayer->setAngle( d ); + emit changed(); + } +} + +void QgsRasterFillSymbolLayerWidget::on_mWidthUnitWidget_changed() +{ + if ( !mLayer ) + { + return; + } + QgsSymbolV2::OutputUnit unit; + switch ( mWidthUnitWidget->getUnit() ) + { + case 0: + unit = QgsSymbolV2::Pixel; + break; + case 1: + unit = QgsSymbolV2::MM; + break; + case 2: + unit = QgsSymbolV2::MapUnit; + break; + } + + mLayer->setWidthUnit( unit ); + mLayer->setWidthMapUnitScale( mOffsetUnitWidget->getMapUnitScale() ); + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::on_mWidthSpinBox_valueChanged( double d ) +{ + if ( !mLayer ) + { + return; + } + mLayer->setWidth( d ); + emit changed(); +} + +void QgsRasterFillSymbolLayerWidget::on_mDataDefinedPropertiesButton_clicked() +{ + if ( !mLayer ) + { + return; + } + + QList< QgsDataDefinedSymbolDialog::DataDefinedSymbolEntry > dataDefinedProperties; + dataDefinedProperties << QgsDataDefinedSymbolDialog::DataDefinedSymbolEntry( "file", tr( "File" ), mLayer->dataDefinedPropertyString( "file" ), + QgsDataDefinedSymbolDialog::fileNameHelpText() ); + dataDefinedProperties << QgsDataDefinedSymbolDialog::DataDefinedSymbolEntry( "alpha", tr( "Opacity" ), mLayer->dataDefinedPropertyString( "alpha" ), + QgsDataDefinedSymbolDialog::doubleHelpText() ); + dataDefinedProperties << QgsDataDefinedSymbolDialog::DataDefinedSymbolEntry( "angle", tr( "Angle" ), mLayer->dataDefinedPropertyString( "angle" ), + QgsDataDefinedSymbolDialog::doubleHelpText() ); + dataDefinedProperties << QgsDataDefinedSymbolDialog::DataDefinedSymbolEntry( "width", tr( "Width" ), mLayer->dataDefinedPropertyString( "width" ), + QgsDataDefinedSymbolDialog::doubleHelpText() ); + QgsDataDefinedSymbolDialog d( dataDefinedProperties, mVectorLayer ); + if ( d.exec() == QDialog::Accepted ) + { + //empty all existing properties first + mLayer->removeDataDefinedProperties(); + + QMap properties = d.dataDefinedProperties(); + QMap::const_iterator it = properties.constBegin(); + for ( ; it != properties.constEnd(); ++it ) + { + if ( !it.value().isEmpty() ) + { + mLayer->setDataDefinedProperty( it.key(), it.value() ); + } + } + emit changed(); + } +} + +void QgsRasterFillSymbolLayerWidget::updatePreviewImage() +{ + if ( !mLayer ) + { + return; + } + + QImage image( mLayer->imageFilePath() ); + if ( image.isNull() ) + { + mLabelImagePreview->setPixmap( 0 ); + return; + } + + if ( image.height() > 150 || image.width() > 150 ) + { + image = image.scaled( 150, 150, Qt::KeepAspectRatio, Qt::SmoothTransformation ); + } + + QImage previewImage( 150, 150, QImage::Format_ARGB32 ); + previewImage.fill( Qt::transparent ); + QRect imageRect(( 150 - image.width() ) / 2.0, ( 150 - image.height() ) / 2.0, image.width(), image.height() ); + QPainter p; + p.begin( &previewImage ); + //draw a checkerboard background + uchar pixDataRGB[] = { 150, 150, 150, 150, + 100, 100, 100, 150, + 100, 100, 100, 150, + 150, 150, 150, 150 + }; + QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 ); + QPixmap pix = QPixmap::fromImage( img.scaled( 8, 8 ) ); + QBrush checkerBrush; + checkerBrush.setTexture( pix ); + p.fillRect( imageRect, checkerBrush ); + + if ( mLayer->alpha() < 1.0 ) + { + p.setOpacity( mLayer->alpha() ); + } + + p.drawImage( imageRect.left(), imageRect.top(), image ); + p.end(); + mLabelImagePreview->setPixmap( QPixmap::fromImage( previewImage ) ); +} diff --git a/src/gui/symbology-ng/qgssymbollayerv2widget.h b/src/gui/symbology-ng/qgssymbollayerv2widget.h index d6ce240df40e..01633dfe36a1 100644 --- a/src/gui/symbology-ng/qgssymbollayerv2widget.h +++ b/src/gui/symbology-ng/qgssymbollayerv2widget.h @@ -320,6 +320,43 @@ class GUI_EXPORT QgsSvgMarkerSymbolLayerV2Widget : public QgsSymbolLayerV2Widget QgsSvgMarkerSymbolLayerV2* mLayer; }; +/////////// + +#include "ui_widget_rasterfill.h" + +class QgsRasterFillSymbolLayer; + +class GUI_EXPORT QgsRasterFillSymbolLayerWidget : public QgsSymbolLayerV2Widget, private Ui::WidgetRasterFill +{ + Q_OBJECT + + public: + QgsRasterFillSymbolLayerWidget( const QgsVectorLayer* vl, QWidget* parent = NULL ); + + static QgsSymbolLayerV2Widget* create( const QgsVectorLayer* vl ) { return new QgsRasterFillSymbolLayerWidget( vl ); } + + // from base class + virtual void setSymbolLayer( QgsSymbolLayerV2* layer ); + virtual QgsSymbolLayerV2* symbolLayer(); + + protected: + QgsRasterFillSymbolLayer* mLayer; + + private slots: + void on_mBrowseToolButton_clicked(); + void on_mImageLineEdit_editingFinished(); + void setCoordinateMode( int index ); + void on_mSpinTransparency_valueChanged( int value ); + void offsetChanged(); + void on_mOffsetUnitWidget_changed(); + void on_mRotationSpinBox_valueChanged( double d ); + void on_mWidthUnitWidget_changed(); + void on_mWidthSpinBox_valueChanged( double d ); + void on_mDataDefinedPropertiesButton_clicked(); + + private: + void updatePreviewImage(); +}; /////////// diff --git a/src/ui/symbollayer/widget_rasterfill.ui b/src/ui/symbollayer/widget_rasterfill.ui new file mode 100755 index 000000000000..b194da7fb7ed --- /dev/null +++ b/src/ui/symbollayer/widget_rasterfill.ui @@ -0,0 +1,372 @@ + + + WidgetRasterFill + + + + 0 + 0 + 424 + 294 + + + + Form + + + + + + + + + + + ... + + + + + + + + + + + Image width + + + mWidthSpinBox + + + + + + + + 0 + 0 + + + + 360.000000000000000 + + + 0.500000000000000 + + + + + + + Coord mode + + + cboCoordinateMode + + + + + + + + Object + + + + + Viewport + + + + + + + + Rotation + + + mRotationSpinBox + + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + QFrame::Panel + + + QFrame::Sunken + + + 1 + + + 0 + + + + + + + + + + + + + 1 + 0 + + + + Original + + + 6 + + + 99999999.000000000000000 + + + 0.200000000000000 + + + + + + + + + + + + + + + 0 + 0 + + + + 100 + + + 100 + + + Qt::Horizontal + + + + + + + % + + + 0 + + + 100 + + + 100 + + + + + + + + + Opacity + + + mSliderTransparency + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + Offset X,Y + + + mSpinOffsetX + + + + + + + + + + 1 + 0 + + + + 6 + + + -999.000000000000000 + + + 999.000000000000000 + + + 0.200000000000000 + + + + + + + + 1 + 0 + + + + 6 + + + -999.000000000000000 + + + 999.000000000000000 + + + 0.200000000000000 + + + + + + + + + + + + + + + 0 + 0 + + + + Data defined properties... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
+
+ + QgsUnitSelectionWidget + QWidget +
qgsunitselectionwidget.h
+ 1 +
+
+ + mImageLineEdit + mBrowseToolButton + mWidthSpinBox + mWidthUnitWidget + mRotationSpinBox + cboCoordinateMode + mSliderTransparency + mSpinTransparency + mSpinOffsetX + mSpinOffsetY + mOffsetUnitWidget + mDataDefinedPropertiesButton + + + + + mSliderTransparency + valueChanged(int) + mSpinTransparency + setValue(int) + + + 311 + 160 + + + 392 + 160 + + + + + mSpinTransparency + valueChanged(int) + mSliderTransparency + setValue(int) + + + 392 + 160 + + + 311 + 160 + + + + +
diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index e4857b636718..055c1e0ca3e9 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -132,6 +132,7 @@ ADD_QGIS_TEST(vectorlayercachetest testqgsvectorlayercache.cpp ) # ADD_QGIS_TEST(maprendererjobtest testmaprendererjob.cpp ) ADD_QGIS_TEST(spatialindextest testqgsspatialindex.cpp) ADD_QGIS_TEST(gradienttest testqgsgradients.cpp ) +ADD_QGIS_TEST(rasterfilltest testqgsrasterfill.cpp ) ADD_QGIS_TEST(shapebursttest testqgsshapeburst.cpp ) ADD_QGIS_TEST(invertedpolygontest testqgsinvertedpolygonrenderer.cpp ) ADD_QGIS_TEST(colorschemeregistry testqgscolorschemeregistry.cpp) diff --git a/tests/src/core/testqgsrasterfill.cpp b/tests/src/core/testqgsrasterfill.cpp new file mode 100644 index 000000000000..544e8f2a81c8 --- /dev/null +++ b/tests/src/core/testqgsrasterfill.cpp @@ -0,0 +1,165 @@ +/*************************************************************************** + testqgsrasterfill.cpp + --------------------- + Date : November 2014 + Copyright : (C) 2014 Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +//qgis includes... +#include +#include +#include +#include +#include +#include +#include +#include +#include +//qgis test includes +#include "qgsmultirenderchecker.h" + +/** \ingroup UnitTests + * This is a unit test for raster fill types. + */ +class TestQgsRasterFill: public QObject +{ + Q_OBJECT; + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {};// will be called before each testfunction is executed. + void cleanup() {};// will be called after every testfunction. + + void rasterFillSymbol(); + + private: + bool mTestHasError; + bool setQml( QString theType ); + bool imageCheck( QString theType ); + QgsMapSettings mMapSettings; + QgsVectorLayer * mpPolysLayer; + QgsRasterFillSymbolLayer* mRasterFill; + QgsFillSymbolV2* mFillSymbol; + QgsSingleSymbolRendererV2* mSymbolRenderer; + QString mTestDataDir; + QString mReport; +}; + + +void TestQgsRasterFill::initTestCase() +{ + mTestHasError = false; + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + QgsApplication::showSettings(); + + //create some objects that will be used in all tests... + QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt + mTestDataDir = myDataDir + QDir::separator(); + + // + //create a poly layer that will be used in all tests... + // + QString myPolysFileName = mTestDataDir + "polys.shp"; + QFileInfo myPolyFileInfo( myPolysFileName ); + mpPolysLayer = new QgsVectorLayer( myPolyFileInfo.filePath(), + myPolyFileInfo.completeBaseName(), "ogr" ); + + QgsVectorSimplifyMethod simplifyMethod; + simplifyMethod.setSimplifyHints( QgsVectorSimplifyMethod::NoSimplification ); + mpPolysLayer->setSimplifyMethod( simplifyMethod ); + + // Register the layer with the registry + QgsMapLayerRegistry::instance()->addMapLayers( + QList() << mpPolysLayer ); + + //setup raster fill + mRasterFill = new QgsRasterFillSymbolLayer(); + mFillSymbol = new QgsFillSymbolV2(); + mFillSymbol->changeSymbolLayer( 0, mRasterFill ); + mSymbolRenderer = new QgsSingleSymbolRendererV2( mFillSymbol ); + mpPolysLayer->setRendererV2( mSymbolRenderer ); + + // We only need maprender instead of mapcanvas + // since maprender does not require a qui + // and is more light weight + // + mMapSettings.setLayers( QStringList() << mpPolysLayer->id() ); + mReport += "

Raster Fill Renderer Tests

\n"; + +} +void TestQgsRasterFill::cleanupTestCase() +{ + QString myReportFile = QDir::tempPath() + QDir::separator() + "qgistest.html"; + QFile myFile( myReportFile ); + if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) ) + { + QTextStream myQTextStream( &myFile ); + myQTextStream << mReport; + myFile.close(); + } +} + +void TestQgsRasterFill::rasterFillSymbol() +{ + mReport += "

Raster fill symbol renderer test

\n"; + mRasterFill->setImageFilePath( mTestDataDir + QString( "sample_image.png" ) ); + mRasterFill->setWidth( 30.0 ); + bool result = imageCheck( "rasterfill" ); + QVERIFY( result ); +} + +// +// Private helper functions not called directly by CTest +// + +bool TestQgsRasterFill::setQml( QString theType ) +{ + //load a qml style and apply to our layer + //the style will correspond to the renderer + //type we are testing + QString myFileName = mTestDataDir + "polys_" + theType + "_symbol.qml"; + bool myStyleFlag = false; + QString error = mpPolysLayer->loadNamedStyle( myFileName, myStyleFlag ); + if ( !myStyleFlag ) + { + qDebug( "%s", error.toLocal8Bit().constData() ); + } + return myStyleFlag; +} + +bool TestQgsRasterFill::imageCheck( QString theTestType ) +{ + //use the QgsRenderChecker test utility class to + //ensure the rendered output matches our control image + mMapSettings.setExtent( mpPolysLayer->extent() ); + QgsMultiRenderChecker myChecker; + myChecker.setControlName( "expected_" + theTestType ); + myChecker.setMapSettings( mMapSettings ); + myChecker.setColorTolerance( 20 ); + bool myResultFlag = myChecker.runTest( theTestType, 500 ); + mReport += myChecker.report(); + return myResultFlag; +} + +QTEST_MAIN( TestQgsRasterFill ) +#include "testqgsrasterfill.moc" diff --git a/tests/testdata/control_images/expected_rasterfill/default/expected_rasterfill.png b/tests/testdata/control_images/expected_rasterfill/default/expected_rasterfill.png new file mode 100644 index 0000000000000000000000000000000000000000..29204013bda6bec35216ed7d9dc000ced4845e9e GIT binary patch literal 641536 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�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 literal 0 HcmV?d00001