Slow Graphic Updates on Map

2396
11
04-03-2021 06:35 AM
TimothyReed1
New Contributor II

I have been working on a JavaFX application using the ArcGIS Java runtime and I am having issues with graphics updating on the map.  The premise is we are trying to track an aircraft as it flys, so we get location updates 1 per second.  When this update is received I search through the graphics on the graphics layer to find the corresponding graphics.  I have three graphics per aircraft (aircraft icon, text, and a tail).  Each graphic is updated during each location update.  With one aircraft the update is very jittery, it appears that the map updates every 2-3 seconds as I get a stepping motion in the graphic (see attachment) as it moves as opposed to a smooth motion.  If I turn on the tails then the lag is even higher and there are times when the poly line does not fully display.  The more aircraft that are being tracked the worse the display gets.  Currently I am not doing the graphic updates in a separate thread (Platform.runLater) as I found that did not improve performance.  The graphics layer is set to dynamic rendering mode, and the map is in 3D. Currently the aircraft graphics are 2D.  Removing the display of the tail (blue line) does not improve the performance.

The flight plan that was being flown is below in the screen shots to show that the aircraft was flying a straight line and not a step.

I can post code if needed.  Thoughts?

Thanks,

Tim

 

 

0 Kudos
11 Replies
MarkBaird
Esri Regular Contributor

Hi Tim,

I was keeping an eye open for your post as Kerry mentioned you had a question.

With the number of graphics you are working with, dynamic rendering is fine.

The display you show in the video above doesn't look very good and I certainly think there is lots of scope to improve this.  To give you some hope, I have tests which allow 20,000 graphics all being updated every 20ms so I'm sure we can make this better.

I'm trying to imagine the use-case here and can picture your app as receiving some feed of regular updates about your aircraft location.  If these updates are coming in on a separate thread, you are fine with updating graphics away from the UI thread; this is quite safe.  Usually the workflow I follow for this kind of rapidly updating app is:

 - Create an initial geometry for your aircraft location (a point for example)

 - Create a symbol

 - create a graphic passing in the symbol and the geometry

 - Then in a loop:

   -> read in the new location and create a new geometry (point).  Remember Points and other geometries are immutable.

   -> apply the new point to the graphic and it will update the display

If you wanted to share some code I'd be happy to make suggestions on how to improve it.

Let me know if this helps.

Mark

 

0 Kudos
TimothyReed1
New Contributor II

Thanks Mark!  I've attached a very high level architecture diagram of how the data is obtained, processed and sent to the map.  Also I have attached snippets from how the updateAircraftOnMap function works.  The first screen shot is where I search for the Graphic and Text, if they dont exist I get a null which then leads me to create new ones and add them to the graphics layer.  The second screen shot is how I create the "tail" using the PolyLine.  If you see anything that can be improved please let me know.  There is some code missing that relates to show/hide the graphics and calculation of the length of the tail.

0 Kudos
MarkBaird
Esri Regular Contributor

Tim,

I've had a quick look at your code and although I've not tried it, I do wonder if your method for looking for an existing graphic is going to work very quickly and if it will scale to lots of graphics.  I can see how your use of the streams works, but I do think a quicker way to get to your graphics would help.

Your architecture diagram makes sense and I've seen plenty of implementations like this.  In my experience, the UDP feed with be a stream of messages giving updates for items where each item has a unique identifier.  This unique identifier looks like ICAO in your implementation.

So your workflow would be something like:

 - An update comes in for item XYZ

 - Check to see if there is a graphic for XYZ.  If there is update it, if not add a new one.

So in your app you need a super efficient way of getting the graphic associated with XYZ.

Searching in the graphics then in the attributes isn't going to be quick if you've got lots of graphics, so I would use a HashMap like this:

private HashMap<String, Graphic> aircraftList;

The key of this can be used for your unique reference and its very fast to look this up and get the graphic.  I've thrown together a crude app to show how this might work.  The app gradually adds up to 10000 graphics and updates all the graphics every 20ms by getting hold of the graphic via the HashMap.

package com.esri.samples.display_map;

import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.mapping.Basemap;
import com.esri.arcgisruntime.mapping.view.Graphic;
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.view.MapView;
import java.util.*;

public class GraphicUpdating extends Application {

