I have a class break renderer installed onto a graphic overlay. I've also pre-allocated and appended graphics to this layer. One of the attributes is used as the field name to break on for the class break renderer. This attribute is initialized to a certain number and the renderer does exactly as I expect it to do when I append the graphics to the graphics overlay. However, when I change the attribute (that is also the field name) of an existing graphic to another value using
graphicAttrList->replaceAttribute()
my expectations are that the renderer would re-evaluate the graphic and draw the correct symbol according to the change in the graphic's attribute value. However, I don't see that happening. Is there a way to get the renderer to re-render the graphics based on changes to the graphics' attribute. This worked in 10.2.6; so either the capability is lo longer avaialble, or I'm not doing something correctly (perhaps replaceAttribute() is the wrong call to make?). Thanks for any help!
-Rainer
Solved! Go to Solution.
Rainer-
Does the following code work for you? It updates on a timer, and uses AttributeListModel::replaceAttribute, without setting the a whole new attribute map back to the list model
#include "Map.h"
#include "MapQuickView.h"
#include "Basemap.h"
#include "GraphicsOverlay.h"
#include "SimpleMarkerSymbol.h"
#include "ClassBreaksRenderer.h"
#include "Graphic.h"
#include "Point.h"
#include "RainerTest.h"
#include <cstdlib>
#include <QTimer>
using namespace Esri::ArcGISRuntime;
RainerTest::RainerTest(QQuickItem* parent /* = nullptr */):
QQuickItem(parent)
{
}
RainerTest::~ RainerTest()
{
}
void RainerTest::componentComplete()
{
QQuickItem::componentComplete();
// find QML MapView component
m_mapView = findChild<MapQuickView*>("mapView");
m_mapView->setWrapAroundMode(WrapAroundMode::Disabled);
// Create a map using the topographic BaseMap
m_map = new Map(Basemap::topographic(this), this);
GraphicsOverlay* go = new GraphicsOverlay(this);
m_mapView->graphicsOverlays()->append(go);
// Create the class breaks for the different sized cities
ClassBreak* classBreak1 = new ClassBreak("Small cities", "Cities with less than 50,000 people", 0, 2, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Circle, QColor(Qt::gray), 30, this), this);
ClassBreak* classBreak2 = new ClassBreak("Moderate cities", "Cities with between 50,000 and 100,000 people", 2.01, 5, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Cross, QColor(Qt::green), 30, this), this);
ClassBreak* classBreak3 = new ClassBreak("Large cities", "Cities with between 100,000 and 500,000 people", 5.01, 7, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Triangle, QColor(Qt::blue), 30, this), this);
ClassBreak* classBreak4 = new ClassBreak("Very large cities", "Cities with more than 500,000 people", 7.01, 10, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Square, QColor(Qt::red), 30, this), this);
// Add the class breaks to the renderer
ClassBreaksRenderer* classBreaksRenderer = new ClassBreaksRenderer("Population", QList<ClassBreak*>() << classBreak1 << classBreak2 << classBreak3 << classBreak4, this);
go->setRenderer(classBreaksRenderer);
// Create a graphic with an intial attribute value
QVariantMap attr;
attr["Population"] = 3;
Point pt(0,0);
m_graphic = new Graphic(pt, attr, this);
go->graphics()->append(m_graphic);
// Set map to map view
m_mapView->setMap(m_map);
// Timer to update graphic
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [this]()
{
int randVal = rand()%(10-1 + 1) + 1;
m_graphic->attributes()->replaceAttribute("Population", randVal);
});
timer->start(50);
}
Hi Rainer,
One thing you should check is that you are re-applying the attributes (a QVariantMap) to your Graphic after updating. The call to Graphic::attributes() returns by value and not by reference so if you do something like:
auto graphicAttrList = myGraphic->attributes();
graphicAttrList->replaceAttribute(); you will need to make sure you follow that with a call to:
myGraphic->setAttributes(graphicAttrList);
I hope that helps,
Luke
Hi Luke,
Thanks for the information, but I don't see a member function called setAttributes() in the Graphic class or in any of its base classes (List of All Members for Graphic | ArcGIS for Developers). I see two getters (from GeoElement base class and Graphic class), but I don't see a setAttributes() setter. Would you please clarify?
Thanks!
-Rainer
Sorry, Rainer that was my mistake - I've just looked at the API and what you have is correct. If you're able to post a little more of your code I can try and reproduce.
Luke
Luke,
Here's a redacted snippet of code. I changed variable names and functions to more generic names. So in this snippet of code I'm re-using pre-allocated graphics and giving them new attributes and geometry. As written, the renderer does change the symbol accordingly - for example on the value of the zzzz attribute. Near the bottom of the code snippet I'm setting new attributes to the graphic in a manner that seems a bit long-winded. In that one line I'm getting the QVariantMap from graphicAttrList and then setting it back to the graphic's attributes. Is there a better (faster) way? Setting the graphic's geometry seems a lot more straight forward.
Thanks!
-Rainer
// Get the graphic list model.
GraphicListModel* graphicListModel = m_graphicsOverlay->graphics();
// Transfer over data from the model to graphic. Do this by changing the attributes // and the geometry for each graphic.
unsigned int offset = ...; // some computed no.
unsigned int i = 0;
SpatialReference sr(102100);
for (Datum* datum : dataVector)
{
// For each datum from the model, transfer its attributes to a graphic from
// the graphics overlay.
AttributeListModel* graphicAttrList = graphicListModel->at(i)->attributes();
// Replace attribute data with data from the model.
graphicAttrList->replaceAttribute("wwww", datum->wwww());
graphicAttrList->replaceAttribute("xxxx", datum->xxxx());
graphicAttrList->replaceAttribute("yyyy", datum->yyyy());
graphicAttrList->replaceAttribute("zzzz", datum->zzzz());
// Get the graphic.
Graphic* graphic = graphicListModel->at(offset + i++);
// Set the new attributes to the graphic.
graphic->attributes()->setAttributesMap(graphicAttrList->attributesMap()); // <- Luke, this is what I'm using - is there a better way?
// Change the geometry of the graphic to that of the echo.
Point geom(datum->x(), datum->y(), sr);
graphic->setGeometry(geom); }
Well, it's been a few days since my last posting - no reply . In the code I presented, I'm updating the attributes of 128 graphics every 50 ms. From the testing I've done, changing the attributes of each graphic carries a performance hit. However, I don't need to change all the attributes since I have my own model from which to pull data if and when a graphic is selected (hooked) by the user. However two attributes are needed: one for the class break renderer to use as its field name, and a unique identifier (UID) attribute that matches the model's UID.
So my question: Is there a faster way to update attributes? Currently the code looks like:
// Replace attribute data with data from the model.
graphicAttrList->replaceAttribute("wwww", datum->wwww());
graphicAttrList->replaceAttribute("xxxx", datum->xxxx());
graphicAttrList->replaceAttribute("yyyy", datum->yyyy()); // field name for class break renderer:
graphicAttrList->replaceAttribute("zzzz", datum->zzzz()); // Is there quicker way to do this? In the code below, I have to get the QVariantMap // from the model (graphicAttrList->attributesMap()) in order to set the attributes // for the graphic so that the class break renderer renders the correct symbol using // the 'zzzz' attribute as its field name. Ideally, I would like to replace the // attribute -- as in the code above -- and be done with it. In QML-land (10.2.6), // that is what I was able to do: change the attribute and then the renderer took care // of rendering the correct symbol. It appears in C++-land (100.1), I need to do the // extra step below, unless I'm missing something...?graphic->attributes()->setAttributesMap(graphicAttrList->attributesMap());
Thanks!!!
-Rainer
Rainer-
Does the following code work for you? It updates on a timer, and uses AttributeListModel::replaceAttribute, without setting the a whole new attribute map back to the list model
#include "Map.h"
#include "MapQuickView.h"
#include "Basemap.h"
#include "GraphicsOverlay.h"
#include "SimpleMarkerSymbol.h"
#include "ClassBreaksRenderer.h"
#include "Graphic.h"
#include "Point.h"
#include "RainerTest.h"
#include <cstdlib>
#include <QTimer>
using namespace Esri::ArcGISRuntime;
RainerTest::RainerTest(QQuickItem* parent /* = nullptr */):
QQuickItem(parent)
{
}
RainerTest::~ RainerTest()
{
}
void RainerTest::componentComplete()
{
QQuickItem::componentComplete();
// find QML MapView component
m_mapView = findChild<MapQuickView*>("mapView");
m_mapView->setWrapAroundMode(WrapAroundMode::Disabled);
// Create a map using the topographic BaseMap
m_map = new Map(Basemap::topographic(this), this);
GraphicsOverlay* go = new GraphicsOverlay(this);
m_mapView->graphicsOverlays()->append(go);
// Create the class breaks for the different sized cities
ClassBreak* classBreak1 = new ClassBreak("Small cities", "Cities with less than 50,000 people", 0, 2, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Circle, QColor(Qt::gray), 30, this), this);
ClassBreak* classBreak2 = new ClassBreak("Moderate cities", "Cities with between 50,000 and 100,000 people", 2.01, 5, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Cross, QColor(Qt::green), 30, this), this);
ClassBreak* classBreak3 = new ClassBreak("Large cities", "Cities with between 100,000 and 500,000 people", 5.01, 7, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Triangle, QColor(Qt::blue), 30, this), this);
ClassBreak* classBreak4 = new ClassBreak("Very large cities", "Cities with more than 500,000 people", 7.01, 10, new SimpleMarkerSymbol(SimpleMarkerSymbolStyle::Square, QColor(Qt::red), 30, this), this);
// Add the class breaks to the renderer
ClassBreaksRenderer* classBreaksRenderer = new ClassBreaksRenderer("Population", QList<ClassBreak*>() << classBreak1 << classBreak2 << classBreak3 << classBreak4, this);
go->setRenderer(classBreaksRenderer);
// Create a graphic with an intial attribute value
QVariantMap attr;
attr["Population"] = 3;
Point pt(0,0);
m_graphic = new Graphic(pt, attr, this);
go->graphics()->append(m_graphic);
// Set map to map view
m_mapView->setMap(m_map);
// Timer to update graphic
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [this]()
{
int randVal = rand()%(10-1 + 1) + 1;
m_graphic->attributes()->replaceAttribute("Population", randVal);
});
timer->start(50);
}
Lucas,
Yes, that test code works. I did make two minor changes shown in bold below for it to compile:
(1) connect(timer, &QTimer::timeout, this, [=]()
(2) Graphic* m_graphic = new Graphic(pt, attr, this);
So, I went back to my code and commented out:
graphic->attributes()->setAttributesMap(graphicAttrList->attributesMap());
and it is working without this line - don't know what's changed - but it's working. I'm going to do a bit more testing to see how well and fast it is working. This is good news...
Thanks.
-Rainer