4

I'm developing an app to draw a movement path from given GPS location from a text file. So far, i'm successfully be able to load the data and draw the path. My solution is, the path must be drawn to a rectangle because it contain data (e.g color and width) and clickable. A Line cannot do this. Furthermore, there are also a triangle gap between these rectangles when it took a turn, so i filled it with triangle shapes. And the path have to be double because i have another data to be displayed on that second layer. This means, lots of polygons to be drawn for a longer path.

I have tried this on 15 km journey, and it takes 1000 lines of gps data. When i parse the file, it draws total up to 5000 polygons for both layers.

My problem is, when it draws the shapes up to 1000 polygons, the app become laggy, not responding. If i let the thread sleeps for 1 seconds, it seems ok. but more speed, it became not responding.

I been reading the web for this solution and could find it. Fyi, i already create another thread to handle the text file. I also narrow down the problem by letting the app do the process without drawing down the polygons, and the process were smooth. I have read that there is no other way to handle the polygons outside the main thread.

UPDATED: I'm using Asynctask background to read a line from textfile, parse it into array which contains Latitude, Longitude, Value1, Value2. Then massive calculation happen there. Upon completionfor each row, i sent objects to onProgressUpdate to update UI thread with markers, polylines, and shapes.

Here is my Asynctask

private class DrawPathAsync extends AsyncTask<File, Object, Void>
{

    FileInputStream is;
    BufferedReader reader;