    private MapView mapView;
    private GraphicsOverlay graphicsOverlay;
    private SimpleMarkerSymbol markerSymbol;
    private HashMap<String, Graphic> aircraftList;
    private Timer timer;

    @Override
    public void start(Stage stage) {

        try {

            // create stack pane and application scene

            BorderPane stackPane = new BorderPane();
            Scene scene = new Scene(stackPane);

            // set title, size, and add scene to stage
            stage.setTitle("Graphic updating sample");
            stage.setWidth(800);
            stage.setHeight(700);
            stage.setScene(scene);
            stage.show();

            // authentication with an API key or named user is required to access basemaps and other location services
            String yourAPIKey = System.getProperty("apiKey");
            //ArcGISRuntimeEnvironment.setApiKey(yourAPIKey);

            // create a map with the standard imagery basemap style
            ArcGISMap map = new ArcGISMap(Basemap.createOpenStreetMap());

            // create a map view and set the map to it
            mapView = new MapView();
            mapView.setMap(map);


            HBox hBox = new HBox();
            stackPane.setTop(hBox);

            stackPane.setCenter(mapView);

            //symbols to be used
            markerSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, 0xFF00FF00, 10);

            //graphics overlay
            graphicsOverlay = new GraphicsOverlay();
            mapView.getGraphicsOverlays().add(graphicsOverlay);

            //start timer for adding and moving graphics
            StartGraphicController(10000);

        } catch (Exception e) {
            // on any error, display the stack trace.
            e.printStackTrace();
        }
    }

    private class Updater extends TimerTask {
        private int maxGraphics;
        private int numGraphics = 0;
        private int updates = 0;
        private Random random = new Random();

        @Override
        public void run() {
            // add a new graphic after every 5 updates
            if (updates++ == 4) {
                updates = 0;

                //check we've not reached the max
                if (numGraphics < maxGraphics) {
                    //System.out.println("adding new");
                    numGraphics++;

                    // create a new graphic
                    Point pt = new Point(random.nextDouble() * 5000000, random.nextDouble() * 5000000);
                    Graphic graphic = new Graphic(pt, markerSymbol);
                    graphicsOverlay.getGraphics().add(graphic);
                    UUID guid = UUID.randomUUID();

                    // add it to the hashmap
                    aircraftList.put(guid.toString(), graphic);
                }
            }

            // loop through all aircraft and update them
            for (String id : aircraftList.keySet()) {
                MoveAircraft(id);
            }
        }

        //constructor
        public Updater(int maxGraphics) {
            this.maxGraphics = maxGraphics;
            System.out.println("constructor");
        }
    }

    private void MoveAircraft(String id) {
        //get graphic from the id
        Graphic graphic = aircraftList.get(id);

        // read current position
        Point pos = (Point) graphic.getGeometry();

        // new graphic
        Point newPos = new Point(pos.getX() + 1000, pos.getY());
        graphic.setGeometry(newPos);
    }

    private void StartGraphicController(int maxGraphics) {
        aircraftList = new HashMap<>();

        timer = new Timer();
        Updater updater = new Updater(maxGraphics);

        timer.schedule(updater,1000,20);
    }

    /**
     * Stops and releases all resources used in application.
     */
    @Override
    public void stop() {

        if (mapView != null) {
            mapView.dispose();
        }
        timer.cancel();
    }

    /**
     * Opens and runs application.
     *
     * @param args arguments passed to this application
     */
    public static void main(String[] args) {
        Application.launch(args);
    }
}

It's not a great bit of code, but you'll see its very fast to update and the UI remains responsive to panning and zooming whilst its updating up to 10000 graphics every 20ms.

0 Kudos
TimothyReed1
New Contributor II

Its funny you suggested that as I had just taken that code out and replaced it with the search in the graphic layer instead.  I was afraid the storing them in the HashMap was what was slowing me down.  I will not be tracking probably more than 30 aircraft at any one time (90 graphics).  But I can certainly roll the HashMap code back in.  

My concern is that even though the UDP listener is on its own thread, is the observer (MainController) on a different thread than the main when its notified of the update which in turn would put the map update on a thread.  I have seen some examples that show that the architecture that I have SHOULD be threaded but I cannot prove that just yet.  I have had debug statements in the code all the way to the update aircraft on map function and they show an update once a second but the map is still slow.  

