Re-render on graphic attribute change

357
7
Jump to solution
10-03-2017 03:53 PM
RainerFarsch1
New Contributor II

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

0 Kudos
1 Solution

Accepted Solutions
LucasDanzinger
Esri Frequent Contributor

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);
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View solution in original post

0 Kudos
7 Replies
LukeSmallwood
Esri Contributor

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

0 Kudos
RainerFarsch1
New Contributor II

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

0 Kudos
LukeSmallwood
Esri Contributor

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

0 Kudos
RainerFarsch1
New Contributor II

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);
   }
0 Kudos
RainerFarsch1
New Contributor II

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

0 Kudos
LucasDanzinger
Esri Frequent Contributor

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);
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

View solution in original post

0 Kudos
RainerFarsch1
New Contributor II

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