From 794ab065dc7ada524ee9b284f90ea70c8e4c75c8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 9 May 2016 12:36:08 +1000 Subject: [PATCH] [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 --- src/core/composer/qgscomposerhtml.cpp | 14 +++ src/core/composer/qgscomposerhtml.h | 3 + tests/src/core/testqgscomposerhtml.cpp | 80 ++++++++++++++++++ .../expected_composerhtml_setfeature.png | Bin 0 -> 8225 bytes .../expected_composerhtml_setfeature_mask.png | Bin 0 -> 6730 bytes .../expected_composerhtml_setfeature.png | Bin 0 -> 10274 bytes 6 files changed, 97 insertions(+) create mode 100644 tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/default/expected_composerhtml_setfeature.png create mode 100644 tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/default/expected_composerhtml_setfeature_mask.png create mode 100644 tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/travis/expected_composerhtml_setfeature.png diff --git a/src/core/composer/qgscomposerhtml.cpp b/src/core/composer/qgscomposerhtml.cpp index 2d4fa9c1750..6e6ba6fba9a 100644 --- a/src/core/composer/qgscomposerhtml.cpp +++ b/src/core/composer/qgscomposerhtml.cpp @@ -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() diff --git a/src/core/composer/qgscomposerhtml.h b/src/core/composer/qgscomposerhtml.h index ca79341715c..201668f13d2 100644 --- a/src/core/composer/qgscomposerhtml.h +++ b/src/core/composer/qgscomposerhtml.h @@ -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 diff --git a/tests/src/core/testqgscomposerhtml.cpp b/tests/src/core/testqgscomposerhtml.cpp index 9578e37c355..31104930129 100644 --- a/tests/src/core/testqgscomposerhtml.cpp +++ b/tests/src/core/testqgscomposerhtml.cpp @@ -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 #include @@ -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() << 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( "" + "
" ) ); + + 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() << childLayer << parentLayer ); +} + QTEST_MAIN( TestQgsComposerHtml ) #include "testqgscomposerhtml.moc" diff --git a/tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/default/expected_composerhtml_setfeature.png b/tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/default/expected_composerhtml_setfeature.png new file mode 100644 index 0000000000000000000000000000000000000000..1f62882e2fde533d91e0f6261d1aac45b11018cb GIT binary patch literal 8225 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%0D~{NpKdZ8ej@zZ{=o(J46Rf3tL+-^dG71 zc?WMxbPbz@v|?Hl0%0sjEL=l9;$NifzF_8;#Puoru!cbxqpWK4Y^gdSn7Smh4~jvM zIe=bL)2AjUkAwK;wQ~T19ckM6<5Q7q!xcb^JuW~pN&KJlw`qEG>^F)C8BqTu;#SmW zf(Uw=odg#>HbrzRYSy^r^u{NTS1hCJ*EVzsOvY{*o9a>6p~d8+xtgn}sTqP#BGcXB zYTdaOl#oyeqlUtj?s81|zQK}ft+2oFr}7!V;C6+MP>jOj;@>%2)#7Aysk1rh@Y?t` zvpum74Bl(7Tg+A=L~-GGka3~BogEz_fFGkHGrg+v5AdCG1l}0%UZtJ9H@$YX&nea7 zjX4fhAE-#|R!0{rV+}F6zSbP}SQAFH1a5chk3T>3`P|jj1tPi!m4%(P!iAQqOR+dMEgPoJVYH>lXgWReBhadQ!5xlLPsyPfssp<(;wMmsV%H!Aczk8o*|2X59R+ z7nNx=rJ{SENB|MKThlpZ1-7wMf4%^YC)w@@aO8=Lu(rWEb+HC#x-Bp+)q%5Q z+k6OGl~6n|K;Ru=+lFU;c@0Y<5P%}W8mzo>T<`7G_UZvnCHE6Lvkc*PF4PD<=zBHT zmGSZMEfV`CCnwRQ6@%FrgY|&h>^OrS0V6j9Iis;;FW3@0ILgB{!J?|~Q)Ao8f%6*z!(>nn6JGc^qe2!M#T)F3Zfd~A*7_ia}8#A1Xi z^F=j&i=*#?A`c=+_?j9u?J*e+0USUXrlS5;FW_opE1ZTM^tlQapwLc!4L+eS<>(bF z;vO`zcoSxG0JMV8@XHe);$X(1p+=!mu>H9uwAW1_3K?G@0^Ry%6J#U1qZ*!FFTs8G zjs4b}Y{0I?wLJc9x4Los{!kHrnl{FV6|{?9k0xpenqxjFfk}vrVVIwUpb6K`4+;Hz zM%oZOX;6p}xTqM()$`Thh?3^@wY+++LlEmduFI_{^rusI=O1>4Rz`_9BvqHK`w>KP zb4-{L8vN927yoc|9ogV>(4}p)QKo)Ua`Ng*x5XN>p93r#1`$(JzJ5DQuVj>jGFl?w zKHBQVo;o-{jv@1VxR`m&dM~;j*ZNnR@qc59{`IL}EAX`fUn}sn0$(fewF3Wv6_{|? z5r-gKhtbah{BQnh{vUY6ugCgYfv*+#T7j<>`2SV`E%JPPN#5$eQ-_(x<>?U5}_ z8QHIr10R`!Pr+0hZAWg^c^AVHx7ujy=tK0c`#r0_AxO_TyU@^37t$sKd8>O@5wiBb z{{gc98&df6