Thoughts?

Thanks!

0 Kudos
MarkBaird
Esri Regular Contributor

If you run my app, it should give you confidence the rendering pipeline is very capable and with your 90 graphics every second update frequency it shouldn't be stressing anything.  Try running my app to be sure.

In my test app, the timer will be called on its own thread.  As soon as the graphic is updated, our internal rendering pipeline will trigger a new image to be generated which is reflected on the MapView.  You don't need to do anything like Platform.runlater when updating graphics; this will slow things down.  It is designed for rapidly updating graphics so you can trigger an update as soon as you have the data.

I'm wondering if you can decouple parts of your app to work out where the lag is caused.  If you can swap parts out with code to simulate updates it might help narrow it down.

I'd also have a look to see you don't have lots of activity on the UI thread of your JavaFX app.  Any intensive data processing or code which causes delays due to network latency will make the UI of your app poor to respond.

0 Kudos
TimothyReed1
New Contributor II

Ran some performance metrics this morning and cleaned up some more code.  I switched back to using the HashMap as you suggested.  Attached are two screen shots that show the performance differences between search the GraphicsLayer and searching the HashMap.  I was always putting the graphics back into the Hash Map after modification but there really isn't any reason to do that, so I pulled that out.

One question I do have is I am using an ArcGISScene and you are using an ArcGISMap.  Is there a big difference between the two.  If I remember correctly I chose ArcGISScene for its 3D capabilities.

Thanks!

0 Kudos
MarkBaird
Esri Regular Contributor

Hi Tim,

Are you seeing any improvement in the speed of updates on the scene yet? 

In terms of differences between SceneView and MapView, I'd expect the scenes to be more demanding on the GPU and I'd not expect you to be able to push it as hard as a scene view in terms of the numbers of graphics.  However I'd still expect it to cope with 1000s of graphics updating many times a second.  Your requirement of 90 graphics a second is very achievable.

As you say SceneViews are there for 3D capabilities and depending on how you render your graphics (ideally in dynamic mode) you will be able to see them rendered in the air with your Z value.  This sample explains surface placement:

https://github.com/Esri/arcgis-runtime-samples-java/tree/master/scene/surface-placement

As an experiment I crudely swapped out my MapView for a SceneView and although I had to do a little more work with specifying the spatial reference it still performs quite well.

import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.SpatialReferences;
import com.esri.arcgisruntime.mapping.ArcGISScene;
import com.esri.arcgisruntime.mapping.Basemap;
import com.esri.arcgisruntime.mapping.view.Graphic;
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
import com.esri.arcgisruntime.mapping.view.SceneView;
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.view.MapView;
import java.util.*;

public class GraphicUpdating extends Application {

    private SceneView mapView;
    private GraphicsOverlay graphicsOverlay;
    private SimpleMarkerSymbol markerSymbol;
    private HashMap<String, Graphic> aircraftList;
    private Timer timer;

