org.mozilla.mozstumbler.client.mapview.MapFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.mozstumbler.client.mapview.MapFragment.java

Source

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.mozstumbler.client.mapview;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.location.Location;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;

import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.mozstumbler.BuildConfig;
import org.mozilla.mozstumbler.R;
import org.mozilla.mozstumbler.client.ClientPrefs;
import org.mozilla.mozstumbler.client.ClientStumblerService;
import org.mozilla.mozstumbler.client.MainApp;
import org.mozilla.mozstumbler.client.ObservedLocationsReceiver;
import org.mozilla.mozstumbler.client.mapview.tiles.AbstractMapOverlay;
import org.mozilla.mozstumbler.client.mapview.tiles.CoverageOverlay;
import org.mozilla.mozstumbler.client.mapview.tiles.LowResMapOverlay;
import org.mozilla.mozstumbler.client.navdrawer.MetricsView;
import org.mozilla.mozstumbler.service.AppGlobals;
import org.mozilla.mozstumbler.service.core.http.HttpUtil;
import org.mozilla.mozstumbler.service.core.http.IHttpUtil;
import org.mozilla.mozstumbler.service.stumblerthread.scanners.GPSScanner;
import org.mozilla.osmdroid.ResourceProxy;
import org.mozilla.osmdroid.api.IGeoPoint;
import org.mozilla.osmdroid.events.DelayedMapListener;
import org.mozilla.osmdroid.events.MapListener;
import org.mozilla.osmdroid.events.ScrollEvent;
import org.mozilla.osmdroid.events.ZoomEvent;
import org.mozilla.osmdroid.tileprovider.BitmapPool;
import org.mozilla.osmdroid.tileprovider.MapTile;
import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource;
import org.mozilla.osmdroid.tileprovider.tilesource.OnlineTileSourceBase;
import org.mozilla.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.mozilla.osmdroid.tileprovider.tilesource.XYTileSource;
import org.mozilla.osmdroid.util.GeoPoint;
import org.mozilla.osmdroid.views.MapView;
import org.mozilla.osmdroid.views.overlay.Overlay;

import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;

