Modo efficiente per aggiungere grandi GeoJson a GoogleMap su Android
Ho una mappa della temperatura per alcune aree con una griglia di 3 km. Cioè ho circa poche migliaia di poligoni con colore.
Sto cercando di mostrarli su Google Map su Android nella mia applicazione kotlin.
Il problema è che devo aggiungere il livello GeoJson nel thread dell'interfaccia utente e ci vogliono 8-15 secondi. Cioè per tutto questo tempo l'applicazione è bloccata
Ci sono molte vecchie risposte su StackOverflow su questo problema, ma non sono tutte rilevanti per me:
- Propongono di diminuire il json: creo già cluster da quadrati vicini con lo stesso valore
- Propongono di utilizzare MapView e gli overlay, ma nell'ultima API di google map non c'è MapView e la possibilità di aggiungere overlay nel thread in background
Provai:
- map.addPolygon invece della creazione di un GeoJsonLayer
- map addOverlay con GroundOverlay
Ancora ho più o meno lo stesso tempo in cui l'interfaccia utente è bloccata
Come posso gestire questo problema? C'è un modo per creare una mappa da migliaia di rettangoli colorati in un thread in background e quindi mostrarla immediatamente nel thread dell'interfaccia utente?
Risposte
Se ci provi già addPolygon()e GroundOverlayci sono due possibilità rimaste:
utilizzando le sovrapposizioni di piastrelle (preferito);
utilizzando il disegno personalizzato su MapViewo MapFragment.
IMHO Tile Overlay è un modo migliore grazie alla possibilità di TileProviderimplementazioni ad alte prestazioni . Ad esempio, puoi creare riquadri per livelli di zoom "bassi" e "corrente" (livello che dovrebbe essere mostrato all'utente all'inizio) e salvarli nell'array ( HashMap, ecc.) O nel percorso del file system ..\zoom_level\x\y\tile.pngse ce ne sono molti di piastrelle. E tessere più "dettagliate" che puoi creare "al volo" (in thread separati) quando deve essere mostrato, e poi anche memorizzarle per usi futuri (se necessario). Ovviamente, hai bisogno di un modulo personalizzato per la GeoJsonlettura veloce (qualcosa come Jackson) e il rendering sulle .pngtessere. Quindi, sembra sia possibile creare TileProviderper il tuo caso, ottimale per prestazioni e consumo di memoria. Puoi usare questa risposta di Alex Vasilkov come prima iterazione.
Se scegli il disegno personalizzato, devi sostituire il onDraw()metodo per MapViewo dispatchDraw()per MapFragment. come in questa risposta . In tal caso è possibile controllare tutto il processo, ma in questo modo è più complesso per l'implementazione.
Aggiornare:
Puoi implementare un'azione per onCameraMove(), come in questa risposta (alcuni passaggi complicati di GoogleMapoggetti usati lì):
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); } } }
Aggiornamento n. 2:
È possibile creare uno "screenshot" (non esattamente uno screenshot, ma creare un'immagine di poligoni sulla bitmap) della vista corrente dei poligoni e spostarlo onCameraMove()(non ridisegnare tutti i poligoni). E poi in onCameraIdle()crea e mostra la nuova visualizzazione completa dei poligoni. Inoltre, è possibile creare bitmap leggermente più grandi della visualizzazione della schermata della mappa (per ridurre e scorrere correttamente). Oppure puoi "saltare" alcune onCameraMove()chiamate (es. Chiamare invalidate()una volta ogni 3 onCameraMove()chiamate, ecc.).
A proposito: in caso di Tile Overlay lo spostamento e lo zoom sono disponibili "dalla scatola". Hai solo bisogno di creare un complicato TileProvider. Ci sono solo diversi riquadri che devono essere generati per l'intero schermo del dispositivo (la dimensione del singolo riquadro è 256x256). Quindi, puoi generare tessere per la schermata corrente, per currentZoomLevel-1, per currentZoomLevel + 1 (in caso di zoom) e + 2 (o 3) tessere a sinistra, destra, in alto e in basso (in caso di scorrimento). Inoltre puoi memorizzare le tessere generate per il futuro utilizzando in alcune cache (HashMap, LRU, ecc.). E puoi generare tessere "extra" (non attualmente visibili) in thread separati.
Suggerisco di non creare queste poche migliaia di poligoni in un colpo solo in quanto ciò influenzerà il tempo e le prestazioni, ma invece puoi creare al volo solo poligoni vicino alla posizione mostrata utilizzando GoogleMap.OnCameraMoveListener e GoogleMap.OnCameraIdleListener.
Modifica: - "vicino ai poligoni alla posizione mostrata al volo" intendo creare solo poligoni che l'utente attualmente vede, cioè entro i confini della regione visibile corrente della mappa: - puoi ottenerlo da
googleMap.projection.visibleRegion.latLngBounds
"GoogleMap.OnCameraMoveListener e GoogleMap.OnCameraIdleListenerwhat", voglio dire che prima hai due approcci qui, aggiorni la mappa con poligoni entro i confini visibili quando l'utente smette di scorrere la mappa "setOnCameraIdleListener" o il secondo è l'uso di setOnCameraMoveStartedListener per aggiornare la mappa con i poligoni entro i confini visibili ogni volta che l'utente inizia a scorrere la mappa.
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
}
}