Allow any symbol to be an animated symbol

Users can now indicate that a symbol should be treated as a animated
symbol, through the new "Animation Settings" option in the symbol
widget's Advanced menu.

This settings panel allows users to enable animation for the symbol
and set a specific frame rate at which the symbol should be redrawn.
When enabled, the @symbol_frame variable can be used in any
symbol data defined property in order to animate that property.

For instance, setting the symbol's rotation to the data defined
expression

    @symbol_frame % 360

will cause the symbol to rotate over time. (with rotation speed
dictated by the symbol's refresh rate)
This commit is contained in:
Nyall Dawson 2022-04-09 13:43:35 +10:00
parent de3f920f9c
commit 4eede35d55
22 changed files with 665 additions and 5 deletions

View File

@ -11,6 +11,58 @@
typedef QList<QgsSymbolLayer *> QgsSymbolLayerList;
class QgsSymbolAnimationSettings
{
%Docstring(signature="appended")
Contains settings relating to symbol animation.
.. versionadded:: 3.26
%End
%TypeHeaderCode
#include "qgssymbol.h"
%End
public:
void setIsAnimated( bool animated );
%Docstring
Sets whether the symbol is animated.
This is a user-facing setting for symbols, which allows users to define whether a
symbol is animated, and allows for creation of animated symbols via data
defined properties.
.. seealso:: :py:func:`isAnimated`
%End
bool isAnimated() const;
%Docstring
Returns ``True`` if the symbol is animated.
This is a user-facing setting for symbols, which allows users to define whether a
symbol is animated, and allows for creation of animated symbols via data
defined properties.
.. seealso:: :py:func:`setIsAnimated`
%End
void setFrameRate( double rate );
%Docstring
Sets the symbol animation frame ``rate`` (in frames per second).
.. seealso:: :py:func:`frameRate`
%End
double frameRate() const;
%Docstring
Returns the symbol animation frame rate (in frames per second).
.. seealso:: :py:func:`setFrameRate`
%End
};
class QgsSymbol
{
%Docstring(signature="appended")
@ -522,6 +574,25 @@ direction.
.. seealso:: :py:func:`setForceRHR`
.. versionadded:: 3.6
%End
QgsSymbolAnimationSettings &animationSettings();
%Docstring
Returns a reference to the symbol animation settings.
.. seealso:: :py:func:`setAnimationSettings`
.. versionadded:: 3.26
%End
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
%Docstring
Sets a the symbol animation ``settings``.
.. seealso:: :py:func:`animationSettings`
.. versionadded:: 3.26
%End
QSet<QString> usedAttributes( const QgsRenderContext &context ) const;
@ -704,6 +775,7 @@ Render editing vertex marker at specified point
private:
QgsSymbol( const QgsSymbol & );
};

View File

@ -0,0 +1,89 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/symbology/qgssymbolanimationsettingswidget.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsSymbolAnimationSettingsWidget: QgsPanelWidget
{
%Docstring(signature="appended")
A widget for customising animation settings for a symbol.
.. versionadded:: 3.26
%End
%TypeHeaderCode
#include "qgssymbolanimationsettingswidget.h"
%End
public:
QgsSymbolAnimationSettingsWidget( QWidget *parent /TransferThis/ = 0 );
%Docstring
Constructor for QgsSymbolAnimationSettingsWidget
%End
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
%Docstring
Sets the animation ``settings`` to show in the widget.
.. seealso:: :py:func:`animationSettings`
%End
QgsSymbolAnimationSettings animationSettings() const;
%Docstring
Returns the animation settings as defined in the widget.
.. seealso:: :py:func:`setAnimationSettings`
%End
};
class QgsSymbolAnimationSettingsDialog : QDialog
{
%Docstring(signature="appended")
A dialog for customising animation settings for a symbol.
.. versionadded:: 3.26
%End
%TypeHeaderCode
#include "qgssymbolanimationsettingswidget.h"
%End
public:
QgsSymbolAnimationSettingsDialog( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags f = Qt::WindowFlags() );
%Docstring
Constructor for QgsSymbolAnimationSettingsDialog
%End
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
%Docstring
Sets the animation ``settings`` to show in the dialog.
.. seealso:: :py:func:`animationSettings`
%End
QgsSymbolAnimationSettings animationSettings() const;
%Docstring
Returns the animation settings as defined in the dialog.
.. seealso:: :py:func:`setAnimationSettings`
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/symbology/qgssymbolanimationsettingswidget.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -447,6 +447,7 @@
%Include auto_generated/symbology/qgsstylemanagerdialog.sip
%Include auto_generated/symbology/qgsstylesavedialog.sip
%Include auto_generated/symbology/qgssvgselectorwidget.sip
%Include auto_generated/symbology/qgssymbolanimationsettingswidget.sip
%Include auto_generated/symbology/qgssymbollayerwidget.sip
%Include auto_generated/symbology/qgssymbollevelsdialog.sip
%Include auto_generated/symbology/qgssymbolselectordialog.sip

View File

@ -854,6 +854,7 @@ void QgsExpression::initVariableHelp()
sVariableHelpTexts()->insert( QStringLiteral( "symbol_layer_index" ), QCoreApplication::translate( "symbol_layer_index", "Current symbol layer index." ) );
sVariableHelpTexts()->insert( QStringLiteral( "symbol_marker_row" ), QCoreApplication::translate( "symbol_marker_row", "Row number for marker (valid for point pattern fills only)." ) );
sVariableHelpTexts()->insert( QStringLiteral( "symbol_marker_column" ), QCoreApplication::translate( "symbol_marker_column", "Column number for marker (valid for point pattern fills only)." ) );
sVariableHelpTexts()->insert( QStringLiteral( "symbol_frame" ), QCoreApplication::translate( "symbol_frame", "Frame number (for animated symbols only)." ) );
sVariableHelpTexts()->insert( QStringLiteral( "symbol_label" ), QCoreApplication::translate( "symbol_label", "Label for the symbol (either a user defined label or the default autogenerated label)." ) );
sVariableHelpTexts()->insert( QStringLiteral( "symbol_id" ), QCoreApplication::translate( "symbol_id", "Internal ID of the symbol." ) );

View File

@ -152,6 +152,7 @@ QgsFillSymbol *QgsFillSymbol::clone() const
cloneSymbol->setForceRHR( mForceRHR );
cloneSymbol->setDataDefinedProperties( dataDefinedProperties() );
cloneSymbol->setFlags( mSymbolFlags );
cloneSymbol->setAnimationSettings( mAnimationSettings );
return cloneSymbol;
}

View File

@ -283,5 +283,6 @@ QgsLineSymbol *QgsLineSymbol::clone() const
cloneSymbol->setForceRHR( mForceRHR );
cloneSymbol->setDataDefinedProperties( dataDefinedProperties() );
cloneSymbol->setFlags( mSymbolFlags );
cloneSymbol->setAnimationSettings( mAnimationSettings );
return cloneSymbol;
}