public final class MapFragment extends android.support.v4.app.Fragment
        implements MetricsView.IMapLayerToggleListener {

    public enum NoMapAvailableMessage {
        eHideNoMapMessage, eNoMapDueToNoAccessibleStorage, eNoMapDueToNoInternet
    }

    private static final String LOG_TAG = AppGlobals.LOG_PREFIX + MapFragment.class.getSimpleName();

    private static final String COVERAGE_REDIRECT_URL = "https://location.services.mozilla.com/map.json";
    private static int sGPSColor;
    private static final String ZOOM_KEY = "zoom";
    private static final int DEFAULT_ZOOM = 13;
    private static final int DEFAULT_ZOOM_AFTER_FIX = 16;
    private static final String LAT_KEY = "latitude";
    private static final String LON_KEY = "longitude";
    private static final int HIGH_ZOOM_THRESHOLD = 14;

    private MapView mMap;
    private AccuracyCircleOverlay mAccuracyOverlay;
    private boolean mFirstLocationFix;
    private boolean mUserPanning = false;
    private final Timer mGetUrl = new Timer();
    private ObservationPointsOverlay mObservationPointsOverlay;
    private GPSListener mGPSListener;
    private LowResMapOverlay mLowResMapOverlayHighZoom;
    private LowResMapOverlay mLowResMapOverlayLowZoom;
    private Overlay mCoverageTilesOverlayLowZoom;
    private Overlay mCoverageTilesOverlayHighZoom;
    private ITileSource mHighResMapSource;
    private View mRootView;
    private TextView mTextViewIsLowResMap;
    private HighLowBandwidthReceiver mHighLowBandwidthChecker;
    private CoverageSetup mCoverageSetup = new CoverageSetup();

    // Used to blank the high-res tile source when adding a low-res overlay
    private class BlankTileSource extends OnlineTileSourceBase {
        BlankTileSource() {
            super("fake", ResourceProxy.string.mapquest_aerial /* arbitrary value */,
                    AbstractMapOverlay.getDisplaySizeBasedMinZoomLevel(), AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP,
                    AbstractMapOverlay.TILE_PIXEL_SIZE, "", new String[] { "" });
        }

        @Override
        public String getTileURLString(MapTile aTile) {
            return null;
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        mRootView = inflater.inflate(R.layout.activity_map, container, false);

        AbstractMapOverlay.setDisplayBasedMinimumZoomLevel(getApplication());

        showMapNotAvailableMessage(NoMapAvailableMessage.eHideNoMapMessage);

        final ImageButton centerMe = (ImageButton) mRootView.findViewById(R.id.my_location_button);
        centerMe.setVisibility(View.INVISIBLE);
        centerMe.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mAccuracyOverlay == null || mAccuracyOverlay.getLocation() == null)
                    return;
                mMap.getController().animateTo((mAccuracyOverlay.getLocation()));
                mUserPanning = false;
            }
        });

        centerMe.setOnTouchListener(new View.OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    centerMe.setBackgroundResource(R.drawable.ic_mylocation_click_android_assets);

                } else if (event.getAction() == MotionEvent.ACTION_UP) {
                    v.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            centerMe.setBackgroundResource(R.drawable.ic_mylocation_android_assets);
                        }
                    }, 200);
                }
                return false;
            }
        });

        mTextViewIsLowResMap = (TextView) mRootView.findViewById(R.id.low_resolution_map_message);
        mTextViewIsLowResMap.setVisibility(View.GONE);

        mMap = (MapView) mRootView.findViewById(R.id.map);
        mMap.setBuiltInZoomControls(true);
        mMap.setMultiTouchControls(true);

        listenForPanning(mMap);

        sGPSColor = getResources().getColor(R.color.gps_track);

        mFirstLocationFix = true;
        int zoomLevel = DEFAULT_ZOOM;
        GeoPoint lastLoc = null;
        if (savedInstanceState != null) {
            mFirstLocationFix = false;
            zoomLevel = savedInstanceState.getInt(ZOOM_KEY, DEFAULT_ZOOM);
            if (savedInstanceState.containsKey(LAT_KEY) && savedInstanceState.containsKey(LON_KEY)) {
                final double latitude = savedInstanceState.getDouble(LAT_KEY);
                final double longitude = savedInstanceState.getDouble(LON_KEY);
                lastLoc = new GeoPoint(latitude, longitude);
            }
        } else {
            lastLoc = ClientPrefs.getInstance().getLastMapCenter();
            if (lastLoc != null) {
                zoomLevel = DEFAULT_ZOOM_AFTER_FIX;
            }
        }

        final GeoPoint loc = lastLoc;
        final int zoom = zoomLevel;
        mMap.getController().setZoom(zoom);
        mMap.getController().setCenter(loc);
        mMap.setMinZoomLevel(AbstractMapOverlay.getDisplaySizeBasedMinZoomLevel());

        mMap.postDelayed(new Runnable() {
            @Override
            public void run() {
                // https://github.com/osmdroid/osmdroid/issues/22
                // These need a fully constructed map, which on first load seems to take a while.
                // Post with no delay does not work for me, adding an arbitrary
                // delay of 300 ms should be plenty.
                Log.d(LOG_TAG, "ZOOM " + zoom);
                mMap.getController().setZoom(zoom);
                mMap.getController().setCenter(loc);
            }
        }, 300);

        Log.d(LOG_TAG, "onCreate");

        mAccuracyOverlay = new AccuracyCircleOverlay(mRootView.getContext(), sGPSColor);
        mMap.getOverlays().add(mAccuracyOverlay);

        mObservationPointsOverlay = new ObservationPointsOverlay(mRootView.getContext());
        mMap.getOverlays().add(mObservationPointsOverlay);

        ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance();
        observer.setMapActivity(this);

        initTextView(R.id.text_cells_visible, "000");
        initTextView(R.id.text_wifis_visible, "000");
        initTextView(R.id.text_observation_count, "00000");

        showCopyright();

        mMap.setMapListener(new DelayedMapListener(new MapListener() {
            @Override
            public boolean onZoom(final ZoomEvent e) {
                // This is key to no-wifi (low-res) tile functions, that
                // when the zoom level changes, we check to see if the
                // low-zoom or high-zoom should be shown
                int z = e.getZoomLevel();
                updateOverlayBaseLayer(z);
                updateOverlayCoverageLayer(z);
                mObservationPointsOverlay.zoomChanged(mMap);
                return true;
            }

            @Override
            public boolean onScroll(final ScrollEvent e) {
                return true;
            }
        }, 0));

        return mRootView;
    }

    MainApp getApplication() {
        return (MainApp) getActivity().getApplication();
    }

    private static String sCoverageUrl; // Only used by CoverageSetup

    private class CoverageSetup {
        private AtomicBoolean isGetUrlAndInitCoverageRunning = new AtomicBoolean();

        private void initOnMainThread() {
            final Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    if (mCoverageTilesOverlayLowZoom != null || // checks if init() has already happened
                    ClientPrefs.getInstance()
                            .getMapTileResolutionType() == ClientPrefs.MapTileResolutionOptions.NoMap) {
                        return;
                    }
                    initCoverageTiles(sCoverageUrl);
                    updateOverlayCoverageLayer(mMap.getZoomLevel());
                }
            };
            mMap.post(runnable);
        }

        void getUrlAndInit() {
            if (isGetUrlAndInitCoverageRunning.get() || mCoverageTilesOverlayLowZoom != null) {
                return;
            }
            isGetUrlAndInitCoverageRunning.set(true);

            final Runnable coverageUrlQuery = new Runnable() {
                @Override
                public void run() {
                    if (sCoverageUrl != null) {
                        initOnMainThread();
                        isGetUrlAndInitCoverageRunning.set(false);
                        return;
                    }

                    mGetUrl.schedule(new TimerTask() {
                        @Override
                        public void run() {
                            IHttpUtil httpUtil = new HttpUtil();

                            java.util.Scanner scanner;
                            try {
                                scanner = new java.util.Scanner(httpUtil.getUrlAsStream(COVERAGE_REDIRECT_URL),
                                        "UTF-8");
                            } catch (Exception ex) {
                                if (AppGlobals.isDebug) {
                                    Log.d(LOG_TAG, ex.toString());
                                }
                                isGetUrlAndInitCoverageRunning.set(false);
                                return;
                            }
                            scanner.useDelimiter("\\A");
                            String result = scanner.next();
                            try {
                                sCoverageUrl = new JSONObject(result).getString("tiles_url");
                            } catch (JSONException ex) {
                                AppGlobals.guiLogInfo("Failed to get coverage url:" + ex.toString());
                            }
                            scanner.close();
                            initOnMainThread();
                            isGetUrlAndInitCoverageRunning.set(false);
                        }
                    }, 0);
                }
            };

            mMap.post(coverageUrlQuery);
        }
    }

    private void initCoverageTiles(String coverageUrl) {
        Log.i(LOG_TAG, "initCoverageTiles: " + coverageUrl);
        mCoverageTilesOverlayLowZoom = new CoverageOverlay(CoverageOverlay.LowResType.LOWER_ZOOM,
                mRootView.getContext(), coverageUrl, mMap);
        mCoverageTilesOverlayHighZoom = new CoverageOverlay(CoverageOverlay.LowResType.HIGHER_ZOOM,
                mRootView.getContext(), coverageUrl, mMap);
    }

    //
    // This determines which level of detail of tile layer is shown.
    //
    private boolean isHighZoom(int zoomLevel) {
        return zoomLevel > HIGH_ZOOM_THRESHOLD;
    }

    // If the map is not in low res mode, return.
    // Otherwise, set the low res overlay based on the current zoom level, and set it to a
    // lower resolution than the zoom level of the map (trick it into showing lower resolution
    // tiles than the map normally would at a given zoom level)
    private void updateOverlayBaseLayer(int zoomLevel) {
        if (mLowResMapOverlayHighZoom == null || mLowResMapOverlayLowZoom == null) {
            return;
        }
        final List<Overlay> overlays = mMap.getOverlays();
        final Overlay overlayRemoved = (!isHighZoom(zoomLevel)) ? mLowResMapOverlayHighZoom
                : mLowResMapOverlayLowZoom;
        final Overlay overlayAdded = (isHighZoom(zoomLevel)) ? mLowResMapOverlayHighZoom : mLowResMapOverlayLowZoom;
        if (overlays.indexOf(overlayRemoved) > -1) {
            overlays.remove(overlayRemoved);
        }
        if (overlays.indexOf(overlayAdded) == -1) {
            overlays.add(0, overlayAdded);
            mMap.invalidate();
        }
    }

    // The MLS coverage follows the same logic as the lower resolution map overlay, in that
    // when at low zoom level, show even lower resolution tiles. The MLS coverage is not
    // dependant on the isHighBandwidth for this behaviour, it always does this.
    private void updateOverlayCoverageLayer(int zoomLevel) {
        if (mCoverageTilesOverlayLowZoom == null || mCoverageTilesOverlayHighZoom == null) {
            return;
        }

        final List<Overlay> overlays = mMap.getOverlays();
        int idx = 0;
        if (overlays.indexOf(mLowResMapOverlayHighZoom) > -1 || overlays.indexOf(mLowResMapOverlayLowZoom) > -1) {
            idx = 1;
        }

        final Overlay overlayRemoved = (!isHighZoom(zoomLevel)) ? mCoverageTilesOverlayHighZoom
                : mCoverageTilesOverlayLowZoom;
        final Overlay overlayAdded = (isHighZoom(zoomLevel)) ? mCoverageTilesOverlayHighZoom
                : mCoverageTilesOverlayLowZoom;
        if (overlays.indexOf(overlayRemoved) > -1) {
            overlays.remove(overlayRemoved);
        }
        if (overlays.indexOf(overlayAdded) == -1) {
            overlays.add(idx, overlayAdded);
            mMap.invalidate();
        }
    }

    // Unfortunately, just showing low/high detail isn't enough data reduction.
    // To handle the case where the user zooms out to show a large area when in low bandwidth mode,
    // we need an additional "LowZoom" overlay. So in low bandwidth mode, you will see
    // that based on the current zoom level of the map, we show "HighZoom" or "LowZoom" overlays.
    private void setHighBandwidthMap(boolean isHighBandwidth) {
        final ClientPrefs prefs = ClientPrefs.getInstance();
        if (prefs == null || getActivity() == null) {
            return;
        }

        final ClientPrefs.MapTileResolutionOptions tileType = prefs.getMapTileResolutionType();
        final int idxTileType = tileType.ordinal();
        if (idxTileType > 0) {
            if (tileType == ClientPrefs.MapTileResolutionOptions.NoMap) {
                mTextViewIsLowResMap.setVisibility(View.VISIBLE);
                mTextViewIsLowResMap.setText(getActivity().getString(R.string.map_turned_off));
                mMap.setTileSource(new BlankTileSource());
                removeLayer(mLowResMapOverlayLowZoom);
                removeLayer(mLowResMapOverlayHighZoom);
                removeLayer(mCoverageTilesOverlayLowZoom);
                removeLayer(mCoverageTilesOverlayHighZoom);
                mLowResMapOverlayHighZoom = mLowResMapOverlayLowZoom = null;
                mCoverageTilesOverlayHighZoom = mCoverageTilesOverlayLowZoom = null;
                return;
            } else if (tileType == ClientPrefs.MapTileResolutionOptions.HighRes) {
                isHighBandwidth = true;
            } else if (tileType == ClientPrefs.MapTileResolutionOptions.LowRes) {
                isHighBandwidth = false;
            }
        }

        final boolean isMLSTileStore = (BuildConfig.TILE_SERVER_URL != null);

        if (idxTileType == 0) {
            if (isHighBandwidth) {
                mTextViewIsLowResMap.setVisibility(View.GONE);
            } else {
                mTextViewIsLowResMap.setText(getActivity().getString(R.string.low_resolution_map));
                mTextViewIsLowResMap.setVisibility(View.VISIBLE);
            }
        } else {
            String[] labels = getActivity().getResources().getStringArray(R.array.map_tile_resolution_options);
            mTextViewIsLowResMap.setText(labels[idxTileType]);
            mTextViewIsLowResMap.setVisibility(View.VISIBLE);
        }

        if (isHighBandwidth) {
            if (mLowResMapOverlayHighZoom == null && mMap.getTileProvider().getTileSource() == mHighResMapSource) {
                // already have set this tile source
                return;
            } else {
                if (mLowResMapOverlayHighZoom != null) {
                    removeLayer(mLowResMapOverlayHighZoom);
                    removeLayer(mLowResMapOverlayLowZoom);

                    mLowResMapOverlayLowZoom = null;
                    mLowResMapOverlayHighZoom = null;
                }
            }

            // We've destroyed 2 layers for lowResMapOverlay
            // Force GC to cleanup underlying LRU caches in overlay
            System.gc();

            if (!isMLSTileStore) {
                mHighResMapSource = TileSourceFactory.MAPQUESTOSM;
            } else {
                mHighResMapSource = new XYTileSource("Stumbler-BaseMap-Tiles", null, 1,
                        AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP, AbstractMapOverlay.TILE_PIXEL_SIZE,
                        AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG, new String[] { BuildConfig.TILE_SERVER_URL });
            }
            System.gc();
            mMap.setTileSource(mHighResMapSource);
        } else {
            if (mLowResMapOverlayHighZoom != null) {
                // already set this
                return;
            }

            // Unhooking the highres map means we should nullify it and force GC
            // to cleanup underlying LRU cache in MapSource
            mHighResMapSource = null;
            System.gc();

            mMap.setTileSource(new BlankTileSource());
            if (mLowResMapOverlayHighZoom == null) {
                mLowResMapOverlayLowZoom = new LowResMapOverlay(LowResMapOverlay.LowResType.LOWER_ZOOM,
                        this.getActivity(), isMLSTileStore, mMap);
                mLowResMapOverlayHighZoom = new LowResMapOverlay(LowResMapOverlay.LowResType.HIGHER_ZOOM,
                        this.getActivity(), isMLSTileStore, mMap);

                updateOverlayBaseLayer(mMap.getZoomLevel());
            }

        }
    }

    public void mapNetworkConnectionChanged() {

        FragmentActivity activity = getActivity();

        if (activity == null) {
            return;
        }

        if (activity.getFilesDir() == null) {
            // Not the ideal spot for this check perhaps, but there is no point in checking
            // the network when storage is not available.
            showMapNotAvailableMessage(NoMapAvailableMessage.eNoMapDueToNoAccessibleStorage);
            return;
        }

        mCoverageSetup.getUrlAndInit();

        final ConnectivityManager cm = (ConnectivityManager) getActivity()
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        final NetworkInfo info = cm.getActiveNetworkInfo();
        final boolean hasNetwork = (info != null) && info.isConnected();
        final boolean hasWifi = (info != null) && (info.getType() == ConnectivityManager.TYPE_WIFI);

        if (!hasNetwork) {
            showMapNotAvailableMessage(NoMapAvailableMessage.eNoMapDueToNoInternet);
            return;
        }

        showMapNotAvailableMessage(NoMapAvailableMessage.eHideNoMapMessage);
        setHighBandwidthMap(hasWifi);
    }

    @SuppressLint("NewApi")
    public void dimToolbar() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return;
        }
        View v = mRootView.findViewById(R.id.status_toolbar_layout);

        final ClientStumblerService service = getApplication().getService();
        float alpha = 0.5f;
        if (service != null && service.isScanning()) {
            alpha = 1.0f;
        }
        v.setAlpha(alpha);
    }

    public void toggleScanning(MenuItem menuItem) {
        MainApp app = getApplication();
        if (app.getService() == null) {
            return;
        }

        boolean isScanning = app.getService().isScanning();
        if (isScanning) {
            app.stopScanning();
        } else {
            app.startScanning();
        }

        dimToolbar();
    }

    private void showCopyright() {
        TextView copyrightArea = (TextView) mRootView.findViewById(R.id.copyright_area);
        if (BuildConfig.TILE_SERVER_URL == null) {
            copyrightArea.setText("Tiles Courtesy of MapQuest\n OpenStreetMap contributors");
        } else {
            copyrightArea.setText(" MapBox  OpenStreetMap contributors");
        }
    }

    void setUserPositionAt(Location location) {
        if (mAccuracyOverlay.getLocation() == null) {
            ImageButton centerMe = (ImageButton) mRootView.findViewById(R.id.my_location_button);
            centerMe.setVisibility(View.VISIBLE);
        }

        mAccuracyOverlay.setLocation(location);

        if (mFirstLocationFix) {
            mMap.getController().setZoom(DEFAULT_ZOOM_AFTER_FIX);
            mFirstLocationFix = false;
            mMap.getController().setCenter(new GeoPoint(location));
            mUserPanning = false;
        } else if (!mUserPanning) {
            mMap.getController().animateTo((mAccuracyOverlay.getLocation()));
        }

    }

    void updateGPSInfo(int satellites, int fixes) {
        formatTextView(R.id.text_satellites_avail, "%d", satellites);
        formatTextView(R.id.text_satellites_used, "%d", fixes);
        // @TODO this is still not accurate
        int icon = fixes >= GPSScanner.MIN_SAT_USED_IN_FIX ? R.drawable.ic_gps_receiving_flaticondotcom
                : R.drawable.ic_gps_no_signal_flaticondotcom;
        ((ImageView) mRootView.findViewById(R.id.fix_indicator)).setImageResource(icon);
    }

    // An overlay for the sole purpose of reporting a user swiping on the map
    private static class SwipeListeningOverlay extends Overlay {
        private static interface OnSwipeListener {
            public void onSwipe();
        }

        final OnSwipeListener mOnSwipe;

        SwipeListeningOverlay(Context ctx, OnSwipeListener onSwipe) {
            super(ctx);
            mOnSwipe = onSwipe;
        }

        @Override
        protected void draw(Canvas c, MapView osmv, boolean shadow) {
            // Nothing to draw
        }

        @Override
        public boolean onTouchEvent(final MotionEvent event, final MapView mapView) {
            if (mOnSwipe != null && event.getAction() == MotionEvent.ACTION_MOVE) {
                mOnSwipe.onSwipe();
            }
            return false;
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(LOG_TAG, "onResume");

        mGPSListener = new GPSListener(this);

        ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance();
        observer.setMapActivity(this);

        dimToolbar();

        mapNetworkConnectionChanged();

        mHighLowBandwidthChecker = new HighLowBandwidthReceiver(this);

        ClientPrefs prefs = ClientPrefs.createGlobalInstance(getActivity().getApplicationContext());
        setShowMLS(prefs.getOnMapShowMLS());

        mObservationPointsOverlay.zoomChanged(mMap);
        mMap.postInvalidate();
    }

    private void saveStateToPrefs() {
        IGeoPoint center = mMap.getMapCenter();
        ClientPrefs.getInstance().setLastMapCenter(center);
    }

    @Override
    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        bundle.putInt(ZOOM_KEY, mMap.getZoomLevel());
        IGeoPoint center = mMap.getMapCenter();
        bundle.putDouble(LON_KEY, center.getLongitude());
        bundle.putDouble(LAT_KEY, center.getLatitude());
        saveStateToPrefs();
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(LOG_TAG, "onPause");
        saveStateToPrefs();

        mGPSListener.removeListener();
        ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance();
        observer.removeMapActivity();
        mHighLowBandwidthChecker.unregister(this.getApplication());
    }

    private void removeLayer(Overlay layer) {
        if (layer == null) {
            return;
        }
        mMap.getOverlays().remove(layer);
        layer.onDetach(mMap);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(LOG_TAG, "onDestroy");

        removeLayer(mLowResMapOverlayHighZoom);
        removeLayer(mLowResMapOverlayLowZoom);
        removeLayer(mCoverageTilesOverlayHighZoom);
        removeLayer(mCoverageTilesOverlayLowZoom);

        mMap.getTileProvider().clearTileCache();
        BitmapPool.getInstance().clearBitmapPool();
    }

    private void listenForPanning(MapView map) {
        map.getOverlays().add(new SwipeListeningOverlay(getActivity().getApplicationContext(),
                new SwipeListeningOverlay.OnSwipeListener() {
                    @Override
                    public void onSwipe() {
                        mUserPanning = true;
                    }
                }));
    }

    public void formatTextView(int textViewId, int stringId, Object... args) {
        String str = getResources().getString(stringId);
        formatTextView(textViewId, str, args);
    }

    public void formatTextView(int textViewId, String str, Object... args) {
        TextView textView = (TextView) mRootView.findViewById(textViewId);
        str = String.format(str, args);
        textView.setText(str);
    }

    private void initTextView(int textViewId, String bound) {
        TextView textView = (TextView) mRootView.findViewById(textViewId);
        Paint textPaint = textView.getPaint();
        int width = (int) Math.ceil(textPaint.measureText(bound));
        textView.setWidth(width);
        android.widget.LinearLayout.LayoutParams params = new android.widget.LinearLayout.LayoutParams(width,
                android.widget.LinearLayout.LayoutParams.MATCH_PARENT);
        textView.setLayoutParams(params);
        textView.setText("0");
    }

    public void newMLSPoint(ObservationPoint point) {
        mObservationPointsOverlay.update(point, mMap, true);
    }

    public void newObservationPoint(ObservationPoint point) {
        mObservationPointsOverlay.update(point, mMap, false);
    }

    @Override
    public void setShowMLS(boolean isOn) {
        mObservationPointsOverlay.mOnMapShowMLS = isOn;
        mMap.invalidate();
    }

    public void showMapNotAvailableMessage(NoMapAvailableMessage noMapAvailableMessage) {
        TextView noMapMessage = (TextView) mRootView.findViewById(R.id.message_area);
        if (noMapAvailableMessage == NoMapAvailableMessage.eHideNoMapMessage) {
            noMapMessage.setVisibility(View.INVISIBLE);
        } else {
            noMapMessage.setVisibility(View.VISIBLE);
            int resId = (noMapAvailableMessage == NoMapAvailableMessage.eNoMapDueToNoInternet)
                    ? R.string.map_offline_mode
                    : R.string.map_unavailable;
            noMapMessage.setText(resId);
        }
    }

}