    @Override
    protected Void doInBackground(File... params) {
        File sFile = params[0];
        Integer count;
        String line = "";
        double radius = 8; //8 meter
        double distance;

        double Heading_y;
        int kaler, gkaler;
        double apprate, gi;

        if (sFile.exists()) {
            try {
                is = new FileInputStream(sFile);
                reader = new BufferedReader(new InputStreamReader(is));
                reader.readLine(); // this will read the first line


                while ((line = reader.readLine()) != null) {

                    String[] valuesArray = line.split("\\s*,\\s*");
                    Float bearing = (float) 0;

                    Double lat = Double.parseDouble(valuesArray[1]);
                    Double lng = Double.parseDouble(valuesArray[2]);

                    LatLng latlng = new LatLng(lat, lng);
                    LatLng center = latlng;
                    apprate = Double.parseDouble(valuesArray[3]); 

                    if (apprate >=0 && apprate < 80) {
                        kaler = appcolor1; 
                    } else if (apprate >=80 && apprate < 100) { 
                        kaler = appcolor2;
                    } else if (apprate >=100 && apprate < 120) {  
                        kaler = appcolor3;
                    } else if (apprate >=120 && apprate < 140) {  
                        kaler = appcolor4;
                    } else if (apprate >=140 && apprate < 160) {  
                        kaler = appcolor5;
                    } else if (apprate >=160 && apprate <= 200) {  
                        kaler = appcolor6;
                    } else {
                        kaler = appcolor7; 
                    }


                    if (points.size()== 2) {
                        points.remove(0);
                        points.add(latlng);
                    } else {
                        points.add(latlng);
                    }


                    //recheck
                    if (points.size() == 2) {

                        distance = SphericalUtil.computeDistanceBetween(center, points.get(0));

                        LatLng pt1 = points.get(0);
                        LatLng pt2 = latlng;

                        bearing = (float) SphericalUtil.computeHeading(pt1, pt2);
                        if (bearing < 0) {
                            bearing = bearing + 360;
                        }

                        LatLng x = SphericalUtil.computeOffset(center, radius, bearing - 90);
                        LatLng y = SphericalUtil.computeOffset(center, radius, bearing + 90);


                        LatLng a = SphericalUtil.computeOffset(x, distance, bearing + 180);
                        LatLng b = SphericalUtil.computeOffset(y, distance, bearing + 180);


                        MarkerPoint mp = new MarkerPoint();
                        mp.latlng = latlng;
                        mp.bearing = bearing;

                        Rect rc = new Rect();
                        rc.a = a;
                        rc.b = b;
                        rc.x = x;
                        rc.y = y;
                        rc.kaler = kaler;
                        rc.pt2 = pt2;

                        publishProgress(mp, rc);
                    }

                    Thread.sleep(50);

                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }

        return null;
    }

    @Override
    protected void onProgressUpdate(Object... values) {

        MarkerPoint mp = (MarkerPoint) values[0];
        Rect rc = (Rect) values[1];



        LatLng latlng = mp.latlng;

        BitmapDescriptor icon = BitmapDescriptorFactory.fromResource(R.mipmap.pointer);
        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(latlng);
        markerOptions.icon(icon);
        markerOptions.rotation(mp.bearing);

        mMap.moveCamera(CameraUpdateFactory.newLatLng(latlng));
        marker1.remove();
        marker1 = mMap.addMarker(markerOptions);

        if (points.size() > 1) {
            path = mMap.addPolyline(new PolylineOptions().add(points.get(0)).add(points.get(1)).color(Color.BLUE).width(5));
            lines.add(path);
        }

        PolygonOptions options = new PolygonOptions()
                .fillColor(rc.kaler)
                .strokeWidth(0)
                .strokeColor(Color.TRANSPARENT);
        options.add(rc.x);
        options.add(rc.y);
        options.add(rc.b);
        options.add(rc.a);

        rect = mMap.addPolygon(options);
        rects.add(rect);

        if (tripoints.size() == 3) {
            tripoints.add(rc.a);
            tripoints.add(rc.b);
        } else {



            tripoints.add(rc.pt2);
            tripoints.add(rc.x);
            tripoints.add(rc.y);
        }

        //check
        //round 2, if triponts = 5 create triangle
        if (tripoints.size() == 5) {
            PolygonOptions options2 = new PolygonOptions()
                    .fillColor(rc.kaler)
                    .strokeWidth(0)
                    .strokeColor(Color.TRANSPARENT);

            options2.add(tripoints.get(0));
            options2.add(tripoints.get(2));
            options2.add(tripoints.get(4));


            t1 = mMap.addPolygon(options2);
            tris.add(t1);


            PolygonOptions options3 = new PolygonOptions()
                    .fillColor(rc.kaler)
                    .strokeWidth(0)
                    .strokeColor(Color.TRANSPARENT);

            options3.add(tripoints.get(0));
            options3.add(tripoints.get(1));
            options3.add(tripoints.get(3));



            t2 = mMap.addPolygon(options3);
            tris.add(t2);



            tripoints.clear();
            tripoints.add(rc.pt2);
            tripoints.add(rc.x);
            tripoints.add(rc.y);
        }


    }

    @Override
    protected void onPostExecute(Void result) {

    }
}

Hope someone can share some tips and solutions.

luca ditrimma
  • 765
  • 1
  • 7
  • 22

3 Answers3

3

I've been having the same issue for some time now and after an extensive research I have isolated the issue to the Google Maps SDK itself. A solution that worked in my case was to use a GroundOverlay and draw custom points/lines/polygons at the correct geo-coordinates. I found this library which basically does that:

https://github.com/antoniocarlon/richmaps

With a few tweaks I was able to create a render thread which takes the drawing/filtering part off the main UI thread and only updates the GroundOverlay when it's finished. In addition I added a simple but fast algorithm to search for which shapes are visible currently. With this approach you get a few bonuses:

  • Only objects within the current viewport are drawn (Google Maps SDK does this but not for shapes too close to eachother, e.g. in by case 600+)
  • Drawing is done in a thread so app startup is improved drastically
  • You can add further customisation options to the map shapes as drawing is done with Canvas
  • Map render time in my case was reduced to about 200ms for 600+ stapes.
Georgi
  • 430
  • 7
  • 21
  • Ground Overlays offers drastic performance improvement compared to plain simple Polygons. However, it's still laggy. Tile Overlays which are designed for extensive imagery makes tens of thousands of polygons work smoothly (even zooming in and out) like the Google Maps app itself – ericn Jun 21 '20 at 20:53
1

I know it is too late to answer this question, but I write my solution. I hope that be helpful. I created a 'PolygonRenderer' class that continuously update polygons on map. Also I created a 'PolygonWrapper' class that contain more information about polygon such as that polygon is added to map or not and some more functions like removeFromMap() and addToMap()

Actully I make polygons lazy load, and only polygons in viewport will be shown and other polygons will be removed. By changing map camera position, visible polygons will be change.

The PolygonRenderer class:

/**
 * Singleton class that handle polygons showing on map.
 * This class is singleton because one instance in enough and operation that this class do it is
 * UI Thread operation so must call in ui thread.
 * This class only show polygons that are in map viewport and handle shapes.
 *
 * @version 1.3
 */
public class PolygonRenderer implements Runnable {

    // Update interval in millisecond
    private final static long UPDATE_INTERVAL = 500;

    // Single instance of this class
    private static PolygonRenderer instance;

    private Thread thread;

    // Keep last update time in millisecond
    private long lastUpdate;

    // Used to stop thread
    private boolean stopFlag;

    // Used to pause thread
    private boolean pauseFlag;
    private final Object pauseLock;

    private LatLngBounds bounds;
    private float zoom;
    private List<PolygonWrapper> polygons;
    private GoogleMap map;

    private PolygonRenderer() {
        this.stopFlag = false;
        this.pauseFlag = false;
        this.pauseLock = new Object();
    }

    public static synchronized PolygonRenderer getInstance() {
        if (instance == null)
            instance = new PolygonRenderer();
        return instance;
    }

    /**
     * Stop polygons refreshing on map
     */
    public synchronized void stop() {
        stopFlag = true;
        if (thread != null) {
            thread.interrupt();
            thread = null;
        }
    }

    /**
     * Pause running thread
     */
    public synchronized void onPause() {
        pauseFlag = true;
    }

    /**
     * Resume thread running
     */
    public synchronized void onResume() {
        pauseFlag = false;
        pauseLock.notifyAll();
    }

    /**
     * Create new polygon wrapper and add it to polygons list.
     *
     * @param activity context activity of map
     * @param id       id of polygon
     * @param geometry data of polygon such as points
     * @param polygons list af all polygons
     * @see PolygonWrapper for more info about polygon wrapper.
     */
    public synchronized void createPolygons(Activity activity, String id, String geometry, List<PolygonWrapper> polygons) {
        try {

            // Read polygon data (coordinates)
            WKTReader wkt = new WKTReader();
            if (geometry.contains("MULTIPOLYGON")) {
                org.locationtech.jts.geom.MultiPolygon multiPolygon = (org.locationtech.jts.geom.MultiPolygon) wkt.read(geometry);

                // Gets each polygon of a multipolygon
                for(int i = 0; i < multiPolygon.getNumGeometries(); i++) {

                    org.locationtech.jts.geom.Polygon polygon = (org.locationtech.jts.geom.Polygon) multiPolygon.getGeometryN(i);

                    // Create polygon options
                    PolygonOptions options = new PolygonOptions();
                    options.strokeWidth(8);
                    options.clickable(true);

                    // Gets each polygon outer coordinates
                    ArrayList<LatLng> outer = new ArrayList<>();
                    Coordinate[] outerCoordinates = polygon.getExteriorRing().getCoordinates();
                    for (Coordinate outerCoordinate : outerCoordinates)
                        outer.add(new LatLng(outerCoordinate.y, outerCoordinate.x));
                    options.addAll(outer);

                    // Getting each polygon interior coordinates (hole) if they exist
                    if(polygon.getNumInteriorRing() > 0){
                        for(int j = 0; j < polygon.getNumInteriorRing(); j++){
                            ArrayList<LatLng> inner = new ArrayList<>();
                            Coordinate[] innerCoordinates = polygon.getInteriorRingN(j).getCoordinates();
                            for (Coordinate innerCoordinate : innerCoordinates)
                                inner.add(new LatLng(innerCoordinate.y, innerCoordinate.x));
                            options.addHole(inner);
                        }
                    }

                    // Create and add polygon wrapper
                    polygons.add(new PolygonWrapper(activity, id, options, PolygonWrapper.Behavior.PART_SHOWING));
                }
            } else {
                org.locationtech.jts.geom.Polygon polygon = (org.locationtech.jts.geom.Polygon) wkt.read(geometry);

                // Create polygon options
                PolygonOptions options = new PolygonOptions();
                options.strokeWidth(8);
                options.clickable(true);

                // Gets polygon outer coordinates
                ArrayList<LatLng> outer = new ArrayList<>();
                Coordinate[] outerCoordinates = polygon.getExteriorRing().getCoordinates();
                for (Coordinate outerCoordinate : outerCoordinates)
                    outer.add(new LatLng(outerCoordinate.y, outerCoordinate.x));
                options.addAll(outer);

                // Getting polygon interior coordinates (hole) if they exist
                if(polygon.getNumInteriorRing() > 0){
                    for(int j = 0; j < polygon.getNumInteriorRing(); j++){
                        ArrayList<LatLng> inner = new ArrayList<>();
                        Coordinate[] innerCoordinates = polygon.getInteriorRingN(j).getCoordinates();
                        for (Coordinate innerCoordinate : innerCoordinates)
                            inner.add(new LatLng(innerCoordinate.y, innerCoordinate.x));
                        options.addHole(inner);
                    }
                }

                // Create and add polygon wrapper
                polygons.add(new PolygonWrapper(activity, id, options, PolygonWrapper.Behavior.PART_SHOWING));
            }

        } catch (org.locationtech.jts.io.ParseException e) {
            e.printStackTrace();
        }
    }

    /**
     * Update visible polygons on map based on locating in map viewport.
     * Also map zoom is important in showing polygons, because of polygons count on map in low zooms.
     * We remove some very small polygons in low zoom.
     * This operations is require to prevent app not responding when polygons are too many.
     * Polygons that are not in viewport will be remove from map.
     * This method must be call in onCameraMove event to get map new bounds and zoom.
     * Operations will be done in new thread. Thread change polygons visibility continuously.
     *
     * @param map      map that polygons must be shown on it
     * @param polygons list of all polygons
     */
    public synchronized void updatePolygons(GoogleMap map, List<PolygonWrapper> polygons) {

        // Limit update interval
        long time = SystemClock.elapsedRealtime();
        if (time - lastUpdate < UPDATE_INTERVAL)
            return;

        // Update last update time
        lastUpdate = time;

        // Bounds and zoom should get in ui thread. so we get them out of thread
        this.bounds = map.getProjection().getVisibleRegion().latLngBounds;
        this.zoom = map.getCameraPosition().zoom;

        // We have only one thread and if it is created so we don't need recreate it
        if (thread != null)
            return;

        // Create and run thread
        this.map = map;
        this.polygons = polygons;
        this.stopFlag = false;
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        while (!stopFlag) {

            // Call by try-catch to prevent unwanted exception and thread stopping
            try {

                // Pause implementation
                synchronized (pauseLock) {
                    while (pauseFlag) {
                        try {
                            pauseLock.wait();
                        } catch (InterruptedException ignored) {
                        }
                    }
                }

                // Update visible polygons on map based on map viewport
                for (PolygonWrapper polygon : polygons) {

                    // Remove polygons that are invisible in given zoom from map
                    if (isVisibleWithZoom(polygon, zoom)) {
                        if (polygon.isAddedToMap()) {
                            polygon.removeFromMap();
                            sleep();
                        }
                        continue;
                    }

                    // Hide out of map viewport polygons
                    if (polygon.isWithin(bounds) && !polygon.isAddedToMap()) {
                        polygon.addToMap(map);
                        sleep();
                    } else if (!polygon.isWithin(bounds)) {
                        polygon.removeFromMap();
                        sleep();
                    }
                }
            } catch (Exception ignored) {
            }
        }
    }

    private boolean isVisibleWithZoom(PolygonWrapper polygon, float zoom) {

        // Compute area of polygon
        double area = SphericalUtil.computeArea(polygon.getOptions().getPoints());

        return (
                (zoom <= 11 && area <= 1000) ||                         // Don't show polygons with area <= 1000 when zoom is <= 11 (map bounds has great area)
                        (zoom > 11 && zoom <= 12 && area <= 500) ||     // Don't show polygons with area <= 500 when zoom is between 11 and 12
                        (zoom > 12 && zoom <= 13 && area <= 250) ||     // Don't show polygons with area <= 250 when zoom is between 12 and 13
                        (zoom > 13 && zoom <= 13.5 && area <= 200) ||   // Don't show polygons with area <= 200 when zoom is between 13 and 13.5
                        (zoom > 13.5 && zoom <= 14 && area <= 150) ||   // Don't show polygons with area <= 150 when zoom is between 13.5 and 14 (map bounds has small area)
                        (zoom > 14 && zoom <= 14.5 && area <= 100)      // Don't show polygons with area <= 100 when zoom is between 14 and 14.5 (map bounds has small area)
        );
    }

    /**
     * Thread sleep allow ui thread to show views and doesn't hang up.
     * Call this method everywhere ui thread action is performing.
     */
    private void sleep() throws InterruptedException {
        Thread.sleep(8);
    }
}

And The PolygonWrapper class:

/**
 * Wrapper class for polygon.
 * See https://stackoverflow.com/questions/36439031/determine-if-polygon-is-within-map-bounds for more info.
 *
 * @version 1.1
 * */
public class PolygonWrapper {

    private final String id;
    private final Behavior behavior;
    private final LatLng northWest, northEast, southEast, southWest;
    private final Activity activity;

    private Polygon polygon;
    private PolygonOptions options;

    public void addToMap(GoogleMap map) {
        activity.runOnUiThread(() -> {
            if (isAddedToMap()) removeFromMap();
            polygon = map.addPolygon(options);
        });
    }

    public void removeFromMap() {
        activity.runOnUiThread(() -> {
            if (isAddedToMap()) {
                polygon.remove();
                polygon = null;
            }
        });
    }

    public PolygonWrapper(Activity activity, String id, PolygonOptions options, Behavior behavior) {
        this.activity = activity;
        this.id = id;
        this.options = options;
        this.behavior = behavior;

        Double north = null, west = null, south = null, east = null;
        for (LatLng latLng : options.getPoints()) {
            if (north == null || latLng.latitude > north)
                north = latLng.latitude;

            if (west == null || latLng.longitude < west)
                west = latLng.longitude;

            if (south == null || latLng.latitude < south)
                south = latLng.latitude;

            if (east == null || latLng.longitude > east)
                east = latLng.longitude;
        }
        northWest = new LatLng(north, west);
        northEast = new LatLng(north, east);
        southEast = new LatLng(south, east);
        southWest = new LatLng(south, west);
    }

    public String getId() {
        return id;
    }

    public PolygonOptions getOptions() {
        return options;
    }

    public Polygon getPolygon() {
        return polygon;
    }

    public PolygonOptions buildBordersRectPolygonOptions() {
        final PolygonOptions rvalue = new PolygonOptions();
        rvalue.add(northWest);
        rvalue.add(northEast);
        rvalue.add(southEast);
        rvalue.add(southWest);
        rvalue.add(northWest);
        rvalue.fillColor(0x6A00FFFF);
        rvalue.strokeColor(0x6AFF0000);
        rvalue.strokeWidth(1f);
        return rvalue;
    }

    public boolean isWithin(LatLngBounds bounds) {
        boolean within = false;
        switch (behavior) {
            case FULL_SHOWING:
                if (bounds.contains(northWest) && bounds.contains(southEast))
                    within = true;
                break;
            case PART_SHOWING:
                if (bounds.contains(northWest)
                        || bounds.contains(southEast)
                        || bounds.contains(northEast)
                        || bounds.contains(southWest)) {
                    within = true;
                } else if (northEast.latitude > bounds.southwest.latitude
                        && northEast.longitude > bounds.southwest.longitude
                        && southWest.latitude < bounds.northeast.latitude
                        && southWest.longitude < bounds.northeast.longitude) {
                    within = true;
                }
                break;
        }
        return within;
    }

    public boolean isAddedToMap() {
        return polygon != null;
    }

    public enum Behavior {
        FULL_SHOWING, PART_SHOWING
    }
}

And we must update polygons in camera position change:

map.setOnCameraMoveListener(() -> {
     // Update visible polygons on map
     PolygonRenderer.getInstance().updatePolygons(map, polygons);
});

Also we need to do this:

@Override
protected void onStop() {
    // Stop polygons renderer class on activity stop
    PolygonRenderer.getInstance().stop();

    super.onStop();
}

In this code I change visibility of polygons by zoom changing too. but this option can be better, if polygons have scale and be for different zoom levels.

Also I used jts-core to get polygon data.

By using this way I handle about 2000-4000 polygon on map.

0

GroundOverlay shows dramatic performance improvement compared to Polygons but it was still laggy for production.

TileOverlay is the correct solution. It's quite to hard to find, but once you got there, the homepage of TileOverlay docs mentioned quite clearly

Tile overlays are useful when you want to add extensive imagery to the map, typically covering large geographical areas. In contrast, ground overlays are useful when you wish to fix a single image at one area on the map.

While the homepage of GroundOverlays documentations say the same thing

If you want to add extensive imagery that covers a large portion of the map, you should consider a Tile overlay.

It's a real shame that the Maps Polygon Android docs does not mention such recommendation so it's quite a chicken and egg problem. Note that thousands of polygons work fine on iOS!

ericn
  • 12,476
  • 16
  • 84
  • 127