View File

@ -498,6 +498,7 @@ QgsMarkerSymbol *QgsMarkerSymbol::clone() const
cloneSymbol->setForceRHR( mForceRHR );
cloneSymbol->setDataDefinedProperties( dataDefinedProperties() );
cloneSymbol->setFlags( mSymbolFlags );
cloneSymbol->setAnimationSettings( mAnimationSettings );
return cloneSymbol;
}

View File

@ -352,6 +352,21 @@ void QgsSymbol::setMapUnitScale( const QgsMapUnitScale &scale )
}
}
QgsSymbolAnimationSettings &QgsSymbol::animationSettings()
{
return mAnimationSettings;
}
const QgsSymbolAnimationSettings &QgsSymbol::animationSettings() const
{
return mAnimationSettings;
}
void QgsSymbol::setAnimationSettings( const QgsSymbolAnimationSettings &settings )
{
mAnimationSettings = settings;
}
QgsSymbol *QgsSymbol::defaultSymbol( QgsWkbTypes::GeometryType geomType )
{
std::unique_ptr< QgsSymbol > s;
@ -498,6 +513,26 @@ void QgsSymbol::startRender( QgsRenderContext &context, const QgsFields &fields
QgsSymbolRenderContext symbolContext( context, QgsUnitTypes::RenderUnknownUnit, mOpacity, false, mRenderHints, nullptr, fields );
std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::updateSymbolScope( this, new QgsExpressionContextScope() ) );
if ( mAnimationSettings.isAnimated() )
{
const long long mapFrameNumber = context.currentFrame();
double animationTimeSeconds = 0;
if ( mapFrameNumber >= 0 && context.frameRate() > 0 )
{
// render is part of an animation, so we base the calculated frame on that
animationTimeSeconds = mapFrameNumber / context.frameRate();
}
else
{
// render is outside of animation, so base the calculated frame on the current epoch
animationTimeSeconds = QDateTime::currentMSecsSinceEpoch() / 1000.0;
}
const long long symbolFrame = static_cast< long long >( std::floor( animationTimeSeconds * mAnimationSettings.frameRate() ) );
scope->setVariable( QStringLiteral( "symbol_frame" ), symbolFrame, true );
}
mSymbolRenderContext->setExpressionContextScope( scope.release() );
mDataDefinedProperties.prepare( context.expressionContext() );

View File

@ -28,6 +28,61 @@ class QgsLineSymbolLayer;
typedef QList<QgsSymbolLayer *> QgsSymbolLayerList;
/**
* \ingroup core
* \class QgsSymbol
*
* \brief Contains settings relating to symbol animation.
*
* \since QGIS 3.26
*/
class CORE_EXPORT QgsSymbolAnimationSettings
{
public:
/**
* Sets whether the symbol is animated.
*
* This is a user-facing setting for symbols, which allows users to define whether a
* symbol is animated, and allows for creation of animated symbols via data
* defined properties.
*
* \see isAnimated()
*/
void setIsAnimated( bool animated ) { mIsAnimated = animated; }
/**
* Returns TRUE if the symbol is animated.
*
* This is a user-facing setting for symbols, which allows users to define whether a
* symbol is animated, and allows for creation of animated symbols via data
* defined properties.
*
* \see setIsAnimated()
*/
bool isAnimated() const { return mIsAnimated; }
/**
* Sets the symbol animation frame \a rate (in frames per second).
*
* \see frameRate()
*/
void setFrameRate( double rate ) { mFrameRate = rate; }
/**
* Returns the symbol animation frame rate (in frames per second).
*
* \see setFrameRate()
*/
double frameRate() const { return mFrameRate; }
private:
bool mIsAnimated = false;
double mFrameRate = 10;
};
/**
* \ingroup core
* \class QgsSymbol
@ -518,6 +573,30 @@ class CORE_EXPORT QgsSymbol
*/
bool forceRHR() const { return mForceRHR; }
/**
* Returns a reference to the symbol animation settings.
*
* \see setAnimationSettings()
* \since QGIS 3.26
*/
QgsSymbolAnimationSettings &animationSettings();
/**
* Returns a reference to the symbol animation settings.
*
* \see setAnimationSettings()
* \since QGIS 3.26
*/
const QgsSymbolAnimationSettings &animationSettings() const SIP_SKIP;
/**
* Sets a the symbol animation \a settings.
*
* \see animationSettings()
* \since QGIS 3.26
*/
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
/**
* Returns a list of attributes required to render this feature.
* This should include any attributes required by the symbology including
@ -721,6 +800,8 @@ class CORE_EXPORT QgsSymbol
bool mClipFeaturesToExtent = true;
bool mForceRHR = false;
QgsSymbolAnimationSettings mAnimationSettings;
Q_DECL_DEPRECATED const QgsVectorLayer *mLayer = nullptr; //current vectorlayer
private:

View File

@ -1308,6 +1308,9 @@ QgsSymbol *QgsSymbolLayerUtils::loadSymbol( const QDomElement &element, const Qg
flags |= Qgis::SymbolFlag::RendererShouldUseSymbolLevels;
symbol->setFlags( flags );
symbol->animationSettings().setIsAnimated( element.attribute( QStringLiteral( "is_animated" ), QStringLiteral( "0" ) ).toInt() );
symbol->animationSettings().setFrameRate( element.attribute( QStringLiteral( "frame_rate" ), QStringLiteral( "10" ) ).toDouble() );
const QDomElement ddProps = element.firstChildElement( QStringLiteral( "data_defined_properties" ) );
if ( !ddProps.isNull() )
{
@ -1402,6 +1405,9 @@ QDomElement QgsSymbolLayerUtils::saveSymbol( const QString &name, const QgsSymbo
if ( symbol->flags() & Qgis::SymbolFlag::RendererShouldUseSymbolLevels )
symEl.setAttribute( QStringLiteral( "renderer_should_use_levels" ), QStringLiteral( "1" ) );
symEl.setAttribute( QStringLiteral( "is_animated" ), symbol->animationSettings().isAnimated() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
symEl.setAttribute( QStringLiteral( "frame_rate" ), qgsDoubleToString( symbol->animationSettings().frameRate() ) );
//QgsDebugMsg( "num layers " + QString::number( symbol->symbolLayerCount() ) );
QDomElement ddProps = doc.createElement( QStringLiteral( "data_defined_properties" ) );
@ -4966,6 +4972,13 @@ double QgsSymbolLayerUtils::rendererFrameRate( const QgsFeatureRenderer *rendere
void visitSymbol( const QgsSymbol *symbol )
{
// symbol may be marked as animated on a symbol level (e.g. when it implements animation
// via data defined properties)
if ( symbol->animationSettings().isAnimated() )
{
if ( symbol->animationSettings().frameRate() > refreshRate )
refreshRate = symbol->animationSettings().frameRate();
}
for ( int idx = 0; idx < symbol->symbolLayerCount(); idx++ )
{
const QgsSymbolLayer *sl = symbol->symbolLayer( idx );

View File

@ -61,6 +61,7 @@ set(QGIS_GUI_SRCS
symbology/qgsstylemanagerdialog.cpp
symbology/qgsstylesavedialog.cpp
symbology/qgssvgselectorwidget.cpp
symbology/qgssymbolanimationsettingswidget.cpp
symbology/qgssymbollayerwidget.cpp
symbology/qgssymbollevelsdialog.cpp
symbology/qgssymbolslistwidget.cpp
@ -1316,6 +1317,7 @@ set(QGIS_GUI_HDRS
symbology/qgsstylemanagerdialog.h
symbology/qgsstylesavedialog.h
symbology/qgssvgselectorwidget.h
symbology/qgssymbolanimationsettingswidget.h
symbology/qgssymbollayerwidget.h
symbology/qgssymbollevelsdialog.h
symbology/qgssymbolselectordialog.h

View File

@ -281,6 +281,7 @@ QgsExpressionContext QgsLayerPropertiesWidget::createExpressionContext() const
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_layer_index" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_row" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_column" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_frame" ), 1, true ) );
// additional scopes
const auto constAdditionalExpressionContextScopes = mContext.additionalExpressionContextScopes();
@ -297,7 +298,8 @@ QgsExpressionContext QgsLayerPropertiesWidget::createExpressionContext() const
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" ) );
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" )
<< QStringLiteral( "symbol_frame" ) );
return expContext;
}

View File

@ -0,0 +1,85 @@
/***************************************************************************
qgssymbolanimationsettingswidget.cpp
---------------------
begin : April 2022
copyright : (C) 2022 by 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 "qgssymbolanimationsettingswidget.h"
#include "qgssymbol.h"
#include <QDialogButtonBox>
QgsSymbolAnimationSettingsWidget::QgsSymbolAnimationSettingsWidget( QWidget *parent )
: QgsPanelWidget( parent )
{
setupUi( this );
mFrameRateSpin->setClearValue( 10 );
mFrameRateSpin->setShowClearButton( true );
connect( mFrameRateSpin, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ this ]
{
if ( !mBlockUpdates )
emit widgetChanged();
} );
connect( mIsAnimatedGroup, &QGroupBox::toggled, this, [ this ]
{
if ( !mBlockUpdates )
emit widgetChanged();
} );
}
void QgsSymbolAnimationSettingsWidget::setAnimationSettings( const QgsSymbolAnimationSettings &settings )
{
mBlockUpdates = true;
mIsAnimatedGroup->setChecked( settings.isAnimated() );
mFrameRateSpin->setValue( settings.frameRate() );
mBlockUpdates = false;
}
QgsSymbolAnimationSettings QgsSymbolAnimationSettingsWidget::animationSettings() const
{
QgsSymbolAnimationSettings settings;
settings.setIsAnimated( mIsAnimatedGroup->isChecked() );
settings.setFrameRate( mFrameRateSpin->value() );
return settings;
}
//
// QgsSymbolAnimationSettingsDialog
//
QgsSymbolAnimationSettingsDialog::QgsSymbolAnimationSettingsDialog( QWidget *parent, Qt::WindowFlags f )
: QDialog( parent, f )
{
QVBoxLayout *vLayout = new QVBoxLayout();
mWidget = new QgsSymbolAnimationSettingsWidget( );
vLayout->addWidget( mWidget );
QDialogButtonBox *bbox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal );
connect( bbox, &QDialogButtonBox::accepted, this, &QgsSymbolAnimationSettingsDialog::accept );
connect( bbox, &QDialogButtonBox::rejected, this, &QgsSymbolAnimationSettingsDialog::reject );
vLayout->addWidget( bbox );
setLayout( vLayout );
setWindowTitle( tr( "Animation Settings" ) );
}
void QgsSymbolAnimationSettingsDialog::setAnimationSettings( const QgsSymbolAnimationSettings &settings )
{
mWidget->setAnimationSettings( settings );
}
QgsSymbolAnimationSettings QgsSymbolAnimationSettingsDialog::animationSettings() const
{
return mWidget->animationSettings();
}

View File

@ -0,0 +1,96 @@
/***************************************************************************
qgssymbolanimationsettingswidget.h
---------------------
begin : April 2022
copyright : (C) 2022 by 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. *
* *
***************************************************************************/
#ifndef QGSSYMBOLANIMATIONSETTINGSWIDGET_H
#define QGSSYMBOLANIMATIONSETTINGSWIDGET_H
#include "ui_qgssymbolanimationsettingswidgetbase.h"
#include "qgis_gui.h"
#include "qgis_sip.h"
#include "qgspanelwidget.h"
#include <QDialog>
class QgsSymbolAnimationSettings;
/**
* \ingroup gui
* \brief A widget for customising animation settings for a symbol.
* \since QGIS 3.26
*/
class GUI_EXPORT QgsSymbolAnimationSettingsWidget: public QgsPanelWidget, private Ui::QgsSymbolAnimationSettingsWidgetBase
{
Q_OBJECT
public:
//! Constructor for QgsSymbolAnimationSettingsWidget
QgsSymbolAnimationSettingsWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr );
/**
* Sets the animation \a settings to show in the widget.
*
* \see animationSettings()
*/
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
/**
* Returns the animation settings as defined in the widget.
*
* \see setAnimationSettings()
*/
QgsSymbolAnimationSettings animationSettings() const;
private:
bool mBlockUpdates = false;
};
/**
* \ingroup gui
* \brief A dialog for customising animation settings for a symbol.
* \since QGIS 3.26
*/
class GUI_EXPORT QgsSymbolAnimationSettingsDialog : public QDialog
{
Q_OBJECT
public:
//! Constructor for QgsSymbolAnimationSettingsDialog
QgsSymbolAnimationSettingsDialog( QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags f = Qt::WindowFlags() );
/**
* Sets the animation \a settings to show in the dialog.
*
* \see animationSettings()
*/
void setAnimationSettings( const QgsSymbolAnimationSettings &settings );
/**
* Returns the animation settings as defined in the dialog.
*
* \see setAnimationSettings()
*/
QgsSymbolAnimationSettings animationSettings() const;
private:
QgsSymbolAnimationSettingsWidget *mWidget = nullptr;
};
#endif // QGSSYMBOLANIMATIONSETTINGSWIDGET_H

