3

I have a temperature map for some area with 3 km grid. I.e. I have about few thousands of polygons with color.

I'm trying to show them on Google Map on android in my kotlin application.

The issue is that I need to add GeoJson layer in UI thread and it takes 8-15 seconds. I.e. all this time the application is frozen

There are a lot of old answers on StackOverflow about this issue but they all are not relevant for me:

  1. They propose to decrease the json - I already create clusters from nearby squares with the same value
  2. They propose to use MapView and overlays - but in the latest google map API there is no MapView and ability to add overlays in background thread

I tried:

  1. map.addPolygon instead of creation of a GeoJsonLayer
  2. map addOverlay with GroundOverlay

Still I have more or less the same time when UI is frozen

How can I manage this issue? Is there any way to create a map from thousands of colorful rectangles in a background thread and then show it immediately in UI thread?

Pavel Bernshtam
  • 4,232
  • 8
  • 38
  • 62

2 Answers2

2

If you already try addPolygon() and GroundOverlay there are two possibilities left:

  1. using Tile Overlays (preferred);

  2. using custom drawing over MapView or MapFragment.

IMHO Tile Overlay is a better way due possibility of high performance TileProvider implementation. For example, you can create tiles for "low" zoom levels and "current" (level that should be shown to user at the beginning) zoom level and store them in array (HashMap, etc.) or file system path ..\zoom_level\x\y\tile.png if there are a lot of tiles. And more "detailed" tiles you can create "on the fly" (in separate thread) when it needs to be shown, and then also store them for future using (if needed). Of course, you need custom module for fast GeoJson reading (something like Jackson) and rendering it to the .png tiles. So, seems it is possible to create TileProvider for your case, optimal by performance and memory consumption. You can use this answer of Alex Vasilkov as first iteration.

If you choose custom drawing you should override onDraw() method for MapView or dispatchDraw() for MapFragment. like in this answer. In that case you can control all of the process, but that way is more complex for implementation.

Update:

You can implement action for onCameraMove(), like in this answer (some tricky passing of GoogleMap object used there):

public class RadarMapView extends MapView implements OnMapReadyCallback {

    private OnMapReadyCallback mMapReadyCallback;
    private GoogleMap mGoogleMap;
    private Marker mMarker;
    private Paint mPaintRadar;

    public RadarMapView(@NonNull Context context) {
        super(context);
        init();
    }

    public RadarMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RadarMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public RadarMapView(@NonNull Context context, @Nullable GoogleMapOptions options) {
        super(context, options);
        init();
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        drawRadarOverTheMap(canvas);
        canvas.restore();
    }

    private void drawRadarOverTheMap(Canvas canvas) {
        if (mGoogleMap == null) {
            return;
        }

        final float centerX = getX() + getWidth() / 2;
        final float centerY = getY() + getHeight() / 2;

        canvas.drawCircle(centerX, centerY, 150, mPaintRadar);
        canvas.drawCircle(centerX, centerY, 300, mPaintRadar);
        canvas.drawCircle(centerX, centerY, 450, mPaintRadar);
    }

    private void init() {
        setWillNotDraw(false);

        mPaintRadar = new Paint();
        mPaintRadar.setColor(Color.GREEN);
        mPaintRadar.setStyle(Paint.Style.STROKE);
        mPaintRadar.setStrokeWidth(10);
    }

    @Override
    public void getMapAsync(OnMapReadyCallback callback) {
        mMapReadyCallback = callback;
        super.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mGoogleMap = googleMap;
        mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
            @Override
            public void onCameraMove() {
                invalidate();     // NB! Exactly this line you need
            }
        });
        if (mMapReadyCallback != null) {
            mMapReadyCallback.onMapReady(googleMap);
        }
    }

}

Update #2:

You can make "screenshot" (not exactly screenshot, but create image of polyigons on the bitmap) of current view of polygons and move it in onCameraMove() (not redraw all polygons). And then in onCameraIdle() create and show new full polygons view. Also, you can create bitmap slightly bigger then map screen view (for zooming out and scrolling properly). Or you can "skip" some of the onCameraMove() calls (e.g. call invalidate() once per 3 onCameraMove() calls etc.).

By the way: in case of Tile Overlays moving and zooming are available "from the box". You only need to create a tricky TileProvider. There are only several tiles need to be generated for whole device screen (size of the single tile is 256x256). So, you can generate tiles for current screen, for currentZoomLevel-1, for currentZoomLevel+1 (in case of zooming) and + 2 (or 3) tiles to the left, right, top and bottom (in case of scrolling). Also you can store generated tiles for future using in some cache (HashMap, LRU, etc.). And you can generate "extra" (not currently visible) tiles in separate threads.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79
  • Thank you! Create tiles pngs fir different zoom levels from geojson looks too much work, but I'll try the second approach! – Pavel Bernshtam Oct 24 '20 at 05:44
  • Look good, but dispatchDraw is not called on map move, so my polygons remans on the same place while I move or zoom the map – Pavel Bernshtam Oct 24 '20 at 08:21
  • @PavelBernshtam Please see updated answer. Briefly: you can do it by call `invalidate()` of your custom view in `public void onCameraMove()`. – Andrii Omelchenko Oct 25 '20 at 10:58
  • Thanks, but it works very not smooth. I.e. my polygons are moving slowly (!) after the map changed location. Very not pleasant user experience ... – Pavel Bernshtam Oct 25 '20 at 11:36
  • @PavelBernshtam Please see Update #2. – Andrii Omelchenko Oct 25 '20 at 12:02
  • Looks like this or another way I should convert my geojson to a bitmap for different zoom levels. If there is no some library it is not simple – Pavel Bernshtam Oct 26 '20 at 07:55
  • @PavelBernshtam Just draw on bitmap with multiple of 256 sides size, like you draw on canvas in `dispatchDraw()` and split it to squares 256x256 (like in [this](https://stackoverflow.com/a/25953122/6950238) answer). And yes, it is not simple. You need really tricky `TileProvider`. – Andrii Omelchenko Oct 26 '20 at 11:34
0

i suggest to not create these few thousands of polygons one shot as this will affect time and performance but instead you can create only near polygons to shown location on the fly making use of GoogleMap.OnCameraMoveListener and GoogleMap.OnCameraIdleListener.

Edit:- "near polygons to shown location on the fly" i mean create only polygons that the user currently see i.e within the current visible region boundaries of map :- you can get it by

googleMap.projection.visibleRegion.latLngBounds

"GoogleMap.OnCameraMoveListener and GoogleMap.OnCameraIdleListenerwhat", I mean you have two approaches here first, you update the map with polygons within the visible boundaries when user stop scrolling the map "setOnCameraIdleListener" or the second one is the use setOnCameraMoveStartedListener to update the map with the polygons within the visible boundaries each time the user start scrolling the map.

 override fun onMapReady(googleMap: GoogleMap?) {
        googleMap ?: return
        with(googleMap) {
            setMinZoomPreference(9f)
            setOnCameraIdleListener {
             // first approach user stopped scrolling so update the map with polygons within the boundaries 
            }
            setOnCameraMoveStartedListener {
             // second approach user started scrolling so update the map with polygons within the boundaries 
            }
        }
Ramy Ibrahim
  • 656
  • 4
  • 19