Skip to content

Commit

Permalink
[feature][layouts] Add map_credits function
Browse files Browse the repository at this point in the history
This function collates a list of all the layer metadata attribution
strings for the layers shown inside a specified map item.

For example:

  array_to_string( map_credits( 'Main Map' ) )

Returns a comma separated list of layer credits for layers shown in the
'Main Map' layout item, e.g 'CC-BY-NC, CC-BY-SA'

There's an optional include_layer_names argument you can use
to include layer names before their attribution:

  array_to_string( map_credits( 'Main Map',
     include_layer_names := true,
     layer_name_separator := ': ' ) )

Returns a comma separated list of layer names and their credits for layers
shown in the 'Main Map' layout item,
e.g. 'Railway lines: CC-BY-NC, Basemap: CC-BY-SA'"
  • Loading branch information
nyalldawson committed Jan 10, 2021
1 parent ed4bb50 commit 88a0a8e
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 0 deletions.
12 changes: 12 additions & 0 deletions resources/function_help/json/map_credits
@@ -0,0 +1,12 @@
{
"name": "map_credits",
"type": "function",
"groups": ["Layout"],
"description": "Returns a list of credit (usage rights) strings for the layers shown in a layout map item.",
"arguments": [ {"arg":"id","description":"map item ID"},
{"arg":"include_layer_names","description":"Set to true to include layer names before their credit strings", "optional": true, "default": "false"},
{"arg":"layer_name_separator","description":"String to insert between layer names and their credit strings, if include_layer_names is true", "optional": true, "default": "': '"} ],
"examples": [ { "expression":"array_to_string( map_credits( 'Main Map' ) )", "returns":"comma separated list of layer credits for layers shown in the 'Main Map' layout item, e.g 'CC-BY-NC, CC-BY-SA'" },
{ "expression":"array_to_string( map_credits( 'Main Map', include_layer_names := true, layer_name_separator := ': ' ) )", "returns":"comma separated list of layer names and their credits for layers shown in the 'Main Map' layout item, e.g. 'Railway lines: CC-BY-NC, Basemap: CC-BY-SA'" }
]
}
66 changes: 66 additions & 0 deletions src/core/expression/qgsexpressioncontextutils.cpp
Expand Up @@ -30,6 +30,7 @@
#include "qgslayoutatlas.h"
#include "qgslayoutmultiframe.h"
#include "qgsfeatureid.h"
#include "qgslayoutitemmap.h"

QgsExpressionContextScope *QgsExpressionContextUtils::globalScope()
{
Expand Down Expand Up @@ -110,6 +111,69 @@ class GetLayoutItemVariables : public QgsScopedExpressionFunction

};


class GetLayoutMapLayerCredits : public QgsScopedExpressionFunction
{
public:
GetLayoutMapLayerCredits( const QgsLayout *c )
: QgsScopedExpressionFunction( QStringLiteral( "map_credits" ),
QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "id" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "include_layer_names" ), true, false )
<< QgsExpressionFunction::Parameter( QStringLiteral( "layer_name_separator" ), true, QStringLiteral( ": " ) ), QStringLiteral( "Layout" ) )
, mLayout( c )
{}

QVariant func( const QVariantList &values, const QgsExpressionContext *, QgsExpression *, const QgsExpressionNodeFunction * ) override
{
if ( !mLayout )
return QVariant();

QString id = values.value( 0 ).toString();

if ( QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( mLayout->itemById( id ) ) )
{
QgsExpressionContext c = map->createExpressionContext();
const QVariantList mapLayers = c.variable( QStringLiteral( "map_layers" ) ).toList();

const bool includeLayerNames = values.value( 1 ).toBool();
const QString layerNameSeparator = values.value( 2 ).toString();

QVariantList res;
for ( const QVariant &value : mapLayers )
{
if ( const QgsMapLayer *layer = qobject_cast< const QgsMapLayer * >( value.value< QObject * >() ) )
{
const QStringList credits = !layer->metadata().rights().isEmpty() ? layer->metadata().rights() : QStringList() << layer->attribution();
for ( const QString &credit : credits )
{
if ( credit.trimmed().isEmpty() )
continue;

const QString creditString = includeLayerNames ? layer->name() + layerNameSeparator + credit
: credit;

if ( !res.contains( creditString ) )
res << creditString;
}
}
}

return res;
}
return QVariant();
}

QgsScopedExpressionFunction *clone() const override
{
return new GetLayoutMapLayerCredits( mLayout );
}

private:

const QgsLayout *mLayout = nullptr;

};

