package cgeo.geocaching.maps.mapsforge.v6;
import cgeo.geocaching.AbstractDialogFragment;
import cgeo.geocaching.AbstractDialogFragment.TargetInfo;
import cgeo.geocaching.CacheListActivity;
import cgeo.geocaching.CachePopup;
import cgeo.geocaching.CompassActivity;
import cgeo.geocaching.EditWaypointActivity;
import cgeo.geocaching.Intents;
import cgeo.geocaching.R;
import cgeo.geocaching.SearchResult;
import cgeo.geocaching.WaypointPopup;
import cgeo.geocaching.activity.AbstractActionBarActivity;
import cgeo.geocaching.activity.ActivityMixin;
import cgeo.geocaching.connector.gc.GCMap;
import cgeo.geocaching.connector.gc.Tile;
import cgeo.geocaching.enumerations.CacheType;
import cgeo.geocaching.enumerations.CoordinatesType;
import cgeo.geocaching.enumerations.LoadFlags;
import cgeo.geocaching.list.StoredList;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.location.Viewport;
import cgeo.geocaching.maps.LivemapStrategy;
import cgeo.geocaching.maps.MapMode;
import cgeo.geocaching.maps.MapOptions;
import cgeo.geocaching.maps.MapProviderFactory;
import cgeo.geocaching.maps.MapState;
import cgeo.geocaching.maps.interfaces.MapSource;
import cgeo.geocaching.maps.interfaces.OnMapDragListener;
import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider;
import cgeo.geocaching.maps.mapsforge.MapsforgeMapSource;
import cgeo.geocaching.maps.mapsforge.v6.caches.CachesBundle;
import cgeo.geocaching.maps.mapsforge.v6.caches.GeoitemRef;
import cgeo.geocaching.maps.mapsforge.v6.layers.DownloadLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.HistoryLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.ITileLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.NavigationLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.PositionLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.RendererLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.TapHandlerLayer;
import cgeo.geocaching.maps.mapsforge.v6.layers.ThunderforestMap;
import cgeo.geocaching.maps.routing.Routing;
import cgeo.geocaching.maps.routing.RoutingMode;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.sensors.GeoData;
import cgeo.geocaching.sensors.GeoDirHandler;
import cgeo.geocaching.sensors.Sensors;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.storage.DataStore;
import cgeo.geocaching.utils.AngleUtils;
import cgeo.geocaching.utils.DisposableHandler;
import cgeo.geocaching.utils.Formatter;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.functions.Action1;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources.NotFoundException;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListAdapter;
import android.widget.TextView;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import butterknife.ButterKnife;
import io.reactivex.disposables.CompositeDisposable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.graphics.AndroidResourceBitmap;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.download.tilesource.OpenStreetMapMapnik;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.model.DisplayModel;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.reader.ReadBuffer;
import org.mapsforge.map.rendertheme.ExternalRenderTheme;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
import org.mapsforge.map.rendertheme.XmlRenderTheme;
import org.mapsforge.map.rendertheme.rule.RenderThemeHandler;
import org.xmlpull.v1.XmlPullParserException;
@SuppressLint("ClickableViewAccessibility")
public class NewMap extends AbstractActionBarActivity {
private MfMapView mapView;
private TileCache tileCache;
private ITileLayer tileLayer;
private HistoryLayer historyLayer;
private PositionLayer positionLayer;
private NavigationLayer navigationLayer;
private CachesBundle caches;
private final MapHandlers mapHandlers = new MapHandlers(new TapHandler(this), new DisplayHandler(this), new ShowProgressHandler(this));
private DistanceView distanceView;
private ArrayList<Location> trailHistory = null;
private String targetGeocode = null;
private Geopoint lastNavTarget = null;
private final Queue<String> popupGeocodes = new ConcurrentLinkedQueue<>();
private ProgressDialog waitDialog;
private LoadDetails loadDetailsThread;
private final UpdateLoc geoDirUpdate = new UpdateLoc(this);
/**
* initialization with an empty subscription to make static code analysis tools more happy
*/
private final CompositeDisposable resumeDisposables = new CompositeDisposable();
private CheckBox myLocSwitch;
private MapOptions mapOptions;
private TargetView targetView;
private static boolean followMyLocation;
private static final String BUNDLE_MAP_STATE = "mapState";
private static final String BUNDLE_TRAIL_HISTORY = "trailHistory";
// Handler messages
// DisplayHandler
public static final int UPDATE_TITLE = 0;
public static final int INVALIDATE_MAP = 1;
// ShowProgressHandler
public static final int HIDE_PROGRESS = 0;
public static final int SHOW_PROGRESS = 1;
// LoadDetailsHandler
public static final int UPDATE_PROGRESS = 0;
public static final int FINISHED_LOADING_DETAILS = 1;
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidGraphicFactory.createInstance(this.getApplication());
// some tiles are rather big, see https://github.com/mapsforge/mapsforge/issues/868
ReadBuffer.setMaximumBufferSize(6500000);
// Get parameters from the intent
mapOptions = new MapOptions(this, getIntent().getExtras());
// Get fresh map information from the bundle if any
if (savedInstanceState != null) {
mapOptions.mapState = savedInstanceState.getParcelable(BUNDLE_MAP_STATE);
trailHistory = savedInstanceState.getParcelableArrayList(BUNDLE_TRAIL_HISTORY);
followMyLocation = mapOptions.mapState.followsMyLocation();
} else {
followMyLocation = followMyLocation && mapOptions.mapMode == MapMode.LIVE;
}
ActivityMixin.onCreate(this, true);
// set layout
ActivityMixin.setTheme(this);
setContentView(R.layout.map_mapsforge_v6);
setTitle();
// initialize map
mapView = (MfMapView) findViewById(R.id.mfmapv5);
mapView.setClickable(true);
mapView.getMapScaleBar().setVisible(true);
mapView.setBuiltInZoomControls(true);
mapView.getMapZoomControls().setZoomLevelMin((byte) 10);
mapView.getMapZoomControls().setZoomLevelMax((byte) 20);
// create a tile cache of suitable size. always initialize it based on the smallest tile size to expect (256 for online tiles)
tileCache = AndroidUtil.createTileCache(this, "mapcache", 256, 1f, this.mapView.getModel().frameBufferModel.getOverdrawFactor());
// attach drag handler
final DragHandler dragHandler = new DragHandler(this);
mapView.setOnMapDragListener(dragHandler);
// prepare initial settings of mapView
if (mapOptions.mapState != null) {
this.mapView.getModel().mapViewPosition.setCenter(MapsforgeUtils.toLatLong(mapOptions.mapState.getCenter()));
this.mapView.getModel().mapViewPosition.setZoomLevel((byte) mapOptions.mapState.getZoomLevel());
this.targetGeocode = mapOptions.mapState.getTargetGeocode();
this.lastNavTarget = mapOptions.mapState.getLastNavTarget();
mapOptions.isLiveEnabled = mapOptions.mapState.isLiveEnabled();
mapOptions.isStoredEnabled = mapOptions.mapState.isStoredEnabled();
} else if (mapOptions.searchResult != null) {
final Viewport viewport = DataStore.getBounds(mapOptions.searchResult.getGeocodes());
if (viewport != null) {
mapView.zoomToViewport(viewport);
}
} else if (StringUtils.isNotEmpty(mapOptions.geocode)) {
final Viewport viewport = DataStore.getBounds(mapOptions.geocode);
if (viewport != null) {
mapView.zoomToViewport(viewport);
}
targetGeocode = mapOptions.geocode;
} else if (mapOptions.coords != null) {
mapView.zoomToViewport(new Viewport(mapOptions.coords, 0, 0));
} else {
mapView.zoomToViewport(new Viewport(Settings.getMapCenter().getCoords(), 0, 0));
}
prepareFilterBar();
Routing.connect();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final boolean result = super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.map_activity, menu);
MapProviderFactory.addMapviewMenuItems(menu);
final MenuItem item = menu.findItem(R.id.menu_toggle_mypos);
myLocSwitch = new CheckBox(this);
myLocSwitch.setButtonDrawable(R.drawable.ic_menu_myposition);
item.setActionView(myLocSwitch);
initMyLocationSwitchButton(myLocSwitch);
return result;
}
@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
super.onPrepareOptionsMenu(menu);
for (final MapSource mapSource : MapProviderFactory.getMapSources()) {
final MenuItem menuItem = menu.findItem(mapSource.getNumericalId());
if (menuItem != null) {
if (mapSource instanceof MapsforgeMapProvider.OfflineMapSource) {
menuItem.setVisible(mapSource.isAvailable());
} else if (mapSource instanceof MapsforgeMapSource) {
menuItem.setVisible(mapSource.isAvailable());
} else {
menuItem.setVisible(false);
}
}
}
try {
final MenuItem itemMapLive = menu.findItem(R.id.menu_map_live);
if (mapOptions.isLiveEnabled) {
itemMapLive.setTitle(res.getString(R.string.map_live_disable));
} else {
itemMapLive.setTitle(res.getString(R.string.map_live_enable));
}
itemMapLive.setVisible(mapOptions.coords == null);
menu.findItem(R.id.menu_store_caches).setVisible(false);
menu.findItem(R.id.menu_store_caches).setVisible(!caches.isDownloading() && caches.getVisibleItemsCount() > 0);
menu.findItem(R.id.menu_mycaches_mode).setChecked(Settings.isExcludeMyCaches());
menu.findItem(R.id.menu_disabled_mode).setChecked(Settings.isExcludeDisabledCaches());
menu.findItem(R.id.menu_direction_line).setChecked(Settings.isMapDirection());
//TODO: circles menu.findItem(R.id.menu_circle_mode).setChecked(this.searchOverlay.getCircles());
menu.findItem(R.id.menu_circle_mode).setVisible(false);
menu.findItem(R.id.menu_trail_mode).setChecked(Settings.isMapTrail());
menu.findItem(R.id.menu_theme_mode).setVisible(tileLayerHasThemes());
menu.findItem(R.id.menu_as_list).setVisible(!caches.isDownloading() && caches.getVisibleItemsCount() > 0);
menu.findItem(R.id.submenu_strategy).setVisible(mapOptions.isLiveEnabled);
switch (Settings.getLiveMapStrategy()) {
case FAST:
menu.findItem(R.id.menu_strategy_fast).setChecked(true);
break;
case AUTO:
menu.findItem(R.id.menu_strategy_auto).setChecked(true);
break;
default: // DETAILED
menu.findItem(R.id.menu_strategy_detailed).setChecked(true);
break;
}
menu.findItem(R.id.submenu_routing).setVisible(Routing.isAvailable());
switch (Settings.getRoutingMode()) {
case STRAIGHT:
menu.findItem(R.id.menu_routing_straight).setChecked(true);
break;
case WALK:
menu.findItem(R.id.menu_routing_walk).setChecked(true);
break;
case BIKE:
menu.findItem(R.id.menu_routing_bike).setChecked(true);
break;
case CAR:
menu.findItem(R.id.menu_routing_car).setChecked(true);
break;
}
menu.findItem(R.id.menu_hint).setVisible(mapOptions.mapMode == MapMode.SINGLE);
menu.findItem(R.id.menu_compass).setVisible(mapOptions.mapMode == MapMode.SINGLE);
} catch (final RuntimeException e) {
Log.e("NewMap.onPrepareOptionsMenu", e);
}
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
ActivityMixin.navigateUp(this);
return true;
case R.id.menu_trail_mode:
Settings.setMapTrail(!Settings.isMapTrail());
historyLayer.requestRedraw();
ActivityMixin.invalidateOptionsMenu(this);
return true;
case R.id.menu_direction_line:
Settings.setMapDirection(!Settings.isMapDirection());
navigationLayer.requestRedraw();
ActivityMixin.invalidateOptionsMenu(this);
return true;
case R.id.menu_map_live:
mapOptions.isLiveEnabled = !mapOptions.isLiveEnabled;
if (mapOptions.isLiveEnabled) {
mapOptions.isStoredEnabled = true;
}
if (mapOptions.mapMode == MapMode.LIVE) {
Settings.setLiveMap(mapOptions.isLiveEnabled);
}
caches.enableStoredLayers(mapOptions.isStoredEnabled);
caches.handleLiveLayers(mapOptions.isLiveEnabled);
ActivityMixin.invalidateOptionsMenu(this);
if (mapOptions.mapMode != MapMode.SINGLE) {
mapOptions.title = StringUtils.EMPTY;
} else {
// reset target cache on single mode map
targetGeocode = mapOptions.geocode;
}
return true;
case R.id.menu_store_caches:
if (!caches.isDownloading()) {
final Set<String> geocodes = caches.getVisibleGeocodes();
if (geocodes.isEmpty()) {
ActivityMixin.showToast(this, res.getString(R.string.warn_save_nothing));
return true;
}
if (Settings.getChooseList()) {
// let user select list to store cache in
new StoredList.UserInterface(this).promptForMultiListSelection(R.string.list_title, new Action1<Set<Integer>>() {
@Override
public void call(final Set<Integer> selectedListIds) {
storeCaches(geocodes, selectedListIds);
}
}, true, Collections.singleton(StoredList.TEMPORARY_LIST.id), false);
} else {
storeCaches(geocodes, Collections.singleton(StoredList.STANDARD_LIST_ID));
}
}
return true;
case R.id.menu_circle_mode:
// overlayCaches.switchCircles();
// mapView.repaintRequired(overlayCaches);
// ActivityMixin.invalidateOptionsMenu(activity);
return true;
case R.id.menu_mycaches_mode:
Settings.setExcludeMine(!Settings.isExcludeMyCaches());
caches.invalidate();
ActivityMixin.invalidateOptionsMenu(this);
if (!Settings.isExcludeMyCaches()) {
Tile.cache.clear();
}
return true;
case R.id.menu_disabled_mode:
Settings.setExcludeDisabled(!Settings.isExcludeDisabledCaches());
caches.invalidate();
ActivityMixin.invalidateOptionsMenu(this);
if (!Settings.isExcludeDisabledCaches()) {
Tile.cache.clear();
}
return true;
case R.id.menu_theme_mode:
selectMapTheme();
return true;
case R.id.menu_as_list: {
CacheListActivity.startActivityMap(this, new SearchResult(caches.getVisibleGeocodes()));
return true;
}
case R.id.menu_strategy_fast: {
item.setChecked(true);
Settings.setLiveMapStrategy(LivemapStrategy.FAST);
return true;
}
case R.id.menu_strategy_auto: {
item.setChecked(true);
Settings.setLiveMapStrategy(LivemapStrategy.AUTO);
return true;
}
case R.id.menu_strategy_detailed: {
item.setChecked(true);
Settings.setLiveMapStrategy(LivemapStrategy.DETAILED);
return true;
}
case R.id.menu_routing_straight: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.STRAIGHT);
navigationLayer.requestRedraw();
return true;
}
case R.id.menu_routing_walk: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.WALK);
navigationLayer.requestRedraw();
return true;
}
case R.id.menu_routing_bike: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.BIKE);
navigationLayer.requestRedraw();
return true;
}
case R.id.menu_routing_car: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.CAR);
navigationLayer.requestRedraw();
return true;
}
case R.id.menu_hint:
menuShowHint();
return true;
case R.id.menu_compass:
menuCompass();
return true;
default:
final MapSource mapSource = MapProviderFactory.getMapSource(id);
if (mapSource != null) {
item.setChecked(true);
setMapSource(mapSource);
return true;
}
}
return false;
}
private void menuCompass() {
final Geocache cache = getCurrentTargetCache();
if (cache != null) {
CompassActivity.startActivityCache(this, cache);
}
}
private void menuShowHint() {
final Geocache cache = getCurrentTargetCache();
if (cache != null) {
cache.showHintToast(this);
}
}
private void prepareFilterBar() {
// show the filter warning bar if the filter is set
if (Settings.getCacheType() != CacheType.ALL) {
final String cacheType = Settings.getCacheType().getL10n();
final TextView filterTitleView = ButterKnife.findById(this, R.id.filter_text);
filterTitleView.setText(cacheType);
findViewById(R.id.filter_bar).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.filter_bar).setVisibility(View.GONE);
}
}
/**
* @param view
* Not used here, required by layout
*/
public void showFilterMenu(final View view) {
// do nothing, the filter bar only shows the global filter
}
private void selectMapTheme() {
final File[] themeFiles = Settings.getMapThemeFiles();
String currentTheme = StringUtils.EMPTY;
final String currentThemePath = Settings.getCustomRenderThemeFilePath();
if (StringUtils.isNotEmpty(currentThemePath)) {
final File currentThemeFile = new File(currentThemePath);
currentTheme = currentThemeFile.getName();
}
final List<String> names = new ArrayList<>();
names.add(res.getString(R.string.map_theme_builtin));
int currentItem = 0;
for (final File file : themeFiles) {
if (currentTheme.equalsIgnoreCase(file.getName())) {
currentItem = names.size();
}
names.add(file.getName());
}
final int selectedItem = currentItem;
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.map_theme_select);
builder.setSingleChoiceItems(names.toArray(new String[names.size()]), selectedItem, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int newItem) {
if (newItem != selectedItem) {
// Adjust index because of <default> selection
if (newItem > 0) {
Settings.setCustomRenderThemeFile(themeFiles[newItem - 1].getPath());
} else {
Settings.setCustomRenderThemeFile(StringUtils.EMPTY);
}
setMapTheme();
}
dialog.cancel();
}
});
builder.show();
}
protected void setMapTheme() {
if (tileLayer == null || tileLayer.getTileLayer() == null) {
return;
}
if (!tileLayer.hasThemes()) {
tileLayer.getTileLayer().requestRedraw();
return;
}
final TileRendererLayer rendererLayer = (TileRendererLayer) tileLayer.getTileLayer();
final String themePath = Settings.getCustomRenderThemeFilePath();
if (StringUtils.isEmpty(themePath)) {
rendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
} else {
try {
final XmlRenderTheme xmlRenderTheme = new ExternalRenderTheme(new File(themePath));
// Validate the theme file
RenderThemeHandler.getRenderTheme(AndroidGraphicFactory.INSTANCE, new DisplayModel(), xmlRenderTheme);
rendererLayer.setXmlRenderTheme(xmlRenderTheme);
} catch (final IOException e) {
Log.w("Failed to set render theme", e);
ActivityMixin.showApplicationToast(getString(R.string.err_rendertheme_file_unreadable));
rendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
} catch (final XmlPullParserException e) {
Log.w("render theme invalid", e);
ActivityMixin.showApplicationToast(getString(R.string.err_rendertheme_invalid));
rendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
}
}
tileCache.purge();
rendererLayer.requestRedraw();
}
private void setMapSource(@NonNull final MapSource mapSource) {
// Update mapsource in settings
Settings.setMapSource(mapSource);
switchTileLayer(mapSource);
}
private void switchTileLayer(final MapSource mapSource) {
// Create new render layer, if mapfile exists
final ITileLayer oldLayer = this.tileLayer;
ITileLayer newLayer = null;
if (mapSource instanceof MapsforgeMapProvider.OfflineMapSource) {
this.mapView.getModel().displayModel.setFixedTileSize(0);
final File mapFile = NewMap.getMapFile();
if (mapFile != null && mapFile.exists()) {
newLayer = new RendererLayer(tileCache, new MapFile(mapFile), this.mapView.getModel().mapViewPosition, false, true, false, AndroidGraphicFactory.INSTANCE);
}
} else {
this.mapView.getModel().displayModel.setFixedTileSize(256);
if (mapSource.getNumericalId() == MapsforgeMapProvider.MAPSFORGE_MAPNIK_ID.hashCode()) {
newLayer = new DownloadLayer(tileCache, this.mapView.getModel().mapViewPosition, OpenStreetMapMapnik.INSTANCE, AndroidGraphicFactory.INSTANCE);
} else if (mapSource.getNumericalId() == MapsforgeMapProvider.MAPSFORGE_CYCLEMAP_ID.hashCode()) {
newLayer = new DownloadLayer(tileCache, this.mapView.getModel().mapViewPosition, ThunderforestMap.INSTANCE, AndroidGraphicFactory.INSTANCE);
}
}
// Exchange layer
if (newLayer != null) {
final Layers layers = this.mapView.getLayerManager().getLayers();
int index = 0;
if (oldLayer != null) {
index = layers.indexOf(oldLayer.getTileLayer()) + 1;
}
layers.add(index, newLayer.getTileLayer());
this.tileLayer = newLayer;
this.setMapTheme();
} else {
this.tileLayer = null;
}
// Cleanup
if (oldLayer != null) {
this.mapView.getLayerManager().getLayers().remove(oldLayer.getTileLayer());
oldLayer.getTileLayer().onDestroy();
}
tileCache.purge();
}
private void resumeTileLayer() {
if (this.tileLayer != null) {
this.tileLayer.onResume();
}
}
private void pauseTileLayer() {
if (this.tileLayer != null) {
this.tileLayer.onPause();
}
}
private boolean tileLayerHasThemes() {
if (tileLayer != null) {
return tileLayer.hasThemes();
}
return false;
}
@Override
protected void onResume() {
super.onResume();
resumeTileLayer();
}
@Override
protected void onStart() {
super.onStart();
initializeLayers();
}
private void initializeLayers() {
switchTileLayer(Settings.getMapSource());
// History Layer
this.historyLayer = new HistoryLayer(trailHistory);
this.mapView.getLayerManager().getLayers().add(this.historyLayer);
// NavigationLayer
Geopoint navTarget = lastNavTarget;
if (navTarget == null) {
navTarget = mapOptions.coords;
if (navTarget == null && StringUtils.isNotEmpty(mapOptions.geocode)) {
final Viewport bounds = DataStore.getBounds(mapOptions.geocode);
if (bounds != null) {
navTarget = bounds.center;
}
}
}
this.navigationLayer = new NavigationLayer(navTarget);
this.mapView.getLayerManager().getLayers().add(this.navigationLayer);
// TapHandler
final TapHandlerLayer tapHandlerLayer = new TapHandlerLayer(this.mapHandlers.getTapHandler());
this.mapView.getLayerManager().getLayers().add(tapHandlerLayer);
// Caches bundle
if (mapOptions.searchResult != null) {
this.caches = new CachesBundle(mapOptions.searchResult, this.mapView, this.mapHandlers);
} else if (StringUtils.isNotEmpty(mapOptions.geocode)) {
this.caches = new CachesBundle(mapOptions.geocode, this.mapView, this.mapHandlers);
} else if (mapOptions.coords != null) {
this.caches = new CachesBundle(mapOptions.coords, mapOptions.waypointType, this.mapView, this.mapHandlers);
} else {
caches = new CachesBundle(this.mapView, this.mapHandlers);
}
// Stored enabled map
caches.enableStoredLayers(mapOptions.isStoredEnabled);
// Live enabled map
caches.handleLiveLayers(mapOptions.isLiveEnabled);
// Position layer
this.positionLayer = new PositionLayer();
this.mapView.getLayerManager().getLayers().add(positionLayer);
//Distance view
this.distanceView = new DistanceView(navTarget, (TextView) findViewById(R.id.distance));
//Target view
this.targetView = new TargetView((TextView) findViewById(R.id.target), StringUtils.EMPTY, StringUtils.EMPTY);
final Geocache target = getCurrentTargetCache();
if (target != null) {
targetView.setTarget(target.getGeocode(), target.getName());
}
this.resumeDisposables.add(this.geoDirUpdate.start(GeoDirHandler.UPDATE_GEODIR));
}
@Override
public void onPause() {
savePrefs();
pauseTileLayer();
super.onPause();
}
@Override
protected void onStop() {
waitDialog = null;
terminateLayers();
super.onStop();
}
private void terminateLayers() {
this.resumeDisposables.clear();
this.caches.onDestroy();
this.caches = null;
this.mapView.getLayerManager().getLayers().remove(this.positionLayer);
this.positionLayer = null;
this.mapView.getLayerManager().getLayers().remove(this.navigationLayer);
this.navigationLayer = null;
this.mapView.getLayerManager().getLayers().remove(this.historyLayer);
this.historyLayer = null;
if (this.tileLayer != null) {
this.mapView.getLayerManager().getLayers().remove(this.tileLayer.getTileLayer());
this.tileLayer.getTileLayer().onDestroy();
this.tileLayer = null;
}
}
/**
* store caches, invoked by "store offline" menu item
*
* @param listIds
* the lists to store the caches in
*/
private void storeCaches(final Set<String> geocodes, final Set<Integer> listIds) {
final int count = geocodes.size();
final LoadDetailsHandler loadDetailsHandler = new LoadDetailsHandler(count, this);
waitDialog = new ProgressDialog(this);
waitDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
waitDialog.setCancelable(true);
waitDialog.setCancelMessage(loadDetailsHandler.disposeMessage());
waitDialog.setMax(count);
waitDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(final DialogInterface arg0) {
try {
if (loadDetailsThread != null) {
loadDetailsThread.stopIt();
}
} catch (final Exception e) {
Log.e("CGeoMap.storeCaches.onCancel", e);
}
}
});
final float etaTime = count * 7.0f / 60.0f;
final int roundedEta = Math.round(etaTime);
if (etaTime < 0.4) {
waitDialog.setMessage(res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm));
} else {
waitDialog.setMessage(res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, roundedEta, roundedEta));
}
loadDetailsHandler.setStart();
waitDialog.show();
loadDetailsThread = new LoadDetails(loadDetailsHandler, geocodes, listIds);
loadDetailsThread.start();
}
@Override
protected void onDestroy() {
this.tileCache.destroy();
this.mapView.getModel().mapViewPosition.destroy();
this.mapView.destroy();
AndroidResourceBitmap.clearResourceBitmaps();
Routing.disconnect();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
final MapState state = prepareMapState();
outState.putParcelable(BUNDLE_MAP_STATE, state);
if (historyLayer != null) {
outState.putParcelableArrayList(BUNDLE_TRAIL_HISTORY, historyLayer.getHistory());
}
}
private MapState prepareMapState() {
return new MapState(MapsforgeUtils.toGeopoint(mapView.getModel().mapViewPosition.getCenter()), mapView.getModel().mapViewPosition.getZoomLevel(), followMyLocation, false, targetGeocode, lastNavTarget, mapOptions.isLiveEnabled, mapOptions.isStoredEnabled);
}
private void centerMap(final Geopoint geopoint) {
mapView.getModel().mapViewPosition.setCenter(new LatLong(geopoint.getLatitude(), geopoint.getLongitude()));
}
public Location getCoordinates() {
final LatLong center = mapView.getModel().mapViewPosition.getCenter();
final Location loc = new Location("newmap");
loc.setLatitude(center.latitude);
loc.setLongitude(center.longitude);
return loc;
}
private void initMyLocationSwitchButton(final CheckBox locSwitch) {
myLocSwitch = locSwitch;
/*
* TODO: Switch back to ImageSwitcher for animations?
* myLocSwitch.setFactory(this);
* myLocSwitch.setInAnimation(activity, android.R.anim.fade_in);
* myLocSwitch.setOutAnimation(activity, android.R.anim.fade_out);
*/
myLocSwitch.setOnClickListener(new MyLocationListener(this));
switchMyLocationButton();
}
// switch My Location button image
private void switchMyLocationButton() {
myLocSwitch.setChecked(followMyLocation);
if (followMyLocation) {
myLocationInMiddle(Sensors.getInstance().currentGeo());
}
}
public void showAddWaypoint(final LatLong tapLatLong) {
final Geocache cache = getCurrentTargetCache();
if (cache != null) {
EditWaypointActivity.startActivityAddWaypoint(this, cache, new Geopoint(tapLatLong.latitude, tapLatLong.longitude));
}
}
// set my location listener
private static class MyLocationListener implements View.OnClickListener {
@NonNull
private final WeakReference<NewMap> mapRef;
MyLocationListener(@NonNull final NewMap map) {
mapRef = new WeakReference<>(map);
}
private void onFollowMyLocationClicked() {
followMyLocation = !followMyLocation;
final NewMap map = mapRef.get();
if (map != null) {
map.switchMyLocationButton();
}
}
@Override
public void onClick(final View view) {
onFollowMyLocationClicked();
}
}
// Set center of map to my location if appropriate.
private void myLocationInMiddle(final GeoData geo) {
if (followMyLocation) {
centerMap(geo.getCoords());
}
}
@Nullable
private static File getMapFile() {
final String mapFileName = Settings.getMapFile();
if (StringUtils.isNotEmpty(mapFileName)) {
return new File(mapFileName);
}
return null;
}
private static final class DisplayHandler extends Handler {
@NonNull
private final WeakReference<NewMap> mapRef;
DisplayHandler(@NonNull final NewMap map) {
this.mapRef = new WeakReference<>(map);
}
@Override
public void handleMessage(final Message msg) {
final NewMap map = mapRef.get();
if (map == null) {
return;
}
final int what = msg.what;
switch (what) {
case UPDATE_TITLE:
map.setTitle();
map.setSubtitle();
break;
case INVALIDATE_MAP:
map.mapView.repaint();
break;
default:
break;
}
}
}
private void setTitle() {
final String title = calculateTitle();
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(title);
}
}
@NonNull
private String calculateTitle() {
if (mapOptions.isLiveEnabled) {
return res.getString(R.string.map_live);
}
if (mapOptions.mapMode == MapMode.SINGLE) {
final Geocache cache = getSingleModeCache();
if (cache != null) {
return cache.getName();
}
}
return StringUtils.defaultIfEmpty(mapOptions.title, res.getString(R.string.map_map));
}
private void setSubtitle() {
final String subtitle = calculateSubtitle();
if (StringUtils.isEmpty(subtitle)) {
return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(subtitle);
}
}
@NonNull
private String calculateSubtitle() {
if (!mapOptions.isLiveEnabled && mapOptions.mapMode == MapMode.SINGLE) {
final Geocache cache = getSingleModeCache();
if (cache != null) {
return Formatter.formatMapSubtitle(cache);
}
}
// count caches in the sub title
final int visible = countVisibleCaches();
final int total = countTotalCaches();
final StringBuilder subtitle = new StringBuilder();
if (total != 0) {
if (visible != total && Settings.isDebug()) {
subtitle.append(visible).append('/').append(res.getQuantityString(R.plurals.cache_counts, total, total));
} else {
subtitle.append(res.getQuantityString(R.plurals.cache_counts, visible, visible));
}
}
// if (Settings.isDebug() && lastSearchResult != null && StringUtils.isNotBlank(lastSearchResult.getUrl())) {
// subtitle.append(" [").append(lastSearchResult.getUrl()).append(']');
// }
return subtitle.toString();
}
private int countVisibleCaches() {
return caches != null ? caches.getVisibleItemsCount() : 0;
}
private int countTotalCaches() {
return caches != null ? caches.getItemsCount() : 0;
}
/** Updates the progress. */
private static final class ShowProgressHandler extends Handler {
private int counter = 0;
@NonNull private final WeakReference<NewMap> mapRef;
ShowProgressHandler(@NonNull final NewMap map) {
this.mapRef = new WeakReference<>(map);
}
@Override
public void handleMessage(final Message msg) {
final int what = msg.what;
if (what == HIDE_PROGRESS) {
if (--counter == 0) {
showProgress(false);
}
} else if (what == SHOW_PROGRESS) {
showProgress(true);
counter++;
}
}
private void showProgress(final boolean show) {
final NewMap map = mapRef.get();
if (map == null) {
return;
}
map.setProgressBarIndeterminateVisibility(show);
}
}
private static final class LoadDetailsHandler extends DisposableHandler {
private final int detailTotal;
private int detailProgress;
private long detailProgressTime;
private final WeakReference<NewMap> mapRef;
LoadDetailsHandler(final int detailTotal, final NewMap map) {
super();
this.detailTotal = detailTotal;
this.detailProgress = 0;
this.mapRef = new WeakReference<>(map);
}
public void setStart() {
detailProgressTime = System.currentTimeMillis();
}
@Override
public void handleRegularMessage(final Message msg) {
final NewMap map = mapRef.get();
if (map == null) {
return;
}
if (msg.what == UPDATE_PROGRESS) {
if (detailProgress < detailTotal) {
detailProgress++;
}
if (map.waitDialog != null) {
final int secondsElapsed = (int) ((System.currentTimeMillis() - detailProgressTime) / 1000);
final int secondsRemaining;
if (detailProgress > 0) {
secondsRemaining = (detailTotal - detailProgress) * secondsElapsed / detailProgress;
} else {
secondsRemaining = (detailTotal - detailProgress) * secondsElapsed;
}
map.waitDialog.setProgress(detailProgress);
if (secondsRemaining < 40) {
map.waitDialog.setMessage(map.res.getString(R.string.caches_downloading) + " " + map.res.getString(R.string.caches_eta_ltm));
} else {
final int minsRemaining = secondsRemaining / 60;
map.waitDialog.setMessage(map.res.getString(R.string.caches_downloading) + " " + map.res.getQuantityString(R.plurals.caches_eta_mins, minsRemaining, minsRemaining));
}
}
} else if (msg.what == FINISHED_LOADING_DETAILS && map.waitDialog != null) {
map.waitDialog.dismiss();
map.waitDialog.setOnCancelListener(null);
}
}
@Override
public void handleDispose() {
final NewMap map = mapRef.get();
if (map == null) {
return;
}
if (map.loadDetailsThread != null) {
map.loadDetailsThread.stopIt();
}
}
}
// class: update location
private static class UpdateLoc extends GeoDirHandler {
// use the following constants for fine tuning - find good compromise between smooth updates and as less updates as possible
// minimum time in milliseconds between position overlay updates
private static final long MIN_UPDATE_INTERVAL = 500;
// minimum change of heading in grad for position overlay update
private static final float MIN_HEADING_DELTA = 15f;
// minimum change of location in fraction of map width/height (whatever is smaller) for position overlay update
private static final float MIN_LOCATION_DELTA = 0.01f;
@NonNull
Location currentLocation = Sensors.getInstance().currentGeo();
float currentHeading;
private long timeLastPositionOverlayCalculation = 0;
/**
* weak reference to the outer class
*/
@NonNull private final WeakReference<NewMap> mapRef;
UpdateLoc(@NonNull final NewMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public void updateGeoDir(@NonNull final GeoData geo, final float dir) {
currentLocation = geo;
currentHeading = AngleUtils.getDirectionNow(dir);
repaintPositionOverlay();
}
@NonNull
public Location getCurrenLocation() {
return currentLocation;
}
/**
* Repaint position overlay but only with a max frequency and if position or heading changes sufficiently.
*/
void repaintPositionOverlay() {
final long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis > timeLastPositionOverlayCalculation + MIN_UPDATE_INTERVAL) {
timeLastPositionOverlayCalculation = currentTimeMillis;
try {
final NewMap map = mapRef.get();
if (map != null) {
final boolean needsRepaintForDistanceOrAccuracy = needsRepaintForDistanceOrAccuracy();
final boolean needsRepaintForHeading = needsRepaintForHeading();
if (needsRepaintForDistanceOrAccuracy && NewMap.followMyLocation) {
map.centerMap(new Geopoint(currentLocation));
}
if (needsRepaintForDistanceOrAccuracy || needsRepaintForHeading) {
map.historyLayer.setCoordinates(currentLocation);
map.navigationLayer.setCoordinates(currentLocation);
map.distanceView.setCoordinates(currentLocation);
map.positionLayer.setCoordinates(currentLocation);
map.positionLayer.setHeading(currentHeading);
map.positionLayer.requestRedraw();
}
}
} catch (final RuntimeException e) {
Log.w("Failed to update location", e);
}
}
}
boolean needsRepaintForHeading() {
final NewMap map = mapRef.get();
if (map == null) {
return false;
}
return Math.abs(AngleUtils.difference(currentHeading, map.positionLayer.getHeading())) > MIN_HEADING_DELTA;
}
boolean needsRepaintForDistanceOrAccuracy() {
final NewMap map = mapRef.get();
if (map == null) {
return false;
}
final Location lastLocation = map.getCoordinates();
float dist = Float.MAX_VALUE;
if (lastLocation != null) {
if (lastLocation.getAccuracy() != currentLocation.getAccuracy()) {
return true;
}
dist = currentLocation.distanceTo(lastLocation);
}
final float[] mapDimension = new float[1];
if (map.mapView.getWidth() < map.mapView.getHeight()) {
final double span = map.mapView.getLongitudeSpan() / 1e6;
Location.distanceBetween(currentLocation.getLatitude(), currentLocation.getLongitude(), currentLocation.getLatitude(), currentLocation.getLongitude() + span, mapDimension);
} else {
final double span = map.mapView.getLatitudeSpan() / 1e6;
Location.distanceBetween(currentLocation.getLatitude(), currentLocation.getLongitude(), currentLocation.getLatitude() + span, currentLocation.getLongitude(), mapDimension);
}
return dist > (mapDimension[0] * MIN_LOCATION_DELTA);
}
}
private static class DragHandler implements OnMapDragListener {
@NonNull
private final WeakReference<NewMap> mapRef;
DragHandler(@NonNull final NewMap parent) {
mapRef = new WeakReference<>(parent);
}
@Override
public void onDrag() {
final NewMap map = mapRef.get();
if (map != null && NewMap.followMyLocation) {
NewMap.followMyLocation = false;
map.switchMyLocationButton();
}
}
}
public void showSelection(@NonNull final List<GeoitemRef> items) {
if (items.isEmpty()) {
return;
}
if (items.size() == 1) {
showPopup(items.get(0));
return;
}
try {
final ArrayList<GeoitemRef> sorted = new ArrayList<>(items);
Collections.sort(sorted, GeoitemRef.NAME_COMPARATOR);
final LayoutInflater inflater = LayoutInflater.from(this);
final ListAdapter adapter = new ArrayAdapter<GeoitemRef>(this, R.layout.cacheslist_item_select, sorted) {
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final View view = convertView == null ? inflater.inflate(R.layout.cacheslist_item_select, parent, false) : convertView;
final TextView tv = (TextView) view.findViewById(R.id.text);
final GeoitemRef item = getItem(position);
tv.setText(item.getName());
//Put the image on the TextView
tv.setCompoundDrawablesWithIntrinsicBounds(item.getMarkerId(), 0, 0, 0);
final TextView infoView = (TextView) view.findViewById(R.id.info);
infoView.setText(item.getItemCode());
return view;
}
};
final AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(res.getString(R.string.map_select_multiple_items))
.setAdapter(adapter, new SelectionClickListener(sorted))
.create();
dialog.setCanceledOnTouchOutside(true);
dialog.show();
} catch (final NotFoundException e) {
Log.e("NewMap.showSelection", e);
}
}
private class SelectionClickListener implements DialogInterface.OnClickListener {
@NonNull
private final List<GeoitemRef> items;
SelectionClickListener(@NonNull final List<GeoitemRef> items) {
this.items = items;
}
@Override
public void onClick(final DialogInterface dialog, final int which) {
if (which >= 0 && which < items.size()) {
final GeoitemRef item = items.get(which);
showPopup(item);
}
}
}
private void showPopup(final GeoitemRef item) {
if (item == null || StringUtils.isEmpty(item.getGeocode())) {
return;
}
try {
if (item.getType() == CoordinatesType.CACHE) {
final Geocache cache = DataStore.loadCache(item.getGeocode(), LoadFlags.LOAD_CACHE_OR_DB);
if (cache != null) {
final RequestDetailsThread requestDetailsThread = new RequestDetailsThread(cache, this);
requestDetailsThread.start();
return;
}
return;
}
if (item.getType() == CoordinatesType.WAYPOINT && item.getId() >= 0) {
popupGeocodes.add(item.getGeocode());
WaypointPopup.startActivityAllowTarget(this, item.getId(), item.getGeocode());
}
} catch (final NotFoundException e) {
Log.e("NewMap.showPopup", e);
}
}
@Nullable
private Geocache getSingleModeCache() {
if (StringUtils.isNotBlank(mapOptions.geocode)) {
return DataStore.loadCache(mapOptions.geocode, LoadFlags.LOAD_CACHE_OR_DB);
}
return null;
}
@Nullable
private Geocache getCurrentTargetCache() {
if (StringUtils.isNotBlank(targetGeocode)) {
return DataStore.loadCache(targetGeocode, LoadFlags.LOAD_CACHE_OR_DB);
}
return null;
}
private void savePrefs() {
Settings.setMapZoom(MapMode.SINGLE, mapView.getMapZoomLevel());
Settings.setMapCenter(new MapsforgeGeoPoint(mapView.getModel().mapViewPosition.getCenter()));
}
private static class RequestDetailsThread extends Thread {
@NonNull private final Geocache cache;
@NonNull private final WeakReference<NewMap> map;
RequestDetailsThread(@NonNull final Geocache cache, @NonNull final NewMap map) {
this.cache = cache;
this.map = new WeakReference<>(map);
}
public boolean requestRequired() {
return CacheType.UNKNOWN == cache.getType() || cache.getDifficulty() == 0;
}
@Override
public void run() {
final NewMap map = this.map.get();
if (map == null) {
return;
}
if (requestRequired()) {
try {
/* final SearchResult search = */GCMap.searchByGeocodes(Collections.singleton(cache.getGeocode()));
} catch (final Exception ex) {
Log.w("Error requesting cache popup info", ex);
ActivityMixin.showToast(map, R.string.err_request_popup_info);
}
}
map.popupGeocodes.add(cache.getGeocode());
CachePopup.startActivityAllowTarget(map, cache.getGeocode());
}
}
/**
* Thread to store the caches in the viewport. Started by Activity.
*/
private class LoadDetails extends Thread {
private final DisposableHandler handler;
private final Collection<String> geocodes;
private final Set<Integer> listIds;
LoadDetails(final DisposableHandler handler, final Collection<String> geocodes, final Set<Integer> listIds) {
this.handler = handler;
this.geocodes = geocodes;
this.listIds = listIds;
}
public void stopIt() {
handler.dispose();
}
@Override
public void run() {
if (CollectionUtils.isEmpty(geocodes)) {
return;
}
for (final String geocode : geocodes) {
try {
if (handler.isDisposed()) {
break;
}
if (!DataStore.isOffline(geocode, null)) {
Geocache.storeCache(null, geocode, listIds, false, handler);
}
} catch (final Exception e) {
Log.e("CGeoMap.LoadDetails.run", e);
} finally {
handler.sendEmptyMessage(UPDATE_PROGRESS);
}
}
// we're done
caches.invalidate(geocodes);
invalidateOptionsMenuCompatible();
handler.sendEmptyMessage(FINISHED_LOADING_DETAILS);
}
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == AbstractDialogFragment.REQUEST_CODE_TARGET_INFO) {
if (resultCode == AbstractDialogFragment.RESULT_CODE_SET_TARGET) {
final TargetInfo targetInfo = data.getExtras().getParcelable(Intents.EXTRA_TARGET_INFO);
if (targetInfo != null) {
lastNavTarget = targetInfo.coords;
if (navigationLayer != null) {
navigationLayer.setDestination(targetInfo.coords);
navigationLayer.requestRedraw();
}
if (distanceView != null) {
distanceView.setDestination(targetInfo.coords);
distanceView.setCoordinates(geoDirUpdate.getCurrenLocation());
}
if (StringUtils.isNotBlank(targetInfo.geocode)) {
targetGeocode = targetInfo.geocode;
final Geocache target = getCurrentTargetCache();
targetView.setTarget(targetGeocode, target != null ? target.getName() : StringUtils.EMPTY);
}
}
}
final List<String> changedGeocodes = new ArrayList<>();
String geocode = popupGeocodes.poll();
while (geocode != null) {
changedGeocodes.add(geocode);
geocode = popupGeocodes.poll();
}
if (caches != null) {
caches.invalidate(changedGeocodes);
}
}
}
}