    @Override
    public void start(Stage stage) {

        try {

            // create stack pane and application scene

            BorderPane stackPane = new BorderPane();
            Scene scene = new Scene(stackPane);

            // set title, size, and add scene to stage
            stage.setTitle("Graphic updating sample");
            stage.setWidth(800);
            stage.setHeight(700);
            stage.setScene(scene);
            stage.show();

            // authentication with an API key or named user is required to access basemaps and other location services
            String yourAPIKey = System.getProperty("apiKey");
            //ArcGISRuntimeEnvironment.setApiKey(yourAPIKey);

            // create a map with the standard imagery basemap style
            //ArcGISMap map = new ArcGISMap(Basemap.createOpenStreetMap());
            ArcGISScene map = new ArcGISScene(Basemap.createStreets());

            // create a map view and set the map to it
            mapView = new SceneView();
            mapView.setArcGISScene(map);


            HBox hBox = new HBox();
            stackPane.setTop(hBox);

            stackPane.setCenter(mapView);

            //symbols to be used
            markerSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, 0xFF00FF00, 10);

            //graphics overlay
            graphicsOverlay = new GraphicsOverlay();
            mapView.getGraphicsOverlays().add(graphicsOverlay);

            //start timer for adding and moving graphics
            StartGraphicController(10000);

        } catch (Exception e) {
            // on any error, display the stack trace.
            e.printStackTrace();
        }
    }

    private class Updater extends TimerTask {
        private int maxGraphics;
        private int numGraphics = 0;
        private int updates = 0;
        private Random random = new Random();

        @Override
        public void run() {
            // add a new graphic after every 5 updates
            if (updates++ == 4) {
                updates = 0;

                //check we've not reached the max
                if (numGraphics < maxGraphics) {
                    //System.out.println("adding new");
                    numGraphics++;

                    // create a new graphic
                    Point pt = new Point(random.nextDouble() * 5000000, random.nextDouble() * 5000000, SpatialReferences.getWebMercator());
                    Graphic graphic = new Graphic(pt, markerSymbol);
                    graphicsOverlay.getGraphics().add(graphic);
                    UUID guid = UUID.randomUUID();

                    // add it to the hashmap
                    aircraftList.put(guid.toString(), graphic);
                }
            }

            // loop through all aircraft and update them
            for (String id : aircraftList.keySet()) {
                MoveAircraft(id);
            }
        }

        //constructor
        public Updater(int maxGraphics) {
            this.maxGraphics = maxGraphics;
            System.out.println("constructor");
        }
    }

    private void MoveAircraft(String id) {
        //get graphic from the id
        Graphic graphic = aircraftList.get(id);

        // read current position
        Point pos = (Point) graphic.getGeometry();

        // new graphic
        Point newPos = new Point(pos.getX() + 1000, pos.getY(), SpatialReferences.getWebMercator());
        graphic.setGeometry(newPos);
    }

    private void StartGraphicController(int maxGraphics) {
        aircraftList = new HashMap<>();

        timer = new Timer();
        Updater updater = new Updater(maxGraphics);

        timer.schedule(updater,1000,20);
    }

    /**
     * Stops and releases all resources used in application.
     */
    @Override
    public void stop() {

        if (mapView != null) {
            mapView.dispose();
        }
        timer.cancel();
    }

    /**
     * Opens and runs application.
     *
     * @param args arguments passed to this application
     */
    public static void main(String[] args) {

        Application.launch(args);
    }
}
0 Kudos
TimothyReed1
New Contributor II

Yes I have made great improvements in performance.  I ripped everything out of a copy of the project and just have the mavlink parser in there to test against.  Makes it easier to test small changes like the HashMap or using the stream to find the graphic.  

I'd like to stop using the database to query for position history data and just use the polyline.  Can I easily obtain the existing points that make up the polyline?   I know how to manipulate it with new locations after the fact, I just cannot see how to get the Point data out of it.  Or is this another case where a HashMap<String, List<Point>> would be faster.

0 Kudos
MarkBaird
Esri Regular Contributor

Tim,

If you've got the geometry class for your graphic its easy to extract the points.  I modified part of a sample to show constructing a line then showing code to get the points:

  /**
   * Creates a Polyline and adds it to a GraphicsOverlay.
   */
  private void createPolyline() {

    // create a purple (0xFF800080) simple line symbol
    SimpleLineSymbol lineSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.DASH, 0xFF800080, 4);

    // create a new point collection for polyline
    PointCollection points = new PointCollection(SPATIAL_REFERENCE);

    // create and add points to the point collection
    points.add(new Point(-2.715, 56.061));
    points.add(new Point(-2.6438, 56.079));
    points.add(new Point(-2.638, 56.079));
    points.add(new Point(-2.636, 56.078));
    points.add(new Point(-2.636, 56.077));
    points.add(new Point(-2.637, 56.076));
    points.add(new Point(-2.715, 56.061));

    // create the polyline from the point collection
    Polyline polyline = new Polyline(points);

    // create the graphic with polyline and symbol
    Graphic graphic = new Graphic(polyline, lineSymbol);

    // add graphic to the graphics overlay
    graphicsOverlay.getGraphics().add(graphic);

    // get the parts (if its simple there will be 1)
    ImmutablePartCollection partCollection = polyline.getParts();
    for (ImmutablePart part : partCollection) {
      // loop through the points which make up the part 
      for (Point pt : part.getPoints()) {
        System.out.println("x=" + pt.getX() + " y=" + pt.getY());
      }
    }
  }

 

You can't change a geometry as they are immutable, so you will need to create a new one each time you want to move it.

Glad you've got some good performance improvements.

0 Kudos