class GetCurrentFormFieldValue : public QgsScopedExpressionFunction
{
public:
Expand Down Expand Up @@ -523,6 +587,7 @@ QgsExpressionContextScope *QgsExpressionContextUtils::layoutScope( const QgsLayo
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_dpi" ), layout->renderContext().dpi(), true ) );

scope->addFunction( QStringLiteral( "item_variables" ), new GetLayoutItemVariables( layout ) );
scope->addFunction( QStringLiteral( "map_credits" ), new GetLayoutMapLayerCredits( layout ) );

if ( layout->reportContext().layer() )
{
Expand Down Expand Up @@ -827,6 +892,7 @@ void QgsExpressionContextUtils::registerContextFunctions()
{
QgsExpression::registerFunction( new GetNamedProjectColor( nullptr ) );
QgsExpression::registerFunction( new GetLayoutItemVariables( nullptr ) );
QgsExpression::registerFunction( new GetLayoutMapLayerCredits( nullptr ) );
QgsExpression::registerFunction( new GetLayerVisibility( QList<QgsMapLayer *>(), 0.0 ) );
QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) );
QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) );
Expand Down
66 changes: 66 additions & 0 deletions tests/src/core/testqgslayoutitem.cpp
Expand Up @@ -174,6 +174,7 @@ class TestQgsLayoutItem: public QObject
void page();
void itemVariablesFunction();
void variables();
void mapCreditsFunction();

private:

Expand Down Expand Up @@ -1471,6 +1472,71 @@ void TestQgsLayoutItem::variables()
QCOMPARE( scope->variable( QStringLiteral( "var2" ) ).toInt(), 7 );
}

void TestQgsLayoutItem::mapCreditsFunction()
{
QgsRectangle extent( 2000, 2800, 2500, 2900 );
QgsLayout l( QgsProject::instance() );

QgsExpression e( QStringLiteral( "array_to_string( map_credits( 'Map_id' ) )" ) );
// no map
QgsExpressionContext c = l.createExpressionContext();
QVariant r = e.evaluate( &c );
QVERIFY( !r.isValid() );

QgsLayoutItemMap *map = new QgsLayoutItemMap( &l );
map->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) );
map->setExtent( extent );
l.addLayoutItem( map );
map->setId( QStringLiteral( "Map_id" ) );

c = l.createExpressionContext();
e.prepare( &c );
r = e.evaluate( &c );
// no layers
QCOMPARE( r.toString(), QString() );

// with layers
std::unique_ptr< QgsVectorLayer > layer = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "A" ), QStringLiteral( "memory" ) );
QgsLayerMetadata metadata;
metadata.setRights( QStringList() << QStringLiteral( "CC BY SA" ) );
layer->setMetadata( metadata );
std::unique_ptr< QgsVectorLayer > layer2 = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "B" ), QStringLiteral( "memory" ) );
metadata.setRights( QStringList() << QStringLiteral( "CC NC" ) );
layer2->setMetadata( metadata );
std::unique_ptr< QgsVectorLayer > layer3 = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "C" ), QStringLiteral( "memory" ) );
metadata.setRights( QStringList() << QStringLiteral( "CC BY SA" ) );
layer3->setMetadata( metadata );
std::unique_ptr< QgsVectorLayer > layer4 = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?field=id_a:integer" ), QStringLiteral( "C" ), QStringLiteral( "memory" ) );

map->setLayers( QList<QgsMapLayer *>() << layer.get() << layer2.get() << layer3.get() << layer4.get() );
e.prepare( &c );
QCOMPARE( e.evaluate( &c ).toString(), QStringLiteral( "CC BY SA,CC NC" ) );
map->setLayers( QList<QgsMapLayer *>() << layer.get() << layer3.get() << layer4.get() );
e.prepare( &c );
QCOMPARE( e.evaluate( &c ).toString(), QStringLiteral( "CC BY SA" ) );

QgsExpression e2( QStringLiteral( "array_to_string( map_credits( 'Map_id', include_layer_names:=true ) )" ) );
e2.prepare( &c );
QCOMPARE( e2.evaluate( &c ).toString(), QStringLiteral( "A: CC BY SA,C: CC BY SA" ) );
map->setLayers( QList<QgsMapLayer *>() << layer.get() << layer2.get() << layer3.get() << layer4.get() );
QgsExpression e3( QStringLiteral( "array_to_string( map_credits( 'Map_id', include_layer_names:=true, layer_name_separator:='|' ) )" ) );
e3.prepare( &c );
QCOMPARE( e3.evaluate( &c ).toString(), QStringLiteral( "A|CC BY SA,B|CC NC,C|CC BY SA" ) );

// second map
QgsLayoutItemMap *map2 = new QgsLayoutItemMap( &l );
map2->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
map2->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) );
map2->setExtent( extent );
l.addLayoutItem( map2 );
map2->setId( QStringLiteral( "Map_2" ) );
map2->setLayers( QList<QgsMapLayer *>() << layer.get() << layer4.get() );
QgsExpression e4( QStringLiteral( "array_to_string( map_credits( 'Map_2', include_layer_names:=true ) )" ) );
e4.prepare( &c );
QCOMPARE( e4.evaluate( &c ).toString(), QStringLiteral( "A: CC BY SA" ) );
}

void TestQgsLayoutItem::rotation()
{
QgsProject proj;
Expand Down

0 comments on commit 88a0a8e

Please sign in to comment.