Skip to content
Permalink
Browse files
[FEATURE] Embed atlas feature into composer HTML source as GeoJSON
This change makes the current atlas feature (and additionally all
attributes of related child features) available to the source of
a composer HTML item, allowing the item to dynamically adjust
its rendered HTML in response to the feature's properties. An
example use case is dynamically populating a HTML table with
all the attributes of related child features for the atlas
feature.

To use this, the HTML source must implement a "setFeature(feature)"
JavaScript function. This function is called whenever the atlas
feature changes, and is passed the atlas feature (+related attributes)
as a GeoJSON Feature.

Sponsored by Kanton of Zug, Switzerland
  • Loading branch information
nyalldawson committed May 9, 2016
1 parent c3d6c79 commit 794ab06
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 0 deletions.
@@ -25,6 +25,7 @@
#include "qgsvectorlayer.h"
#include "qgsproject.h"
#include "qgsdistancearea.h"
#include "qgsjsonutils.h"

#include "qgswebpage.h"
#include "qgswebframe.h"
@@ -210,6 +211,14 @@ void QgsComposerHtml::loadHtml( const bool useCache, const QgsExpressionContext
qApp->processEvents();
}

//inject JSON feature
if ( !mAtlasFeatureJSON.isEmpty() )
{
mWebPage->mainFrame()->evaluateJavaScript( QString( "if ( typeof setFeature === \"function\" ) { setFeature(%1); }" ).arg( mAtlasFeatureJSON ) );
//needs an extra process events here to give javascript a chance to execute
qApp->processEvents();
}

recalculateFrameSizes();
//trigger a repaint
emit contentsChanged();
@@ -544,6 +553,11 @@ void QgsComposerHtml::setExpressionContext( const QgsFeature &feature, QgsVector
mDistanceArea->setEllipsoidalMode( mComposition->mapSettings().hasCrsTransformEnabled() );
}
mDistanceArea->setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) );

// create JSON representation of feature
QgsJSONExporter exporter( layer );
exporter.setIncludeRelated( true );
mAtlasFeatureJSON = exporter.exportFeature( feature );
}

void QgsComposerHtml::refreshExpressionContext()
@@ -248,6 +248,9 @@ class CORE_EXPORT QgsComposerHtml: public QgsComposerMultiFrame
QString mUserStylesheet;
bool mEnableUserStylesheet;

//! JSON string representation of current atlas feature
QString mAtlasFeatureJSON;

QgsNetworkContentFetcher* mFetcher;

double htmlUnitsToMM(); //calculate scale factor
@@ -21,6 +21,10 @@
#include "qgscomposition.h"
#include "qgsmultirenderchecker.h"
#include "qgsfontutils.h"
#include "qgsvectorlayer.h"
#include "qgsmaplayerregistry.h"
#include "qgsvectordataprovider.h"
#include "qgsproject.h"
#include <QObject>
#include <QtTest/QtTest>

@@ -43,6 +47,8 @@ class TestQgsComposerHtml : public QObject
void table(); //test if rendering a HTML url works
void tableMultiFrame(); //tests multiframe capabilities of composer html
void htmlMultiFrameSmartBreak(); //tests smart page breaks in html multi frame
void javascriptSetFeature(); //test that JavaScript setFeature() function is correctly called

private:
QgsComposition *mComposition;
QgsMapSettings *mMapSettings;
@@ -243,6 +249,80 @@ void TestQgsComposerHtml::htmlMultiFrameSmartBreak()
QVERIFY( result );
}

void TestQgsComposerHtml::javascriptSetFeature()
{
//test that JavaScript setFeature() function is correctly called

// first need to setup some layers with a relation

//parent layer
QgsVectorLayer* parentLayer = new QgsVectorLayer( "Point?field=fldtxt:string&field=fldint:integer&field=foreignkey:integer", "parent", "memory" );
QgsVectorDataProvider* pr = parentLayer->dataProvider();
QgsFeature pf1;
pf1.setFields( parentLayer->fields() );
pf1.setAttributes( QgsAttributes() << "test1" << 67 << 123 );
QgsFeature pf2;
pf2.setFields( parentLayer->fields() );
pf2.setAttributes( QgsAttributes() << "test2" << 68 << 124 );
QVERIFY( pr->addFeatures( QgsFeatureList() << pf1 << pf2 ) );

// child layer
QgsVectorLayer* childLayer = new QgsVectorLayer( "Point?field=x:string&field=y:integer&field=z:integer", "referencedlayer", "memory" );
pr = childLayer->dataProvider();
QgsFeature f1;
f1.setFields( childLayer->fields() );
f1.setAttributes( QgsAttributes() << "foo" << 123 << 321 );
QgsFeature f2;
f2.setFields( childLayer->fields() );
f2.setAttributes( QgsAttributes() << "bar" << 123 << 654 );
QgsFeature f3;
f3.setFields( childLayer->fields() );
f3.setAttributes( QgsAttributes() << "foobar" << 124 << 554 );
QVERIFY( pr->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );

QgsMapLayerRegistry::instance()->addMapLayers( QList<QgsMapLayer *>() << childLayer << parentLayer );

//atlas
mComposition->atlasComposition().setCoverageLayer( parentLayer );
mComposition->atlasComposition().setEnabled( true );

QgsRelation rel;
rel.setRelationId( "rel1" );
rel.setRelationName( "relation one" );
rel.setReferencingLayer( childLayer->id() );
rel.setReferencedLayer( parentLayer->id() );
rel.addFieldPair( "y", "foreignkey" );
QgsProject::instance()->relationManager()->addRelation( rel );

QgsComposerHtml* htmlItem = new QgsComposerHtml( mComposition, false );
QgsComposerFrame* htmlFrame = new QgsComposerFrame( mComposition, htmlItem, 0, 0, 100, 200 );
htmlFrame->setFrameEnabled( true );
htmlItem->addFrame( htmlFrame );
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
htmlItem->setEvaluateExpressions( true );
// hopefully arial bold 40px is big enough to avoid cross-platform rendering issues
htmlItem->setHtml( QString( "<body style=\"margin: 10px; font-family: Arial; font-weight: bold; font-size: 40px;\">"
"<div id=\"dest\"></div><script>setFeature=function(feature){"
"document.getElementById('dest').innerHTML = feature.properties.foreignkey + ',' +"
" feature.properties['relation one'][0].z + ',' + feature.properties['relation one'][1].z;}"
"</script></body>" ) );

mComposition->setAtlasMode( QgsComposition::ExportAtlas );
QVERIFY( mComposition->atlasComposition().beginRender() );
QVERIFY( mComposition->atlasComposition().prepareForFeature( 0 ) );

htmlItem->loadHtml();

QgsCompositionChecker checker( "composerhtml_setfeature", mComposition );
checker.setControlPathPrefix( "composer_html" );
bool result = checker.testComposition( mReport );
mComposition->removeMultiFrame( htmlItem );
delete htmlItem;
QVERIFY( result );

QgsMapLayerRegistry::instance()->removeMapLayers( QList<QgsMapLayer *>() << childLayer << parentLayer );
}


QTEST_MAIN( TestQgsComposerHtml )
#include "testqgscomposerhtml.moc"
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 794ab06

Please sign in to comment.