View File

@ -90,6 +90,7 @@ QgsExpressionContext QgsSymbolLayerWidget::createExpressionContext() const
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_layer_index" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_row" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_column" ), 1, true ) );
expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_frame" ), 1, true ) );
// additional scopes
const auto constAdditionalExpressionContextScopes = mContext.additionalExpressionContextScopes();
@ -107,7 +108,7 @@ QgsExpressionContext QgsSymbolLayerWidget::createExpressionContext() const
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" );
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" ) << QStringLiteral( "symbol_frame" );
if ( expContext.hasVariable( QStringLiteral( "zoom_level" ) ) )

View File

@ -25,6 +25,7 @@
#include "qgsmarkersymbol.h"
#include "qgslinesymbol.h"
#include "qgsfillsymbol.h"
#include "qgssymbolanimationsettingswidget.h"
#include <QMessageBox>
@ -50,7 +51,8 @@ QgsSymbolsListWidget::QgsSymbolsListWidget( QgsSymbol *symbol, QgsStyle *style,
mStandardizeRingsAction = new QAction( tr( "Force Right-Hand-Rule Orientation" ), this );
mStandardizeRingsAction->setCheckable( true );
connect( mStandardizeRingsAction, &QAction::toggled, this, &QgsSymbolsListWidget::forceRHRToggled );
mAnimationSettingsAction = new QAction( tr( "Animation Settings…" ), this );
connect( mAnimationSettingsAction, &QAction::triggered, this, &QgsSymbolsListWidget::showAnimationSettings );
// select correct page in stacked widget
QgsPropertyOverrideButton *opacityDDBtn = nullptr;
@ -136,6 +138,7 @@ QgsSymbolsListWidget::~QgsSymbolsListWidget()
// The menu can be passed in the constructor, so may live longer than this widget
mStyleItemsListWidget->advancedMenu()->removeAction( mClipFeaturesAction );
mStyleItemsListWidget->advancedMenu()->removeAction( mStandardizeRingsAction );
mStyleItemsListWidget->advancedMenu()->removeAction( mAnimationSettingsAction );
}
void QgsSymbolsListWidget::registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsSymbolLayer::Property key )
@ -260,6 +263,32 @@ void QgsSymbolsListWidget::forceRHRToggled( bool checked )
emit changed();
}
void QgsSymbolsListWidget::showAnimationSettings()
{
QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this );
if ( panel && panel->dockMode() )
{
QgsSymbolAnimationSettingsWidget *widget = new QgsSymbolAnimationSettingsWidget( panel );
widget->setPanelTitle( tr( "Animation Settings" ) );
widget->setAnimationSettings( mSymbol->animationSettings() );
connect( widget, &QgsPanelWidget::widgetChanged, this, [ this, widget ]()
{
mSymbol->setAnimationSettings( widget->animationSettings() );
emit changed();
} );
panel->openPanel( widget );
return;
}
QgsSymbolAnimationSettingsDialog d( this );
d.setAnimationSettings( mSymbol->animationSettings() );
if ( d.exec() == QDialog::Accepted )
{
mSymbol->setAnimationSettings( d.animationSettings() );
emit changed();
}
}
void QgsSymbolsListWidget::saveSymbol()
{
if ( !mStyle )
@ -474,7 +503,8 @@ QgsExpressionContext QgsSymbolsListWidget::createExpressionContext() const
<< QgsExpressionContext::EXPR_GEOMETRY_RING_NUM
<< QgsExpressionContext::EXPR_GEOMETRY_POINT_COUNT << QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM
<< QgsExpressionContext::EXPR_CLUSTER_COLOR << QgsExpressionContext::EXPR_CLUSTER_SIZE
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" ) );
<< QStringLiteral( "symbol_layer_count" ) << QStringLiteral( "symbol_layer_index" )
<< QStringLiteral( "symbol_frame" ) );
return expContext;
}
@ -546,6 +576,10 @@ void QgsSymbolsListWidget::updateSymbolInfo()
{
mStyleItemsListWidget->advancedMenu()->removeAction( action );
}
else if ( mAnimationSettingsAction->text() == action->text() )
{
mStyleItemsListWidget->advancedMenu()->removeAction( action );
}
}
if ( mSymbol->type() == Qgis::SymbolType::Line || mSymbol->type() == Qgis::SymbolType::Fill )
@ -557,6 +591,7 @@ void QgsSymbolsListWidget::updateSymbolInfo()
{
mStyleItemsListWidget->advancedMenu()->addAction( mStandardizeRingsAction );
}
mStyleItemsListWidget->advancedMenu()->addAction( mAnimationSettingsAction );
mStyleItemsListWidget->showAdvancedButton( mAdvancedMenu || !mStyleItemsListWidget->advancedMenu()->isEmpty() );

