package org.osmdroid.samplefragments.data;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import org.osmdroid.R;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.samplefragments.BaseSampleFragment;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.IconOverlay;
import org.osmdroid.views.overlay.Overlay;
/**
* #394 #398 Demonstration how to load/update markers from
* Async Background task.
*
* Created by k3b on 01.09.2016.
*/
public class AsyncTaskDemoFragment extends BaseSampleFragment {
public static final String TAG = "osmAsync";
/** If there is more than 200 millisecs no zoom/scroll update markers */
protected static final int DEFAULT_INACTIVITY_DELAY_IN_MILLISECS = 200;
// ===========================================================
// Constants
// ===========================================================
public static final String TITLE = "AsyncTaskDemoFragment - Load Icons in AsyncTask";
private static final int MENU_ZOOMIN_ID = Menu.FIRST;
private static final int MENU_ZOOMOUT_ID = MENU_ZOOMIN_ID + 1;
private static final int MENU_LAST_ID = MENU_ZOOMIN_ID + 1; // Always set to last unused id
@Override
public String getSampleTitle() {
return TITLE;
}
@Override
protected void addOverlays() {
super.addOverlays();
mMapView.setTileSource(TileSourceFactory.MAPNIK);
// If there is more than 200 millisecs no zoom/scroll update markers
mMapView.setMapListener(new DelayedMapListener(new MapListener() {
@Override
public boolean onScroll(ScrollEvent event) {
reloadMarker();
return false;
}
@Override
public boolean onZoom(ZoomEvent event) {
reloadMarker();
return false;
}
}, DEFAULT_INACTIVITY_DELAY_IN_MILLISECS));
mMapView.setBuiltInZoomControls(true);
mMapView.setMultiTouchControls(true);
mMapView.setTilesScaledToDpi(true);
final Context context = getActivity();
mMarkerIcon = context.getResources().getDrawable(R.drawable.person);
mCurrentBackgroundContentFolder = new FolderOverlay();
mMapView.getOverlays().add(mCurrentBackgroundContentFolder);
setHasOptionsMenu(true);
// MapView.OnFirstLayoutListener initial map display also triggers onScroll to update the markers
mMapView.addOnFirstLayoutListener(new MapView.OnFirstLayoutListener() {
@Override
public void onFirstLayout(View v, int left, int top, int right, int bottom) {
mMapView.zoomToBoundingBox(new BoundingBox(56.0,7.0, 45.0, 16.0), false);
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// Put overlay items first
mMapView.getOverlayManager().onCreateOptionsMenu(menu, MENU_LAST_ID, mMapView);
menu.add(0, MENU_ZOOMIN_ID, Menu.NONE, "ZoomIn");
menu.add(0, MENU_ZOOMOUT_ID, Menu.NONE, "ZoomOut");
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
mMapView.getOverlayManager().onPrepareOptionsMenu(menu, MENU_LAST_ID, mMapView);
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (mMapView.getOverlayManager().onOptionsItemSelected(item, MENU_LAST_ID, mMapView)) {
return true;
}
switch (item.getItemId()) {
case MENU_ZOOMIN_ID:
mMapView.getController().zoomIn();
return true;
case MENU_ZOOMOUT_ID:
mMapView.getController().zoomOut();
return true;
}
return false;
}
//---------------------------------------------------------------
/** Load {@link FolderOverlay} with {@link IconOverlay}s in a Background Task {@link BackgroundMarkerLoaderTask}.
mCurrentBackgroundMarkerLoaderTask.cancel() allows aboarding the loading task on screen rotation.
There are 0 or one tasks running at a time. */
private BackgroundMarkerLoaderTask mCurrentBackgroundMarkerLoaderTask = null;
/** implementation detail: mMarkerIcon attached to each generated {@link IconOverlay}*/
private Drawable mMarkerIcon = null;
/** This must be reomoved from {@link org.osmdroid.views.MapView} when
* {@link BackgroundMarkerLoaderTask} finishes */
private FolderOverlay mCurrentBackgroundContentFolder = null;
/** if > 0 there where zoom/scroll events while {@link BackgroundMarkerLoaderTask} was active so
* {@link #reloadMarker()} bust be called again. */
private int mMissedMapZoomScrollUpdates = 0;
/** called by {@link org.osmdroid.views.MapView} if zoom or scroll has changed to
* reload marker for new visible region in the {@link org.osmdroid.views.MapView} */
private void reloadMarker() {
// initialized
if (mCurrentBackgroundMarkerLoaderTask == null) {
// start background load
int zoom = this.mMapView.getZoomLevel();
BoundingBox world = this.mMapView.getBoundingBox();
reloadMarker(world, zoom);
} else {
// background load is already active. Remember that at least one scroll/zoom was missing
mMissedMapZoomScrollUpdates++;
}
}
/** called by MapView if zoom or scroll has changed to reload marker for new visible region */
private void reloadMarker(BoundingBox latLonArea, int zoom) {
Log.d(TAG,"reloadMarker " + latLonArea + ", zoom " + zoom);
this.mCurrentBackgroundMarkerLoaderTask = new BackgroundMarkerLoaderTask();
this.mCurrentBackgroundMarkerLoaderTask.execute(
latLonArea.getLatSouth(), latLonArea.getLatNorth(),
latLonArea.getLonEast(), latLonArea.getLonWest(), (double) zoom);
}
/** Implements load {@link FolderOverlay} with {@link IconOverlay}s in a Background Task. */
private class BackgroundMarkerLoaderTask extends AsyncTask<Double, Integer, FolderOverlay> {
/**
* Computation of the map itmes in the non-gui background thread. .
*
* @param params latMin, latMax, lonMin, longMax, zoom.
* @return A new FolderOverlay that contain map data for latMin, latMax, lonMin, longMax, zoom.
* @see #onPreExecute()
* @see #onPostExecute
* @see #publishProgress
*/
@Override
protected FolderOverlay doInBackground(Double... params) {
FolderOverlay result = new FolderOverlay();
try {
if (params.length != 5) throw new IllegalArgumentException("expected latMin, latMax, lonMin, longMax, zoom");
int paramNo = 0;
double latMin = params[paramNo++];
double latMax = params[paramNo++];
double lonMin = params[paramNo++];
double lonMax = params[paramNo++];
if (latMin > latMax) {
double t = latMax;
latMax = latMin;
latMin = t;
}
if (latMax-latMin < 0.00001)
return null;
//this is a problem, abort https://github.com/osmdroid/osmdroid/issues/521
if (lonMin > lonMax) {
double t = lonMax;
lonMax = lonMin;
lonMin = t;
}
int zoom = params[paramNo++].intValue();
Log.d(TAG,"async doInBackground" +
" latMin=" +latMin+
" ,latMax=" +latMax+
" ,lonMin=" +lonMin+
" ,lonMax=" +lonMax+
", zoom=" + zoom);
// simulate heavy computation ...
if (isCancelled())
return null;
Thread.sleep(1000, 0);
if (isCancelled())
return null;
// i.e.
// SELECT poi.lat, poi.lon, poi.id, poi.name FROM poi
// WHERE poi.lat >= {latMin} AND poi.lat <= {latMax}
// AND poi.lon >= {lonMin} AND poi.lon <= {lonMax}
// AND {zoom} >= poi.zoomMin AND {zoom} <= poi.zoomMax
double latStep = Math.abs(latMax-latMin) / 6;
double lonStep = Math.abs(lonMax-lonMin) / 6;
for (double lat = latMin; lat <= latMax; lat += latStep) {
for (double lon = lonMin; lon <= lonMax; lon += lonStep) {
result.add(createMarker(lat, lon, zoom));
if (isCancelled())
break;
}
if (isCancelled())
break;
}
} catch (Exception ex) {
// TODO more specific error handling
Log.e(TAG,"doInBackground " + ex.getMessage(),ex);
cancel(false);
}
if (!isCancelled()) {
Log.d(TAG,"doInBackground result " + result.getItems().size());
return result;
}
Log.d(TAG,"doInBackground cancelled");
return null;
}
// This is called in gui-thread when doInBackground() is finished.
@Override
protected void onPostExecute(FolderOverlay result) {
if (!isCancelled() && (result != null)) {
showMarker(result);
}
mCurrentBackgroundMarkerLoaderTask = null;
// there was map move/zoom while {@link BackgroundMarkerLoaderTask} was active. must reload
if (mMissedMapZoomScrollUpdates > 0) {
Log.d(TAG,"onPostExecute: lost " + mMissedMapZoomScrollUpdates + " MapZoomScrollUpdates. Reload items.");
mMissedMapZoomScrollUpdates = 0;
reloadMarker();
}
}
}
private Overlay createMarker(double lat, double lon, int zoom) {
return new IconOverlay(new GeoPoint(lat,lon), mMarkerIcon);
}
/** called in gui thread by {@link BackgroundMarkerLoaderTask} after loading has finished. */
private void showMarker(FolderOverlay newMarker) {
boolean modified = false;
if (this.mCurrentBackgroundContentFolder != null) {
Log.d(TAG,"showMarker remove old " + this.mCurrentBackgroundContentFolder.getItems().size());
this.mMapView.getOverlays().remove(this.mCurrentBackgroundContentFolder);
this.mCurrentBackgroundContentFolder.onDetach(mMapView);
this.mCurrentBackgroundContentFolder = null;
modified = true;
}
if (newMarker != null) {
this.mCurrentBackgroundContentFolder = newMarker;
Log.d(TAG,"showMarker add new " + this.mCurrentBackgroundContentFolder.getItems().size() + ", isAnimating=" + mMapView.isAnimating());
mMapView.getOverlays().add(newMarker);
modified = true;
}
if (modified) {
if (mMapView.isAnimating()) {
mMapView.postInvalidate();
} else {
mMapView.invalidate();
}
}
}
@Override
public void onDestroyView(){
// called i.e. for screen rotation
if (mCurrentBackgroundMarkerLoaderTask != null) {
// make shure that running {@link BackgroundMarkerLoaderTask} does not try to
// update destroyed gui when finished
mCurrentBackgroundMarkerLoaderTask.cancel(false);
mCurrentBackgroundMarkerLoaderTask = null;
}
super.onDestroy();
}
}