package cgeo.geocaching.maps;
import cgeo.geocaching.CacheListActivity;
import cgeo.geocaching.CompassActivity;
import cgeo.geocaching.R;
import cgeo.geocaching.SearchResult;
import cgeo.geocaching.activity.ActivityMixin;
import cgeo.geocaching.compatibility.Compatibility;
import cgeo.geocaching.connector.ConnectorFactory;
import cgeo.geocaching.connector.gc.GCLogin;
import cgeo.geocaching.connector.gc.MapTokens;
import cgeo.geocaching.connector.gc.Tile;
import cgeo.geocaching.enumerations.CacheType;
import cgeo.geocaching.enumerations.LoadFlags;
import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag;
import cgeo.geocaching.enumerations.WaypointType;
import cgeo.geocaching.list.StoredList;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.location.Viewport;
import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl;
import cgeo.geocaching.maps.interfaces.GeoPointImpl;
import cgeo.geocaching.maps.interfaces.MapActivityImpl;
import cgeo.geocaching.maps.interfaces.MapControllerImpl;
import cgeo.geocaching.maps.interfaces.MapItemFactory;
import cgeo.geocaching.maps.interfaces.MapProvider;
import cgeo.geocaching.maps.interfaces.MapSource;
import cgeo.geocaching.maps.interfaces.MapViewImpl;
import cgeo.geocaching.maps.interfaces.OnMapDragListener;
import cgeo.geocaching.maps.routing.Routing;
import cgeo.geocaching.maps.routing.RoutingMode;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.models.Waypoint;
import cgeo.geocaching.network.AndroidBeam;
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.ui.WeakReferenceHandler;
import cgeo.geocaching.utils.AndroidRxUtils;
import cgeo.geocaching.utils.AngleUtils;
import cgeo.geocaching.utils.DisposableHandler;
import cgeo.geocaching.utils.Formatter;
import cgeo.geocaching.utils.LeastRecentlyUsedSet;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.MapUtils;
import cgeo.geocaching.utils.functions.Action1;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.res.Resources;
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.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.CheckBox;
import android.widget.ImageSwitcher;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.ViewSwitcher.ViewFactory;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import butterknife.ButterKnife;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
/**
* Class representing the Map in c:geo
*/
public class CGeoMap extends AbstractMap implements ViewFactory {
/** max. number of caches displayed in the Live Map */
public static final int MAX_CACHES = 500;
/**
* initialization with an empty subscription to make static code analysis tools more happy
*/
private final CompositeDisposable resumeDisposables = new CompositeDisposable();
/** Handler Messages */
private static final int HIDE_PROGRESS = 0;
private static final int SHOW_PROGRESS = 1;
private static final int UPDATE_TITLE = 0;
private static final int INVALIDATE_MAP = 1;
private static final int UPDATE_PROGRESS = 0;
private static final int FINISHED_LOADING_DETAILS = 1;
private static final String BUNDLE_MAP_SOURCE = "mapSource";
private static final String BUNDLE_MAP_STATE = "mapState";
private static final String BUNDLE_LIVE_ENABLED = "liveEnabled";
private static final String BUNDLE_TRAIL_HISTORY = "trailHistory";
// Those are initialized in onCreate() and will never be null afterwards
private Resources res;
private Activity activity;
private MapItemFactory mapItemFactory;
private final LeastRecentlyUsedSet<Geocache> caches = new LeastRecentlyUsedSet<>(MAX_CACHES + DataStore.getAllCachesCount());
private MapViewImpl mapView;
private CachesOverlay overlayCaches;
private PositionAndScaleOverlay overlayPositionAndScale;
private final GeoDirHandler geoDirUpdate = new UpdateLoc(this);
// status data
/** Last search result used for displaying header */
private SearchResult lastSearchResult = null;
private MapTokens tokens = null;
private boolean noMapTokenShowed = false;
// map status data
private static boolean followMyLocation = true;
// threads
private Disposable loadTimer;
private LoadDetails loadDetailsThread = null;
/** Time of last {@link LoadRunnable} run */
private volatile long loadThreadRun = 0L;
//Interthread communication flag
private volatile boolean downloaded = false;
/** Count of caches currently visible */
private int cachesCnt = 0;
/** List of waypoints in the viewport */
private final LeastRecentlyUsedSet<Waypoint> waypoints = new LeastRecentlyUsedSet<>(MAX_CACHES);
// storing for offline
private ProgressDialog waitDialog = null;
private int detailTotal = 0;
private int detailProgress = 0;
private long detailProgressTime = 0L;
// views
private CheckBox myLocSwitch = null;
// other things
private boolean markersInvalidated = false; // previous state for loadTimer
private boolean centered = false; // if map is already centered
private boolean alreadyCentered = false; // -""- for setting my location
private static final Set<String> dirtyCaches = new HashSet<>();
/**
* if live map is enabled, this is the minimum zoom level, independent of the stored setting
*/
private static final int MIN_LIVEMAP_ZOOM = 12;
// Thread pooling
private static final BlockingQueue<Runnable> displayQueue = new ArrayBlockingQueue<>(1);
private static final ThreadPoolExecutor displayExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, displayQueue, new ThreadPoolExecutor.DiscardOldestPolicy());
private static final BlockingQueue<Runnable> downloadQueue = new ArrayBlockingQueue<>(1);
private static final ThreadPoolExecutor downloadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, downloadQueue, new ThreadPoolExecutor.DiscardOldestPolicy());
private static final BlockingQueue<Runnable> loadQueue = new ArrayBlockingQueue<>(1);
private static final ThreadPoolExecutor loadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, loadQueue, new ThreadPoolExecutor.DiscardOldestPolicy());
private MapOptions mapOptions;
// handlers
/** Updates the titles */
private static final class DisplayHandler extends WeakReferenceHandler<CGeoMap> {
DisplayHandler(@NonNull final CGeoMap map) {
super(map);
}
@Override
public void handleMessage(final Message msg) {
final CGeoMap map = getReference();
if (map == null) {
return;
}
final int what = msg.what;
switch (what) {
case UPDATE_TITLE:
map.setTitle();
map.setSubtitle();
break;
case INVALIDATE_MAP:
map.mapView.repaintRequired(null);
break;
default:
break;
}
}
}
private final Handler displayHandler = new DisplayHandler(this);
private void setTitle() {
getActionBar().setTitle(calculateTitle());
}
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));
}
@Nullable
private Geocache getSingleModeCache() {
// use a copy of the caches list to avoid concurrent modification
for (final Geocache geocache : caches.getAsList()) {
if (geocache.getGeocode().equals(mapOptions.geocode)) {
return geocache;
}
}
return null;
}
private void setSubtitle() {
final String subtitle = calculateSubtitle();
if (StringUtils.isEmpty(subtitle)) {
return;
}
getActionBar().setSubtitle(subtitle);
}
private String calculateSubtitle() {
// count caches in the sub title
countVisibleCaches();
if (!mapOptions.isLiveEnabled && mapOptions.mapMode == MapMode.SINGLE) {
final Geocache cache = getSingleModeCache();
if (cache != null) {
return Formatter.formatMapSubtitle(cache);
}
}
final StringBuilder subtitle = new StringBuilder();
if (!caches.isEmpty()) {
final int totalCount = caches.size();
if (cachesCnt != totalCount && Settings.isDebug()) {
subtitle.append(cachesCnt).append('/').append(res.getQuantityString(R.plurals.cache_counts, totalCount, totalCount));
} else {
subtitle.append(res.getQuantityString(R.plurals.cache_counts, cachesCnt, cachesCnt));
}
}
if (Settings.isDebug() && lastSearchResult != null && StringUtils.isNotBlank(lastSearchResult.getUrl())) {
subtitle.append(" [").append(lastSearchResult.getUrl()).append(']');
}
return subtitle.toString();
}
@NonNull
private ActionBar getActionBar() {
final ActionBar actionBar = activity.getActionBar();
assert actionBar != null;
return actionBar;
}
/** Updates the progress. */
private static final class ShowProgressHandler extends Handler {
private int counter = 0;
@NonNull private final WeakReference<CGeoMap> mapRef;
ShowProgressHandler(@NonNull final CGeoMap map) {
this.mapRef = new WeakReference<>(map);
}
@Override
public void handleMessage(final Message msg) {
final int what = msg.what;
if (what == SHOW_PROGRESS) {
showProgress(true);
counter++;
} else if (what == HIDE_PROGRESS && --counter == 0) {
showProgress(false);
}
}
private void showProgress(final boolean show) {
final CGeoMap map = mapRef.get();
if (map == null) {
return;
}
final ProgressBar progress = ButterKnife.findById(map.activity, R.id.actionbar_progress);
if (progress != null) {
final int visibility = show ? View.VISIBLE : View.GONE;
progress.setVisibility(visibility);
}
map.activity.setProgressBarIndeterminateVisibility(show);
}
}
private final Handler showProgressHandler = new ShowProgressHandler(this);
private static final class LoadDetailsHandler extends DisposableHandler {
private final WeakReference<CGeoMap> mapRef;
LoadDetailsHandler(final CGeoMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public void handleRegularMessage(final Message msg) {
final CGeoMap map = mapRef.get();
if (map != null) {
final ProgressDialog waitDialog = map.waitDialog;
if (waitDialog != null) {
if (msg.what == UPDATE_PROGRESS) {
final int detailProgress = map.detailProgress;
final int secondsElapsed = (int) ((System.currentTimeMillis() - map.detailProgressTime) / 1000);
// FIXME: the Math.max below is purely defensive programming around an issue reported
// in https://github.com/cgeo/cgeo/issues/6447. This code should be rewritten to, at least,
// no longer use global variables to pass information between the handler and its user.
final int secondsRemaining = (map.detailTotal - detailProgress) * secondsElapsed / Math.max(detailProgress, 1);
final Resources res = map.res;
waitDialog.setProgress(detailProgress);
if (secondsRemaining < 40) {
waitDialog.setMessage(res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm));
} else {
final int minsRemaining = secondsRemaining / 60;
waitDialog.setMessage(res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, minsRemaining, minsRemaining));
}
} else if (msg.what == FINISHED_LOADING_DETAILS) {
waitDialog.dismiss();
waitDialog.setOnCancelListener(null);
}
}
}
}
@Override
public void handleDispose() {
final CGeoMap map = mapRef.get();
if (map != null) {
final LoadDetails loadDetailsThread = map.loadDetailsThread;
if (loadDetailsThread != null) {
loadDetailsThread.stopIt();
}
}
}
}
/* Current source id */
private int currentSourceId;
public CGeoMap(final MapActivityImpl activity) {
super(activity);
}
protected void countVisibleCaches() {
cachesCnt = mapView.getViewport().count(caches.getAsList());
}
@Override
public void onSaveInstanceState(final Bundle outState) {
outState.putInt(BUNDLE_MAP_SOURCE, currentSourceId);
outState.putParcelable(BUNDLE_MAP_STATE, currentMapState());
outState.putBoolean(BUNDLE_LIVE_ENABLED, mapOptions.isLiveEnabled);
outState.putParcelableArrayList(BUNDLE_TRAIL_HISTORY, overlayPositionAndScale.getHistory());
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// class init
res = this.getResources();
activity = this.getActivity();
final MapProvider mapProvider = Settings.getMapProvider();
mapItemFactory = mapProvider.getMapItemFactory();
// Get parameters from the intent
final Bundle extras = activity.getIntent().getExtras();
mapOptions = new MapOptions(activity, extras);
ArrayList<Location> trailHistory = null;
// Get fresh map information from the bundle if any
if (savedInstanceState != null) {
currentSourceId = savedInstanceState.getInt(BUNDLE_MAP_SOURCE, Settings.getMapSource().getNumericalId());
mapOptions.mapState = savedInstanceState.getParcelable(BUNDLE_MAP_STATE);
mapOptions.isLiveEnabled = savedInstanceState.getBoolean(BUNDLE_LIVE_ENABLED, false);
trailHistory = savedInstanceState.getParcelableArrayList(BUNDLE_TRAIL_HISTORY);
} else {
currentSourceId = Settings.getMapSource().getNumericalId();
}
// If recreating from an obsolete map source, we may need a restart
if (changeMapSource(Settings.getMapSource())) {
return;
}
// reset status
noMapTokenShowed = false;
ActivityMixin.onCreate(activity, true);
// set layout
ActivityMixin.setTheme(activity);
getActionBar().setDisplayHomeAsUpEnabled(true);
activity.setContentView(mapProvider.getMapLayoutId());
setTitle();
// initialize map
mapView = (MapViewImpl) activity.findViewById(mapProvider.getMapViewId());
mapView.setMapSource();
mapView.setBuiltInZoomControls(true);
mapView.displayZoomControls(true);
mapView.preLoad();
mapView.setOnDragListener(new MapDragListener(this));
// initialize overlays
mapView.clearOverlays();
overlayCaches = mapView.createAddMapOverlay(mapView.getContext(), Compatibility.getDrawable(getResources(), R.drawable.marker));
overlayPositionAndScale = mapView.createAddPositionAndScaleOverlay(mapOptions.coords, mapOptions.geocode);
if (trailHistory != null) {
overlayPositionAndScale.setHistory(trailHistory);
}
mapView.repaintRequired(null);
setZoom(Settings.getMapZoom(mapOptions.mapMode));
mapView.getMapController().setCenter(Settings.getMapCenter());
if (mapOptions.mapState != null) {
followMyLocation = mapOptions.mapState.followsMyLocation();
if (overlayCaches.getCircles() != mapOptions.mapState.showsCircles()) {
overlayCaches.switchCircles();
}
} else if (mapOptions.mapMode != MapMode.LIVE) {
followMyLocation = false;
}
if (mapOptions.geocode != null || mapOptions.searchResult != null || mapOptions.coords != null || mapOptions.mapState != null) {
centerMap(mapOptions.geocode, mapOptions.searchResult, mapOptions.coords, mapOptions.mapState);
}
prepareFilterBar();
LiveMapHint.getInstance().showHint(activity);
AndroidBeam.disable(activity);
}
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();
}
/**
* Set the zoom of the map. The zoom is restricted to a certain minimum in case of live map.
*
*/
private void setZoom(final int zoom) {
mapView.getMapController().setZoom(mapOptions.isLiveEnabled ? Math.max(zoom, MIN_LIVEMAP_ZOOM) : zoom);
}
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(activity, R.id.filter_text);
filterTitleView.setText(cacheType);
activity.findViewById(R.id.filter_bar).setVisibility(View.VISIBLE);
} else {
activity.findViewById(R.id.filter_bar).setVisibility(View.GONE);
}
}
@Override
public void onResume() {
super.onResume();
resumeDisposables.addAll(geoDirUpdate.start(GeoDirHandler.UPDATE_GEODIR), startTimer());
final List<String> toRefresh;
synchronized (dirtyCaches) {
toRefresh = new ArrayList<>(dirtyCaches);
dirtyCaches.clear();
}
if (!toRefresh.isEmpty()) {
AndroidRxUtils.refreshScheduler.scheduleDirect(new Runnable() {
@Override
public void run() {
for (final String geocode: toRefresh) {
final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS);
if (cache != null) {
// new collection type needs to remove first
caches.remove(cache);
// re-add to update the freshness
caches.add(cache);
}
}
displayExecutor.execute(new DisplayRunnable(CGeoMap.this));
}
});
}
}
@Override
public void onPause() {
resumeDisposables.clear();
savePrefs();
mapView.destroyDrawingCache();
MapUtils.clearCachedItems();
super.onPause();
}
@Override
public void onStop() {
// Ensure that handlers will not try to update the dialog once the view is detached.
waitDialog = null;
super.onStop();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
super.onCreateOptionsMenu(menu);
MapProviderFactory.addMapviewMenuItems(menu);
final SubMenu subMenuStrategy = menu.findItem(R.id.submenu_strategy).getSubMenu();
subMenuStrategy.setHeaderTitle(res.getString(R.string.map_strategy_title));
/* if we have an Actionbar find the my position toggle */
final MenuItem item = menu.findItem(R.id.menu_toggle_mypos);
myLocSwitch = new CheckBox(activity);
myLocSwitch.setButtonDrawable(R.drawable.ic_menu_myposition);
item.setActionView(myLocSwitch);
initMyLocationSwitchButton(myLocSwitch);
return true;
}
@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) {
menuItem.setVisible(mapSource.isAvailable());
}
}
try {
final MenuItem itemMapLive = menu.findItem(R.id.menu_map_live);
final int titleResource = mapOptions.isLiveEnabled ? R.string.map_live_disable : R.string.map_live_enable;
itemMapLive.setTitle(res.getString(titleResource));
itemMapLive.setVisible(mapOptions.coords == null);
final Set<String> geocodesInViewport = getGeocodesForCachesInViewport();
menu.findItem(R.id.menu_store_caches).setVisible(!isLoading() && CollectionUtils.isNotEmpty(geocodesInViewport) && new SearchResult(geocodesInViewport).hasUnsavedCaches());
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());
menu.findItem(R.id.menu_circle_mode).setChecked(overlayCaches.getCircles());
menu.findItem(R.id.menu_trail_mode).setChecked(Settings.isMapTrail());
menu.findItem(R.id.menu_theme_mode).setVisible(mapView.hasMapThemes());
menu.findItem(R.id.menu_as_list).setVisible(!isLoading() && caches.size() > 1);
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);
}
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("CGeoMap.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(activity);
return true;
case R.id.menu_trail_mode:
Settings.setMapTrail(!Settings.isMapTrail());
mapView.repaintRequired(overlayPositionAndScale);
ActivityMixin.invalidateOptionsMenu(activity);
return true;
case R.id.menu_direction_line:
Settings.setMapDirection(!Settings.isMapDirection());
mapView.repaintRequired(overlayPositionAndScale);
ActivityMixin.invalidateOptionsMenu(activity);
return true;
case R.id.menu_map_live:
mapOptions.isLiveEnabled = !mapOptions.isLiveEnabled;
if (mapOptions.mapMode == MapMode.LIVE) {
Settings.setLiveMap(mapOptions.isLiveEnabled);
}
markersInvalidated = true;
lastSearchResult = null;
mapOptions.searchResult = null;
ActivityMixin.invalidateOptionsMenu(activity);
if (mapOptions.mapMode != MapMode.SINGLE) {
mapOptions.title = StringUtils.EMPTY;
}
updateMapTitle();
return true;
case R.id.menu_store_caches:
if (!isLoading()) {
final Set<String> geocodesInViewport = getGeocodesForCachesInViewport();
final List<String> geocodes = new ArrayList<>();
for (final String geocode : geocodesInViewport) {
if (!DataStore.isOffline(geocode, null)) {
geocodes.add(geocode);
}
}
detailTotal = geocodes.size();
detailProgress = 0;
if (detailTotal == 0) {
ActivityMixin.showToast(activity, res.getString(R.string.warn_save_nothing));
return true;
}
if (Settings.getChooseList()) {
// let user select list to store cache in
new StoredList.UserInterface(activity).promptForMultiListSelection(R.string.list_title, new Action1<Set<Integer>>() {
@Override
public void call(final Set<Integer> selectedListIds) {
storeCaches(geocodes, selectedListIds);
}
}, true, Collections.<Integer>emptySet(), 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());
markersInvalidated = true;
ActivityMixin.invalidateOptionsMenu(activity);
if (!Settings.isExcludeMyCaches()) {
Tile.cache.clear();
}
return true;
case R.id.menu_disabled_mode:
Settings.setExcludeDisabled(!Settings.isExcludeDisabledCaches());
markersInvalidated = true;
ActivityMixin.invalidateOptionsMenu(activity);
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(activity, new SearchResult(getGeocodesForCachesInViewport()));
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);
mapView.repaintRequired(overlayPositionAndScale);
return true;
}
case R.id.menu_routing_walk: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.WALK);
mapView.repaintRequired(overlayPositionAndScale);
return true;
}
case R.id.menu_routing_bike: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.BIKE);
mapView.repaintRequired(overlayPositionAndScale);
return true;
}
case R.id.menu_routing_car: {
item.setChecked(true);
Settings.setRoutingMode(RoutingMode.CAR);
mapView.repaintRequired(overlayPositionAndScale);
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);
changeMapSource(mapSource);
return true;
}
}
return false;
}
private void menuCompass() {
final Geocache cache = getSingleModeCache();
if (cache != null) {
CompassActivity.startActivityCache(this.getActivity(), cache);
}
}
private void menuShowHint() {
final Geocache cache = getSingleModeCache();
if (cache != null) {
cache.showHintToast(getActivity());
}
}
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(activity);
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);
}
mapView.setMapTheme();
}
dialog.cancel();
}
});
builder.show();
}
/**
* @return a non-null Set of geocodes corresponding to the caches that are shown on screen.
*/
private Set<String> getGeocodesForCachesInViewport() {
final Set<String> geocodes = new HashSet<>();
final List<Geocache> cachesProtected = caches.getAsList();
final Viewport viewport = mapView.getViewport();
for (final Geocache cache : cachesProtected) {
if (viewport.contains(cache)) {
geocodes.add(cache.getGeocode());
}
}
return geocodes;
}
/**
* Restart the current activity if the map provider has changed, or change the map source if needed.
*
* @param newSource
* the new map source, which can be the same as the current one
* @return true if a restart is needed, false otherwise
*/
private boolean changeMapSource(@NonNull final MapSource newSource) {
final MapSource oldSource = MapProviderFactory.getMapSource(currentSourceId);
final boolean restartRequired = oldSource == null || !MapProviderFactory.isSameActivity(oldSource, newSource);
Settings.setMapSource(newSource);
currentSourceId = newSource.getNumericalId();
if (restartRequired) {
mapRestart();
} else if (mapView != null) { // changeMapSource can be called by onCreate()
mapOptions.mapState = currentMapState();
mapView.setMapSource();
// re-center the map
centered = false;
centerMap(mapOptions.geocode, mapOptions.searchResult, mapOptions.coords, mapOptions.mapState);
// re-build menues
ActivityMixin.invalidateOptionsMenu(activity);
}
return restartRequired;
}
/**
* Restart the current activity with the default map source.
*/
private void mapRestart() {
mapOptions.mapState = currentMapState();
activity.finish();
mapOptions.startIntent(activity, Settings.getMapProvider().getMapClass());
}
/**
* Get the current map state from the map view if it exists or from the mapStateIntent field otherwise.
*
* @return the current map state as an array of int, or null if no map state is available
*/
private MapState currentMapState() {
if (mapView == null) {
return null;
}
final GeoPointImpl mapCenter = mapView.getMapViewCenter();
return new MapState(mapCenter.getCoords(), mapView.getMapZoomLevel(), followMyLocation, overlayCaches.getCircles(), null, null, mapOptions.isLiveEnabled, false);
}
private void savePrefs() {
Settings.setMapZoom(mapOptions.mapMode, mapView.getMapZoomLevel());
Settings.setMapCenter(mapView.getMapViewCenter());
}
// Set center of map to my location if appropriate.
private void myLocationInMiddle(final GeoData geo) {
if (followMyLocation) {
centerMap(geo.getCoords());
}
}
// 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;
Location currentLocation = Sensors.getInstance().currentGeo();
float currentHeading;
private long timeLastPositionOverlayCalculation = 0;
/**
* weak reference to the outer class
*/
private final WeakReference<CGeoMap> mapRef;
UpdateLoc(final CGeoMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public void updateGeoDir(@NonNull final GeoData geo, final float dir) {
currentLocation = geo;
currentHeading = AngleUtils.getDirectionNow(dir);
repaintPositionOverlay();
}
/**
* 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 CGeoMap map = mapRef.get();
if (map != null) {
final boolean needsRepaintForDistanceOrAccuracy = needsRepaintForDistanceOrAccuracy();
final boolean needsRepaintForHeading = needsRepaintForHeading();
if (needsRepaintForDistanceOrAccuracy && followMyLocation) {
map.centerMap(new Geopoint(currentLocation));
}
if (needsRepaintForDistanceOrAccuracy || needsRepaintForHeading) {
map.overlayPositionAndScale.setCoordinates(currentLocation);
map.overlayPositionAndScale.setHeading(currentHeading);
map.mapView.repaintRequired(map.overlayPositionAndScale);
}
}
} catch (final RuntimeException e) {
Log.w("Failed to update location", e);
}
}
}
boolean needsRepaintForHeading() {
final CGeoMap map = mapRef.get();
return map != null && Math.abs(AngleUtils.difference(currentHeading, map.overlayPositionAndScale.getHeading())) > MIN_HEADING_DELTA;
}
boolean needsRepaintForDistanceOrAccuracy() {
final CGeoMap map = mapRef.get();
if (map == null) {
return false;
}
final Location lastLocation = map.overlayPositionAndScale.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);
}
}
/**
* Starts the load timer.
*/
private Disposable startTimer() {
if (mapOptions.coords != null) {
// display just one point
displayPoint(mapOptions.coords);
loadTimer = new CompositeDisposable();
} else {
loadTimer = Schedulers.newThread().schedulePeriodicallyDirect(new LoadTimerAction(this), 0, 250, TimeUnit.MILLISECONDS);
}
return loadTimer;
}
private static final class LoadTimerAction implements Runnable {
@NonNull private final WeakReference<CGeoMap> mapRef;
private int previousZoom = -100;
private Viewport previousViewport;
LoadTimerAction(@NonNull final CGeoMap map) {
this.mapRef = new WeakReference<>(map);
}
@Override
public void run() {
final CGeoMap map = mapRef.get();
if (map == null) {
return;
}
try {
// get current viewport
final Viewport viewportNow = map.mapView.getViewport();
// Since zoomNow is used only for local comparison purposes,
// it is ok to use the Google Maps compatible zoom level of OSM Maps
final int zoomNow = map.mapView.getMapZoomLevel();
// check if map moved or zoomed
//TODO Portree Use Rectangle inside with bigger search window. That will stop reloading on every move
final boolean moved = map.markersInvalidated || (map.mapOptions.isLiveEnabled && !map.downloaded) || previousViewport == null || zoomNow != previousZoom ||
(mapMoved(previousViewport, viewportNow) && (map.cachesCnt <= 0 || CollectionUtils.isEmpty(map.caches) || !previousViewport.includes(viewportNow)));
// update title on any change
if (moved || !viewportNow.equals(previousViewport)) {
map.updateMapTitle();
}
// save new values
if (moved) {
map.markersInvalidated = false;
final long currentTime = System.currentTimeMillis();
if ((currentTime - map.loadThreadRun) > 1000) {
previousViewport = viewportNow;
previousZoom = zoomNow;
loadExecutor.execute(new LoadRunnable(map));
}
}
} catch (final Exception e) {
Log.w("CGeoMap.startLoadtimer.start", e);
}
}
}
/**
* get if map is loading something
*
*/
private boolean isLoading() {
return !loadTimer.isDisposed() &&
(loadExecutor.getActiveCount() > 0 ||
downloadExecutor.getActiveCount() > 0 ||
displayExecutor.getActiveCount() > 0);
}
/**
* Worker thread that loads caches and waypoints from the database and then spawns the {@link DownloadRunnable}.
* started by the load timer.
*/
private static class LoadRunnable extends DoRunnable {
LoadRunnable(@NonNull final CGeoMap map) {
super(map);
}
@Override
public void runWithMap(final CGeoMap map) {
map.doLoadRun();
}
}
private void doLoadRun() {
try {
showProgressHandler.sendEmptyMessage(SHOW_PROGRESS);
loadThreadRun = System.currentTimeMillis();
final SearchResult searchResult;
final MapMode mapMode = mapOptions.mapMode;
if (mapMode == MapMode.LIVE) {
searchResult = mapOptions.isLiveEnabled ? new SearchResult() : new SearchResult(DataStore.loadStoredInViewport(mapView.getViewport(), Settings.getCacheType()));
} else {
// map started from another activity
searchResult = mapOptions.searchResult != null ? new SearchResult(mapOptions.searchResult) : new SearchResult();
if (mapOptions.geocode != null) {
searchResult.addGeocode(mapOptions.geocode);
}
}
// live mode search result
if (mapOptions.isLiveEnabled) {
searchResult.addSearchResult(DataStore.loadCachedInViewport(mapView.getViewport(), Settings.getCacheType()));
}
downloaded = true;
final Set<Geocache> cachesFromSearchResult = searchResult.getCachesFromSearchResult(LoadFlags.LOAD_WAYPOINTS);
// update the caches
// new collection type needs to remove first
caches.removeAll(cachesFromSearchResult);
caches.addAll(cachesFromSearchResult);
final boolean excludeMine = Settings.isExcludeMyCaches();
final boolean excludeDisabled = Settings.isExcludeDisabledCaches();
if (mapMode == MapMode.LIVE) {
synchronized (caches) {
filter(caches);
}
}
countVisibleCaches();
// we don't want to see any stale waypoints
waypoints.clear();
if (cachesCnt < Settings.getWayPointsThreshold() || mapOptions.geocode != null) {
if (mapOptions.isLiveEnabled || mapMode == MapMode.LIVE || mapMode == MapMode.COORDS) {
//All visible waypoints
final CacheType type = Settings.getCacheType();
final Set<Waypoint> waypointsInViewport = DataStore.loadWaypoints(mapView.getViewport(), excludeMine, excludeDisabled, type);
waypoints.addAll(waypointsInViewport);
} else {
//All waypoints from the viewed caches
for (final Geocache c : caches.getAsList()) {
waypoints.addAll(c.getWaypoints());
}
}
}
//render
displayExecutor.execute(new DisplayRunnable(this));
if (mapOptions.isLiveEnabled) {
downloadExecutor.execute(new DownloadRunnable(this));
}
lastSearchResult = searchResult;
} finally {
showProgressHandler.sendEmptyMessage(HIDE_PROGRESS); // hide progress
}
}
/**
* Worker thread downloading caches from the Internet.
* Started by {@link LoadRunnable}.
*/
private static class DownloadRunnable extends DoRunnable {
DownloadRunnable(final CGeoMap map) {
super(map);
}
@Override
public void runWithMap(final CGeoMap map) {
map.doDownloadRun();
}
}
private void doDownloadRun() {
try {
showProgressHandler.sendEmptyMessage(SHOW_PROGRESS); // show progress
if (Settings.isGCConnectorActive() && tokens == null) {
tokens = GCLogin.getInstance().getMapTokens();
if (StringUtils.isEmpty(tokens.getUserSession()) || StringUtils.isEmpty(tokens.getSessionToken())) {
tokens = null;
if (!noMapTokenShowed) {
ActivityMixin.showToast(activity, res.getString(R.string.map_token_err));
noMapTokenShowed = true;
}
}
}
final SearchResult searchResult = ConnectorFactory.searchByViewport(mapView.getViewport().resize(0.8), tokens);
downloaded = true;
final Set<Geocache> result = searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB);
filter(result);
// update the caches
// first remove filtered out
final Set<String> filteredCodes = searchResult.getFilteredGeocodes();
Log.d("Filtering out " + filteredCodes.size() + " caches: " + filteredCodes.toString());
caches.removeAll(DataStore.loadCaches(filteredCodes, LoadFlags.LOAD_CACHE_ONLY));
DataStore.removeCaches(filteredCodes, EnumSet.of(RemoveFlag.CACHE));
// new collection type needs to remove first to refresh
caches.removeAll(result);
caches.addAll(result);
lastSearchResult = searchResult;
//render
displayExecutor.execute(new DisplayRunnable(this));
} finally {
showProgressHandler.sendEmptyMessage(HIDE_PROGRESS); // hide progress
}
}
/**
* Thread to Display (down)loaded caches. Started by {@link LoadRunnable} and {@link DownloadRunnable}
*/
private static class DisplayRunnable extends DoRunnable {
DisplayRunnable(@NonNull final CGeoMap map) {
super(map);
}
@Override
public void runWithMap(final CGeoMap map) {
map.doDisplayRun();
}
}
private void doDisplayRun() {
try {
showProgressHandler.sendEmptyMessage(SHOW_PROGRESS);
// display caches
final List<Geocache> cachesToDisplay = caches.getAsList();
final List<Waypoint> waypointsToDisplay = new ArrayList<>(waypoints);
final List<CachesOverlayItemImpl> itemsToDisplay = new ArrayList<>();
if (!cachesToDisplay.isEmpty()) {
// Only show waypoints for single view or setting
// when less than showWaypointsthreshold Caches shown
if (mapOptions.mapMode == MapMode.SINGLE || cachesCnt < Settings.getWayPointsThreshold()) {
for (final Waypoint waypoint : waypointsToDisplay) {
if (waypoint != null && waypoint.getCoords() != null) {
itemsToDisplay.add(getWaypointItem(waypoint));
}
}
}
for (final Geocache cache : cachesToDisplay) {
if (cache != null && cache.getCoords() != null) {
itemsToDisplay.add(getCacheItem(cache));
}
}
}
// don't add other waypoints to overlayCaches if just one point should be displayed
if (mapOptions.coords == null) {
overlayCaches.updateItems(itemsToDisplay);
}
displayHandler.sendEmptyMessage(INVALIDATE_MAP);
updateMapTitle();
} finally {
showProgressHandler.sendEmptyMessage(HIDE_PROGRESS);
}
}
private void displayPoint(final Geopoint coords) {
final Waypoint waypoint = new Waypoint("some place", mapOptions.waypointType != null ? mapOptions.waypointType : WaypointType.WAYPOINT, false);
waypoint.setCoords(coords);
final CachesOverlayItemImpl item = getWaypointItem(waypoint);
overlayCaches.updateItems(item);
displayHandler.sendEmptyMessage(INVALIDATE_MAP);
updateMapTitle();
cachesCnt = 1;
}
private void updateMapTitle() {
displayHandler.sendEmptyMessage(UPDATE_TITLE);
}
private abstract static class DoRunnable implements Runnable {
private final WeakReference<CGeoMap> mapRef;
protected DoRunnable(@NonNull final CGeoMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public final void run() {
final CGeoMap map = mapRef.get();
if (map != null) {
runWithMap(map);
}
}
protected abstract void runWithMap(final CGeoMap map);
}
/**
* store caches, invoked by "store offline" menu item
*
* @param listIds
* the lists to store the caches in
*/
private void storeCaches(final List<String> geocodes, final Set<Integer> listIds) {
final LoadDetailsHandler loadDetailsHandler = new LoadDetailsHandler(this);
waitDialog = new ProgressDialog(activity);
waitDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
waitDialog.setCancelable(true);
waitDialog.setCancelMessage(loadDetailsHandler.disposeMessage());
waitDialog.setMax(detailTotal);
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 = detailTotal * 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));
}
waitDialog.show();
detailProgressTime = System.currentTimeMillis();
loadDetailsThread = new LoadDetails(loadDetailsHandler, geocodes, listIds);
loadDetailsThread.start();
}
/**
* Thread to store the caches in the viewport. Started by Activity.
*/
private class LoadDetails extends Thread {
private final DisposableHandler handler;
private final List<String> geocodes;
private final Set<Integer> listIds;
LoadDetails(final DisposableHandler handler, final List<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 {
// one more cache over
detailProgress++;
handler.sendEmptyMessage(UPDATE_PROGRESS);
}
}
// we're done
handler.sendEmptyMessage(FINISHED_LOADING_DETAILS);
}
}
private static synchronized void filter(final Collection<Geocache> caches) {
final boolean excludeMine = Settings.isExcludeMyCaches();
final boolean excludeDisabled = Settings.isExcludeDisabledCaches();
final List<Geocache> removeList = new ArrayList<>();
for (final Geocache cache : caches) {
if ((excludeMine && (cache.isFound() || cache.isOwner())) || (excludeDisabled && (cache.isDisabled() || cache.isArchived()))) {
removeList.add(cache);
}
}
caches.removeAll(removeList);
}
private static boolean mapMoved(final Viewport referenceViewport, final Viewport newViewport) {
return Math.abs(newViewport.getLatitudeSpan() - referenceViewport.getLatitudeSpan()) > 50e-6 ||
Math.abs(newViewport.getLongitudeSpan() - referenceViewport.getLongitudeSpan()) > 50e-6 ||
Math.abs(newViewport.center.getLatitude() - referenceViewport.center.getLatitude()) > referenceViewport.getLatitudeSpan() / 4 ||
Math.abs(newViewport.center.getLongitude() - referenceViewport.center.getLongitude()) > referenceViewport.getLongitudeSpan() / 4;
}
// center map to desired location
private void centerMap(final Geopoint coords) {
if (coords == null) {
return;
}
final MapControllerImpl mapController = mapView.getMapController();
final GeoPointImpl target = makeGeoPoint(coords);
if (alreadyCentered) {
mapController.animateTo(target);
} else {
mapController.setCenter(target);
}
alreadyCentered = true;
}
// move map to view results of searchIntent
private void centerMap(final String geocodeCenter, final SearchResult searchCenter, final Geopoint coordsCenter, final MapState mapState) {
final MapControllerImpl mapController = mapView.getMapController();
if (!centered && mapState != null) {
try {
mapController.setCenter(mapItemFactory.getGeoPointBase(mapState.getCenter()));
setZoom(mapState.getZoomLevel());
} catch (final RuntimeException e) {
Log.e("centermap", e);
}
centered = true;
alreadyCentered = true;
} else if (!centered && (geocodeCenter != null || mapOptions.searchResult != null)) {
try {
Viewport viewport = null;
if (geocodeCenter != null) {
viewport = DataStore.getBounds(geocodeCenter);
} else if (searchCenter != null) {
viewport = DataStore.getBounds(searchCenter.getGeocodes());
}
if (viewport == null) {
return;
}
mapController.setCenter(mapItemFactory.getGeoPointBase(viewport.center));
if (viewport.getLatitudeSpan() != 0 && viewport.getLongitudeSpan() != 0) {
mapController.zoomToSpan((int) (viewport.getLatitudeSpan() * 1e6), (int) (viewport.getLongitudeSpan() * 1e6));
}
} catch (final RuntimeException e) {
Log.e("centermap", e);
}
centered = true;
alreadyCentered = true;
} else if (!centered && coordsCenter != null) {
try {
mapController.setCenter(makeGeoPoint(coordsCenter));
} catch (final Exception e) {
Log.e("centermap", e);
}
centered = true;
alreadyCentered = true;
}
}
// switch My Location button image
private void switchMyLocationButton() {
// FIXME: temporary workaround for the absence of "follow my location" on Android 3.x (see issue #4289).
if (myLocSwitch != null) {
myLocSwitch.setChecked(followMyLocation);
if (followMyLocation) {
myLocationInMiddle(Sensors.getInstance().currentGeo());
}
}
}
// set my location listener
private static class MyLocationListener implements View.OnClickListener {
private final WeakReference<CGeoMap> mapRef;
MyLocationListener(@NonNull final CGeoMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public void onClick(final View view) {
final CGeoMap map = mapRef.get();
if (map != null) {
map.onFollowMyLocationClicked();
}
}
}
private void onFollowMyLocationClicked() {
followMyLocation = !followMyLocation;
switchMyLocationButton();
}
public static class MapDragListener implements OnMapDragListener {
private final WeakReference<CGeoMap> mapRef;
public MapDragListener(@NonNull final CGeoMap map) {
mapRef = new WeakReference<>(map);
}
@Override
public void onDrag() {
final CGeoMap map = mapRef.get();
if (map != null) {
map.onDrag();
}
}
}
private void onDrag() {
if (followMyLocation) {
followMyLocation = false;
switchMyLocationButton();
}
}
// make geopoint
private GeoPointImpl makeGeoPoint(final Geopoint coords) {
return mapItemFactory.getGeoPointBase(coords);
}
@Override
public View makeView() {
final ImageView imageView = new ImageView(activity);
imageView.setScaleType(ScaleType.CENTER);
imageView.setLayoutParams(new ImageSwitcher.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
return imageView;
}
public static void markCacheAsDirty(final String geocode) {
synchronized (dirtyCaches) {
dirtyCaches.add(geocode);
}
}
private CachesOverlayItemImpl getCacheItem(final Geocache cache) {
final CachesOverlayItemImpl item = mapItemFactory.getCachesOverlayItem(cache, cache.applyDistanceRule());
item.setMarker(MapUtils.getCacheMarker(getResources(), cache));
return item;
}
private CachesOverlayItemImpl getWaypointItem(final Waypoint waypoint) {
final CachesOverlayItemImpl item = mapItemFactory.getCachesOverlayItem(waypoint, waypoint.getWaypointType().applyDistanceRule());
item.setMarker(MapUtils.getWaypointMarker(getResources(), waypoint));
return item;
}
}