View File

@ -96,6 +96,7 @@ class GUI_EXPORT QgsSymbolsListWidget : public QWidget, private Ui::SymbolsListW
void createAuxiliaryField();
void createSymbolAuxiliaryField();
void forceRHRToggled( bool checked );
void showAnimationSettings();
void saveSymbol();
void updateSymbolDataDefinedProperty();
@ -109,6 +110,7 @@ class GUI_EXPORT QgsSymbolsListWidget : public QWidget, private Ui::SymbolsListW
QMenu *mAdvancedMenu = nullptr;
QAction *mClipFeaturesAction = nullptr;
QAction *mStandardizeRingsAction = nullptr;
QAction *mAnimationSettingsAction = nullptr;
QgsVectorLayer *mLayer = nullptr;
QgsMapCanvas *mMapCanvas = nullptr;

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsSymbolAnimationSettingsWidgetBase</class>
<widget class="QgsPanelWidget" name="QgsSymbolAnimationSettingsWidgetBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>343</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dash Space Pattern</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="mIsAnimatedGroup">
<property name="title">
<string>Is Animated</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="1,2">
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Frame rate</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QgsDoubleSpinBox" name="mFrameRateSpin">
<property name="suffix">
<string> fps</string>
</property>
<property name="minimum">
<double>0.010000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>10.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>The symbol will be animated using the specified frame rate. The &lt;code&gt;@symbol_frame&lt;/code&gt; variable can be used in symbol property expressions in order to dynamically alter the symbol's appearance over time.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsPanelWidget</class>
<extends>QWidget</extends>
<header>qgspanelwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>qgsdoublespinbox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -636,6 +636,28 @@ class TestQgsSymbol(unittest.TestCase):
assert self.imageCheck('Reprojection errors linestring', 'reprojection_errors_linestring', image)
def test_animation_settings(self):
s = QgsFillSymbol()
self.assertFalse(s.animationSettings().isAnimated())
s.animationSettings().setIsAnimated(True)
self.assertTrue(s.animationSettings().isAnimated())
s.animationSettings().setFrameRate(30)
self.assertEqual(s.animationSettings().frameRate(), 30)
s.setForceRHR(True)
doc = QDomDocument()
context = QgsReadWriteContext()
element = QgsSymbolLayerUtils.saveSymbol('test', s, doc, context)
s2 = QgsSymbolLayerUtils.loadSymbol(element, context)
self.assertTrue(s2.animationSettings().isAnimated())
self.assertEqual(s2.animationSettings().frameRate(), 30)
s3 = s2.clone()
self.assertTrue(s3.animationSettings().isAnimated())
self.assertEqual(s3.animationSettings().frameRate(), 30)
def renderCollection(self, geom, symbol):
f = QgsFeature()
f.setGeometry(geom)
@ -926,7 +948,24 @@ class TestQgsMarkerSymbol(unittest.TestCase):
rendered_image = self.renderGeometry(s, g, QgsMapSettings.DrawSymbolBounds)
self.assertTrue(self.imageCheck('marker_bounds_layer_disabled', 'marker_bounds_layer_disabled', rendered_image))
def renderGeometry(self, symbol, geom, flags=QgsMapSettings.Flags()):
def test_animation(self):
markerSymbol = QgsMarkerSymbol()
markerSymbol.deleteSymbolLayer(0)
markerSymbol.appendSymbolLayer(
QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayerBase.Triangle, color=QColor(255, 0, 0),
strokeColor=QColor(0, 255, 0), size=10, angle=0))
markerSymbol[0].setStrokeStyle(Qt.NoPen)
markerSymbol.animationSettings().setIsAnimated(True)
markerSymbol[0].setDataDefinedProperty(QgsSymbolLayer.PropertyAngle, QgsProperty.fromExpression('@symbol_frame * 90'))
g = QgsGeometry.fromWkt('Point(1 1)')
rendered_image = self.renderGeometry(markerSymbol, g, frame=0)
self.assertTrue(self.imageCheck('animated_frame1', 'animated_frame1', rendered_image))
rendered_image = self.renderGeometry(markerSymbol, g, frame=1)
self.assertTrue(self.imageCheck('animated_frame2', 'animated_frame2', rendered_image))
def renderGeometry(self, symbol, geom, flags=QgsMapSettings.Flags(), frame=None):
f = QgsFeature()
f.setGeometry(geom)
@ -945,6 +984,9 @@ class TestQgsMarkerSymbol(unittest.TestCase):
ms.setExtent(extent)
ms.setOutputSize(image.size())
if frame is not None:
ms.setFrameRate(10)
ms.setCurrentFrame(frame)
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI

View File

@ -635,6 +635,14 @@ class PyQgsSymbolLayerUtils(unittest.TestCase):
renderer = QgsSingleSymbolRenderer(marker_symbol)
self.assertEqual(QgsSymbolLayerUtils.rendererFrameRate(renderer), 60)
s = QgsMarkerSymbol()
renderer = QgsSingleSymbolRenderer(s.clone())
self.assertEqual(QgsSymbolLayerUtils.rendererFrameRate(renderer), -1)
s.animationSettings().setIsAnimated(True)
s.animationSettings().setFrameRate(30)
renderer = QgsSingleSymbolRenderer(s.clone())
self.assertEqual(QgsSymbolLayerUtils.rendererFrameRate(renderer), 30)
def imageCheck(self, name, reference_image, image):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B