com.androzic.Androzic.java Source code

Java tutorial

Introduction

Here is the source code for com.androzic.Androzic.java

Source

/*
 * Androzic - android navigation client that uses OziExplorer maps (ozf2, ozfx3).
 * Copyright (C) 2010-2013  Andrey Novikov <http://andreynovikov.info/>
 *
 * This file is part of Androzic application.
 *
 * Androzic is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * Androzic is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with Androzic.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.androzic;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.GeomagneticField;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import android.widget.Toast;

import com.androzic.data.Bounds;
import com.androzic.data.MapObject;
import com.androzic.data.Route;
import com.androzic.data.Track;
import com.androzic.data.Waypoint;
import com.androzic.data.WaypointSet;
import com.androzic.location.ILocationListener;
import com.androzic.location.ILocationService;
import com.androzic.location.LocationService;
import com.androzic.map.BaseMap;
import com.androzic.map.ozf.OzfMap;
import com.androzic.map.MapIndex;
import com.androzic.map.MockMap;
import com.androzic.map.ozf.OzfDecoder;
import com.androzic.map.forge.ForgeMap;
import com.androzic.map.online.OnlineMap;
import com.androzic.map.online.OpenStreetMapTileProvider;
import com.androzic.map.online.TileFactory;
import com.androzic.map.online.TileProvider;
import com.androzic.map.online.TileProviderFactory;
import com.androzic.navigation.NavigationService;
import com.androzic.overlay.NavigationOverlay;
import com.androzic.overlay.OverlayManager;
import com.androzic.overlay.RouteOverlay;
import com.androzic.overlay.TrackOverlay;
import com.androzic.ui.TooltipManager;
import com.androzic.ui.Viewport;
import com.androzic.util.Astro.Zenith;
import com.androzic.util.CSV;
import com.androzic.util.CoordinateParser;
import com.androzic.util.FileUtils;
import com.androzic.util.Geo;
import com.androzic.util.OziExplorerFiles;
import com.androzic.util.StringFormatter;
import com.androzic.util.WaypointFileHelper;
import com.androzic.vnspeech.WordManager;
import com.jhlabs.map.proj.ProjectionException;

import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.graphics.AndroidSvgBitmapStore;
import org.mapsforge.map.android.rendertheme.BufferedAssetsRenderTheme;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
import org.mapsforge.map.rendertheme.XmlRenderTheme;
import org.mapsforge.map.rendertheme.XmlRenderThemeMenuCallback;
import org.mapsforge.map.rendertheme.XmlRenderThemeStyleLayer;
import org.mapsforge.map.rendertheme.XmlRenderThemeStyleMenu;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Androzic extends BaseApplication
        implements OnSharedPreferenceChangeListener, XmlRenderThemeMenuCallback {
    private static final String TAG = "Androzic";

    public static final String BROADCAST_WAYPOINT_ADDED = "com.androzic.waypointAdded";
    public static final String BROADCAST_WAYPOINT_REMOVED = "com.androzic.waypointRemoved";

    public static final int PATH_DATA = 0x001;
    public static final int PATH_ICONS = 0x008;
    public static final int PATH_MARKERICONS = 0x010;

    public boolean angleMagnetic = false;
    public int sunriseType = 0;

    private boolean initialized = false;
    private List<TileProvider> onlineMaps;
    private MapIndex maps;
    private List<BaseMap> suitableMaps;
    private List<BaseMap> coveringMaps;
    private BaseMap currentMap;
    /**
     * Indicates whether current map covers all screen or not
     */
    private boolean coveredAll;
    /**
     * Indicates whether current map should be covered by better map
     */
    private boolean coveringBestMap;
    private double[] coveringLoc = new double[] { 0.0, 0.0 };
    private Rectangle coveringScreen = new Rectangle();
    private boolean invalidCoveringMaps = true;
    private double[] mapCenter = new double[] { 0.0, 0.0 };
    private double[] location = new double[] { Double.NaN, Double.NaN };
    private double magneticDeclination = 0;

    private ILocationService locationService = null;
    private int magInterval;
    private long lastMagnetic = 0;

    public NavigationService navigationService = null;

    public Location lastKnownLocation;
    public boolean gpsEnabled;
    public int gpsStatus;
    public int gpsFSats;
    public int gpsTSats;
    public boolean gpsContinous;
    public boolean gpsGeoid;
    public boolean shouldEnableFollowing;

    @SuppressLint("UseSparseArrays")
    private final AbstractMap<Long, MapObject> mapObjects = new HashMap<>();
    private final List<Waypoint> waypoints = new ArrayList<>();
    private final List<WaypointSet> waypointSets = new ArrayList<>();
    private WaypointSet defWaypointSet;
    private final List<Track> tracks = new ArrayList<>();
    private final List<Route> routes = new ArrayList<>();

    // Map activity state
    protected Waypoint undoWaypoint = null;
    public Route editingRoute = null;
    public Track editingTrack = null;
    public Stack<Waypoint> routeEditingWaypoints = null;

    // Plugins
    private AbstractMap<String, Intent> pluginPreferences = new HashMap<>();
    private AbstractMap<String, Pair<Drawable, Intent>> pluginViews = new HashMap<>();

    // Mapsforge vector maps style
    public XmlRenderTheme xmlRenderTheme;
    XmlRenderThemeStyleMenu xmlRenderThemeStyleMenu;

    private boolean memmsg = false;

    private Locale locale = null;
    public String charset;

    public String dataPath;
    private String rootPath;
    private String mapPath;
    private String sasPath;
    public String iconPath;
    public String markerPath;
    private File cacheDir;
    public boolean mapsInited = false;
    private MapHolder mapHolder;
    public OverlayManager overlayManager;
    public Drawable customCursor = null;
    public boolean iconsEnabled = false;
    public int iconX = 0;
    public int iconY = 0;
    private int onlineMapPrescaleFactor;
    private int onlineMapTileExpiration;

    public boolean isPaid = false;

    protected boolean adjacentMaps = false;
    protected boolean cropMapBorder = true;
    protected boolean drawMapBorder = false;

    private HandlerThread renderingThread;
    private HandlerThread longOperationsThread;
    private Handler mapsHandler;
    private Handler uiHandler;

    public Handler getUIHandler() {
        return uiHandler;
    }

    public Looper getRenderingThreadLooper() {
        //return Looper.getMainLooper();
        return renderingThread.getLooper();
    }

    public Looper getLongOperationsThreadLooper() {
        return longOperationsThread.getLooper();
    }

    public MapHolder getMapHolder() {
        return mapHolder;
    }

    public void setMapHolder(MapHolder holder) {
        mapHolder = holder;
        if (currentMap != null && !currentMap.activated()) {
            try {
                currentMap.activate(mapHolder, currentMap.getAbsoluteMPP(), true);
            } catch (final Throwable e) {
                e.printStackTrace();
                uiHandler.post(new MapActivationError(currentMap, e));
            }
        }

        if (onlineMaps != null) {
            for (TileProvider map : onlineMaps) {
                if (map.instance != null)
                    map.listener = mapHolder;
            }
        }

        if (currentMap != null && currentMap instanceof OzfMap)
            overlayManager.initGrids((OzfMap) currentMap);
    }

    public void updateViewportDimensions(int width, int height) {
        BaseMap.viewportWidth = width;
        BaseMap.viewportHeight = height;
        if (currentMap != null && currentMap.activated())
            currentMap.recalculateCache();
    }

    public java.util.Map<String, Intent> getPluginsPreferences() {
        return pluginPreferences;
    }

    public java.util.Map<String, Pair<Drawable, Intent>> getPluginsViews() {
        return pluginViews;
    }

    public Zenith getZenith() {
        switch (sunriseType) {
        case 0:
            return Zenith.OFFICIAL;
        case 1:
            return Zenith.CIVIL;
        case 2:
            return Zenith.NAUTICAL;
        case 3:
            return Zenith.ASTRONOMICAL;
        default:
            return Zenith.OFFICIAL;
        }
    }

    public long getNewUID() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        long uid = preferences.getLong(getString(R.string.app_lastuid), 0);
        uid++;
        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putLong(getString(R.string.app_lastuid), uid);
        editor.commit();
        return uid;
    }

    public long addMapObject(MapObject mapObject) {
        mapObject._id = getNewUID();
        synchronized (mapObjects) {
            mapObjects.put(mapObject._id, mapObject);
        }
        if (mapHolder != null)
            mapHolder.refreshMap();
        return mapObject._id;
    }

    public boolean removeMapObject(long id) {
        synchronized (mapObjects) {
            MapObject mo = mapObjects.remove(id);
            if (mo != null && mo.bitmap != null)
                mo.bitmap.recycle();
            if (mapHolder != null)
                mapHolder.refreshMap();
            return mo != null;
        }
    }

    public void onUpdateMapObject(MapObject mapObject) {
        mapObject.drawImage = false;
        overlayManager.onMapObjectsChanged();
        if (mapHolder != null)
            mapHolder.refreshMap();
    }

    /**
     * Clear all map objects.
     */
    public void clearMapObjects() {
        synchronized (mapObjects) {
            mapObjects.clear();
        }
        if (mapHolder != null)
            mapHolder.refreshMap();
    }

    public MapObject getMapObject(long id) {
        return mapObjects.get(id);
    }

    public Iterable<MapObject> getMapObjects() {
        return mapObjects.values();
    }

    public int addWaypoint(final Waypoint newWaypoint) {
        if (newWaypoint.set == null)
            newWaypoint.set = defWaypointSet;
        synchronized (waypoints) {
            waypoints.add(newWaypoint);
        }
        sendBroadcast(new Intent(Androzic.BROADCAST_WAYPOINT_ADDED));
        return waypoints.lastIndexOf(newWaypoint);
    }

    public int addWaypoints(final List<Waypoint> newWaypoints) {
        if (newWaypoints != null) {
            for (Waypoint waypoint : newWaypoints)
                waypoint.set = defWaypointSet;
            synchronized (waypoints) {
                waypoints.addAll(newWaypoints);
            }
        }
        sendBroadcast(new Intent(Androzic.BROADCAST_WAYPOINT_ADDED));
        return waypoints.size() - 1;
    }

    public int addWaypoints(final List<Waypoint> newWaypoints, final WaypointSet waypointSet) {
        if (newWaypoints != null) {
            for (Waypoint waypoint : newWaypoints)
                waypoint.set = waypointSet;
            synchronized (waypoints) {
                waypoints.addAll(newWaypoints);
            }
            waypointSets.add(waypointSet);
        }
        sendBroadcast(new Intent(Androzic.BROADCAST_WAYPOINT_ADDED));
        return waypoints.size() - 1;
    }

    public boolean removeWaypoint(final Waypoint delWaypoint) {
        boolean removed;
        synchronized (waypoints) {
            removed = waypoints.remove(delWaypoint);
        }
        if (removed)
            sendBroadcast(new Intent(Androzic.BROADCAST_WAYPOINT_REMOVED));
        return removed;
    }

    public void removeWaypoint(final int delWaypoint) {
        synchronized (waypoints) {
            waypoints.remove(delWaypoint);
        }
        sendBroadcast(new Intent(Androzic.BROADCAST_WAYPOINT_REMOVED));
    }

    /**
     * Clear all waypoints.
     */
    public void clearWaypoints() {
        synchronized (waypoints) {
            waypoints.clear();
        }
    }

    // TODO Should we keep it? Not used anymore...
    /**
     * Clear waypoints from specific waypoint set.
     * @param set waypoint set
     */
    public void clearWaypoints(WaypointSet set) {
        for (Iterator<Waypoint> iter = waypoints.iterator(); iter.hasNext();) {
            Waypoint wpt = iter.next();
            if (wpt.set == set) {
                iter.remove();
            }
        }
    }

    /**
     * Clear waypoints from default waypoint set.
     */
    public void clearDefaultWaypoints() {
        clearWaypoints(defWaypointSet);
    }

    public Waypoint getWaypoint(final int index) {
        return waypoints.get(index);
    }

    public int getWaypointIndex(Waypoint wpt) {
        return waypoints.indexOf(wpt);
    }

    public List<Waypoint> getWaypoints() {
        return waypoints;
    }

    public List<Waypoint> getWaypoints(WaypointSet set) {
        List<Waypoint> wpts = new ArrayList<Waypoint>();
        synchronized (waypoints) {
            for (Waypoint wpt : waypoints) {
                if (wpt.set == set) {
                    wpts.add(wpt);
                }
            }
        }
        return wpts;
    }

    public List<Waypoint> getDefaultWaypoints() {
        return getWaypoints(defWaypointSet);
    }

    public boolean hasWaypoints() {
        return waypoints.size() > 0;
    }

    public void saveWaypoints(WaypointSet set) {
        try {
            if (set.path == null)
                set.path = dataPath + File.separator + FileUtils.sanitizeFilename(set.name) + ".wpt";
            File file = new File(set.path);
            File dir = file.getParentFile();
            if (!dir.exists())
                dir.mkdirs();
            if (!file.exists())
                file.createNewFile();
            if (file.canWrite())
                OziExplorerFiles.saveWaypointsToFile(file, charset, getWaypoints(set));
        } catch (Exception e) {
            Toast.makeText(this, getString(R.string.err_write), Toast.LENGTH_LONG).show();
            Log.e("ANDROZIC", e.toString(), e);
        }
    }

    public void saveWaypoints() {
        for (WaypointSet wptset : waypointSets) {
            saveWaypoints(wptset);
        }
        overlayManager.onWaypointsChanged();
    }

    public void saveDefaultWaypoints() {
        saveWaypoints(defWaypointSet);
    }

    public boolean ensureVisible(MapObject waypoint) {
        return ensureVisible(waypoint.latitude, waypoint.longitude);
    }

    public boolean ensureVisible(double lat, double lon) {
        if (mapHolder != null)
            mapHolder.setFollowing(false);
        boolean mapChanged = setMapCenter(lat, lon, true, true, false);
        if (!mapChanged && mapHolder != null)
            mapHolder.conditionsChanged();
        return mapChanged;
    }

    public int addWaypointSet(final WaypointSet newWaypointSet) {
        waypointSets.add(newWaypointSet);
        return waypointSets.lastIndexOf(newWaypointSet);
    }

    public List<WaypointSet> getWaypointSets() {
        return waypointSets;
    }

    public void removeWaypointSet(final int index) {
        if (index == 0)
            throw new IllegalArgumentException("Default waypoint set should be never removed");
        final WaypointSet wptset = waypointSets.remove(index);
        for (Iterator<Waypoint> iter = waypoints.iterator(); iter.hasNext();) {
            Waypoint wpt = iter.next();
            if (wpt.set == wptset) {
                iter.remove();
            }
        }
    }

    private void clearWaypointSets() {
        waypointSets.clear();
    }

    public int addTrack(final Track track) {
        tracks.add(track);
        TrackOverlay trackOverlay = new TrackOverlay(track);
        overlayManager.fileTrackOverlays.add(trackOverlay);
        return tracks.lastIndexOf(track);
    }

    /**
     * Notify overlay that track properties have changed
     * @param track Changed track
     */
    public void dispatchTrackPropertiesChanged(Track track) {
        for (Iterator<TrackOverlay> iter = overlayManager.fileTrackOverlays.iterator(); iter.hasNext();) {
            TrackOverlay to = iter.next();
            if (to.getTrack() == track) {
                to.onTrackPropertiesChanged();
            }
        }
    }

    public boolean removeTrack(final Track track) {
        track.removed = true;
        boolean removed = tracks.remove(track);
        if (removed) {
            for (Iterator<TrackOverlay> iter = overlayManager.fileTrackOverlays.iterator(); iter.hasNext();) {
                TrackOverlay to = iter.next();
                if (to.getTrack().removed) {
                    to.onBeforeDestroy();
                    iter.remove();
                }
            }
        }
        return removed;
    }

    public void clearTracks() {
        for (Track track : tracks) {
            track.removed = true;
        }
        tracks.clear();
        for (Iterator<TrackOverlay> iter = overlayManager.fileTrackOverlays.iterator(); iter.hasNext();) {
            TrackOverlay to = iter.next();
            if (to.getTrack().removed) {
                to.onBeforeDestroy();
                iter.remove();
            }
        }
    }

    public Track getTrack(final int index) {
        return tracks.get(index);
    }

    public int getTrackIndex(final Track track) {
        return tracks.indexOf(track);
    }

    public List<Track> getTracks() {
        return tracks;
    }

    public boolean hasTracks() {
        return tracks.size() > 0;
    }

    public Route trackToRoute2(Track track, float sensitivity) throws IllegalArgumentException {
        Route route = new Route();
        List<Track.TrackPoint> points = track.getAllPoints();
        Track.TrackPoint tp = points.get(0);
        route.addWaypoint("RWPT", tp.latitude, tp.longitude).proximity = 0;

        if (points.size() < 2)
            throw new IllegalArgumentException("Track too short");

        tp = points.get(points.size() - 1);
        route.addWaypoint("RWPT", tp.latitude, tp.longitude).proximity = points.size() - 1;

        int prx = Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(this).getString(
                getString(R.string.pref_navigation_proximity), getString(R.string.def_navigation_proximity)));
        double proximity = prx * sensitivity;
        boolean peaks = true;
        int s = 1;

        while (peaks) {
            peaks = false;
            //Log.d("ANDROZIC", s+","+peaks);
            for (int i = s; i > 0; i--) {
                Waypoint sp = route.getWaypoint(i - 1);
                Waypoint fp = route.getWaypoint(i);
                if (fp.silent)
                    continue;
                double c = Geo.bearing(sp.latitude, sp.longitude, fp.latitude, fp.longitude);
                double xtkMin = 0, xtkMax = 0;
                int tpMin = 0, tpMax = 0;
                //Log.d("ANDROZIC", "vector: "+i+","+c);
                //Log.d("ANDROZIC", sp.name+"-"+fp.name+","+sp.proximity+"-"+fp.proximity);
                for (int j = sp.proximity; j < fp.proximity; j++) {
                    tp = points.get(j);
                    double b = Geo.bearing(tp.latitude, tp.longitude, fp.latitude, fp.longitude);
                    double d = Geo.distance(tp.latitude, tp.longitude, fp.latitude, fp.longitude);
                    double xtk = Geo.xtk(d, c, b);
                    if (xtk != Double.NEGATIVE_INFINITY && xtk < xtkMin) {
                        xtkMin = xtk;
                        tpMin = j;
                    }
                    if (xtk != Double.NEGATIVE_INFINITY && xtk > xtkMax) {
                        xtkMax = xtk;
                        tpMax = j;
                    }
                }
                // mark this vector to skip it on next pass
                if (xtkMin >= -proximity && xtkMax <= proximity) {
                    fp.silent = true;
                    continue;
                }
                if (xtkMin < -proximity) {
                    tp = points.get(tpMin);
                    route.insertWaypoint(i - 1, "RWPT", tp.latitude, tp.longitude).proximity = tpMin;
                    //Log.w("ANDROZIC", "min peak: "+s+","+tpMin+","+xtkMin);
                    s++;
                    peaks = true;
                }
                if (xtkMax > proximity) {
                    tp = points.get(tpMax);
                    int after = xtkMin < -proximity && tpMin < tpMax ? i : i - 1;
                    route.insertWaypoint(after, "RWPT", tp.latitude, tp.longitude).proximity = tpMax;
                    //Log.w("ANDROZIC", "max peak: "+s+","+tpMax+","+xtkMax);
                    s++;
                    peaks = true;
                }
            }
            //Log.d("ANDROZIC", s+","+peaks);
            if (s > 500)
                peaks = false;
        }
        s = 0;
        for (Waypoint wpt : route.getWaypoints()) {
            wpt.name += s;
            wpt.proximity = prx;
            wpt.silent = false;
            s++;
        }
        route.name = "RT_" + track.name;
        route.show = true;
        return route;
    }

    public Route trackToRoute(Track track, float sensitivity) throws IllegalArgumentException {
        Route route = new Route();
        List<Track.TrackPoint> points = track.getAllPoints();
        Track.TrackPoint lrp = points.get(0);
        route.addWaypoint("RWPT0", lrp.latitude, lrp.longitude);

        if (points.size() < 2)
            throw new IllegalArgumentException("Track too short");

        Track.TrackPoint cp = points.get(1);
        Track.TrackPoint lp = lrp;
        Track.TrackPoint tp = null;
        int i = 1;
        int prx = Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(this).getString(
                getString(R.string.pref_navigation_proximity), getString(R.string.def_navigation_proximity)));
        double proximity = prx * sensitivity;
        double d = 0, t = 0, b, pb = 0, cb = -1, icb = 0, xtk = 0;

        while (i < points.size()) {
            cp = points.get(i);
            d += Geo.distance(lp.latitude, lp.longitude, cp.latitude, cp.longitude);
            b = Geo.bearing(lp.latitude, lp.longitude, cp.latitude, cp.longitude);
            t += Geo.turn(pb, b);
            if (Math.abs(t) >= 360) {
                t = t - 360 * Math.signum(t);
            }
            //Log.d("ANDROZIC", i+","+b+","+t);
            lp = cp;
            pb = b;
            i++;

            // calculate initial track
            if (cb < 0) {
                if (d > proximity) {
                    cb = Geo.bearing(lrp.latitude, lrp.longitude, cp.latitude, cp.longitude);
                    pb = cb;
                    t = 0;
                    icb = cb + 180;
                    if (icb >= 360)
                        icb -= 360;
                    // Log.w("ANDROZIC", "Found vector:" + cb);
                }
                continue;
            }
            // find turn
            if (Math.abs(t) > 10) {
                if (tp == null) {
                    tp = cp;
                    // Log.w("ANDROZIC", "Found turn: "+i);
                    continue;
                }
            } else if (tp != null && xtk < proximity / 10) {
                tp = null;
                xtk = 0;
                // Log.w("ANDROZIC", "Reset turn: "+i);
            }
            // if turn in progress check xtk
            if (tp != null) {
                double xd = Geo.distance(cp.latitude, cp.longitude, tp.latitude, tp.longitude);
                double xb = Geo.bearing(cp.latitude, cp.longitude, tp.latitude, tp.longitude);
                xtk = Geo.xtk(xd, icb, xb);
                // turned at sharp angle
                if (xtk == Double.NEGATIVE_INFINITY)
                    xtk = Geo.xtk(xd, cb, xb);
                // Log.w("ANDROZIC", "XTK: "+xtk);
                if (Math.abs(xtk) > proximity * 3) {
                    lrp = tp;
                    route.addWaypoint("RWPT" + route.length(), lrp.latitude, lrp.longitude);
                    cb = Geo.bearing(lrp.latitude, lrp.longitude, cp.latitude, cp.longitude);
                    // Log.e("ANDROZIC", "Set WPT: "+(route.length()-1)+","+cb);
                    pb = cb;
                    t = 0;
                    icb = cb + 180;
                    if (icb >= 360)
                        icb -= 360;
                    tp = null;
                    d = 0;
                    xtk = 0;
                }
                continue;
            }
            // if still direct but pretty far away add a point
            if (d > proximity * 200) {
                lrp = cp;
                route.addWaypoint("RWPT" + route.length(), lrp.latitude, lrp.longitude);
                // Log.e("ANDROZIC", "Set WPT: "+(route.length()-1));
                d = 0;
            }
        }
        lrp = points.get(i - 1);
        route.addWaypoint("RWPT" + route.length(), lrp.latitude, lrp.longitude);
        route.name = "RT_" + track.name;
        route.show = true;
        return route;
    }

    public int addRoute(final Route route) {
        routes.add(route);
        RouteOverlay routeOverlay = new RouteOverlay(route);
        overlayManager.routeOverlays.add(routeOverlay);
        return routes.lastIndexOf(route);
    }

    /**
     * Notify overlay that route properties have changed
     * @param route Changed route
     */
    public void dispatchRoutePropertiesChanged(Route route) {
        for (Iterator<RouteOverlay> iter = overlayManager.routeOverlays.iterator(); iter.hasNext();) {
            RouteOverlay to = iter.next();
            if (to.getRoute() == route) {
                to.onRoutePropertiesChanged();
            }
        }
    }

    public boolean removeRoute(final Route route) {
        route.removed = true;
        boolean removed = routes.remove(route);
        if (removed) {
            for (Iterator<RouteOverlay> iter = overlayManager.routeOverlays.iterator(); iter.hasNext();) {
                RouteOverlay to = iter.next();
                if (to.getRoute().removed) {
                    to.onBeforeDestroy();
                    iter.remove();
                }
            }
        }
        return removed;
    }

    public void clearRoutes() {
        for (Route route : routes) {
            route.removed = true;
        }
        routes.clear();
        for (Iterator<RouteOverlay> iter = overlayManager.routeOverlays.iterator(); iter.hasNext();) {
            RouteOverlay to = iter.next();
            if (to.getRoute().removed) {
                to.onBeforeDestroy();
                iter.remove();
            }
        }
    }

    @Nullable
    public Route getRoute(final int index) {
        try {
            return routes.get(index);
        } catch (IndexOutOfBoundsException e) {
            // This can be caused when resuming unfinished route
            Log.w(TAG, "Bad route index: " + index);
            return null;
        }
    }

    @Nullable
    public Route getRouteByFile(String filepath) {
        for (Route route : routes) {
            if (filepath.equals(route.filepath))
                return route;
        }
        return null;
    }

    public int getRouteIndex(final Route route) {
        return routes.indexOf(route);
    }

    public List<Route> getRoutes() {
        return routes;
    }

    public boolean hasRoutes() {
        return routes.size() > 0;
    }

    public double getDeclination(double lat, double lon) {
        GeomagneticField mag = new GeomagneticField((float) lat, (float) lon, 0.0f, System.currentTimeMillis());
        return mag.getDeclination();
    }

    public double fixDeclination(double declination) {
        if (angleMagnetic) {
            declination -= magneticDeclination;
            declination = (declination + 360.0) % 360.0;
        }
        return declination;
    }

    public double[] getLocation() {
        double[] res = new double[2];
        res[0] = Double.isNaN(location[0]) ? mapCenter[0] : location[0];
        res[1] = Double.isNaN(location[1]) ? mapCenter[1] : location[1];
        return res;
    }

    public Location getLocationAsLocation() {
        Location loc = new Location("fake");
        loc.setLatitude(Double.isNaN(location[0]) ? mapCenter[0] : location[0]);
        loc.setLongitude(Double.isNaN(location[1]) ? mapCenter[1] : location[1]);
        return loc;
    }

    public void initializeMapCenter() {
        double[] coordinate = null;
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        String loc = sharedPreferences.getString(getString(R.string.loc_last), null);
        if (loc != null) {
            coordinate = CoordinateParser.parse(loc);
            setMapCenter(coordinate[0], coordinate[1], true, true, true);
        }
        if (coordinate == null) {
            setMapCenter(0, 0, true, true, true);
        }
    }

    public double[] getMapCenter() {
        double[] res = new double[2];
        res[0] = mapCenter[0];
        res[1] = mapCenter[1];
        return res;
    }

    /**
     * Sets map center to specified coordinates
     * @param lat New latitude
     * @param lon New longitude
     * @param checkcoverage Check if map covers specified location
     * @param reindex Recreate index of maps for new location
     * @param findbest Look for best map in new location
     * @return true if current map was changed
     */
    public boolean setMapCenter(double lat, double lon, boolean checkcoverage, boolean reindex, boolean findbest) {
        mapCenter[0] = lat;
        mapCenter[1] = lon;
        return checkcoverage && updateLocationMaps(reindex, findbest);
    }

    /**
     * Updates available map list for current location
     * 
     * @param reindex
     *            Recreate index of maps for current location 
     * @param findbest
     *            Look for best map in current location
     * @return true if current map was changed
     */
    public boolean updateLocationMaps(boolean reindex, boolean findbest) {
        if (maps == null)
            return false;

        boolean covers = currentMap != null && currentMap.coversLatLon(mapCenter[0], mapCenter[1]);

        if (!covers || reindex || findbest)
            suitableMaps = maps.getMaps(mapCenter[0], mapCenter[1]);

        if (covers && !findbest)
            return false;

        BaseMap newMap = null;
        if (suitableMaps.size() > 0) {
            newMap = suitableMaps.get(0);
        }
        if (newMap == null) {
            newMap = MockMap.getMap(mapCenter[0], mapCenter[1]);
        }
        return setMap(newMap, false);
    }

    public boolean scrollMap(int dx, int dy, boolean checkcoverage) {
        if (currentMap != null) {
            int[] xy = new int[2];
            double[] ll = new double[2];

            currentMap.getXYByLatLon(mapCenter[0], mapCenter[1], xy);
            currentMap.getLatLonByXY(xy[0] + dx, xy[1] + dy, ll);

            if (ll[0] > 90.0)
                ll[0] = 90.0;
            if (ll[0] < -90.0)
                ll[0] = -90.0;
            if (ll[1] > 180.0)
                ll[1] = 180.0;
            if (ll[1] < -180.0)
                ll[1] = -180.0;

            return setMapCenter(ll[0], ll[1], checkcoverage, false, false);
        }
        return false;
    }

    public int[] getXYbyLatLon(double lat, double lon) {
        int[] xy = new int[] { 0, 0 };
        getXYbyLatLon(lat, lon, xy);
        return xy;
    }

    public void getXYbyLatLon(double lat, double lon, int[] xy) {
        if (currentMap != null) {
            currentMap.getXYByLatLon(lat, lon, xy);
        }
    }

    public void getLatLonByXY(int x, int y, double[] ll) {
        if (currentMap != null) {
            currentMap.getLatLonByXY(x, y, ll);
        }
    }

    public double getZoom() {
        if (currentMap != null)
            return currentMap.getZoom();
        else
            return 0.0;
    }

    public boolean setZoom(double zoom) {
        if (zoom == getZoom())
            return false;
        if (currentMap != null) {
            currentMap.setZoom(zoom);
            invalidCoveringMaps = true;
            if (mapHolder != null)
                mapHolder.conditionsChanged();
            return true;
        }
        return false;
    }

    public boolean zoomIn() {
        if (currentMap != null) {
            double zoom = getNextZoom();
            if (zoom > 0) {
                currentMap.setZoom(zoom);
                invalidCoveringMaps = true;
                if (mapHolder != null)
                    mapHolder.conditionsChanged();
                return true;
            }
        }
        return false;
    }

    public boolean zoomOut() {
        if (currentMap != null) {
            double zoom = getPrevZoom();
            if (zoom > 0) {
                currentMap.setZoom(zoom);
                invalidCoveringMaps = true;
                if (mapHolder != null)
                    mapHolder.conditionsChanged();
                return true;
            }
        }
        return false;
    }

    public double getNextZoom() {
        if (currentMap != null)
            return currentMap.getNextZoom();
        else
            return 0.0;
    }

    public double getPrevZoom() {
        if (currentMap != null)
            return currentMap.getPrevZoom();
        else
            return 0.0;
    }

    public boolean zoomBy(float factor) {
        if (currentMap != null) {
            currentMap.zoomBy(factor);
            invalidCoveringMaps = true;
            if (mapHolder != null)
                mapHolder.conditionsChanged();
            return true;
        }
        return false;
    }

    public List<TileProvider> getOnlineMaps() {
        return onlineMaps;
    }

    @Nullable
    public String getMapTitle() {
        if (currentMap != null)
            return currentMap.title;
        else
            return null;
    }

    @Nullable
    public String getMapLicense() {
        if (currentMap != null && currentMap instanceof OnlineMap) {
            TileProvider provider = ((OnlineMap) currentMap).tileProvider;
            return provider.license;
        }
        return null;
    }

    public BaseMap getCurrentMap() {
        return currentMap;
    }

    public Collection<BaseMap> getMaps() {
        return maps.getMaps();
    }

    public List<BaseMap> getMaps(double[] loc) {
        return maps.getMaps(loc[0], loc[1]);
    }

    public boolean prevMap() {
        updateLocationMaps(true, false);
        int id = 0;
        if (currentMap != null) {
            int pos = suitableMaps.indexOf(currentMap);
            if (pos >= 0 && pos < suitableMaps.size() - 1) {
                id = suitableMaps.get(pos + 1).id;
            } else {
                id = suitableMaps.get(0).id;
            }
        } else if (suitableMaps.size() > 0) {
            id = suitableMaps.get(suitableMaps.size() - 1).id;
        }
        return id != 0 && selectMap(id);
    }

    public boolean nextMap() {
        updateLocationMaps(true, false);
        int id = 0;
        if (currentMap != null) {
            int pos = suitableMaps.indexOf(currentMap);
            if (pos > 0) {
                id = suitableMaps.get(pos - 1).id;
            } else {
                id = suitableMaps.get(0).id;
            }
        } else if (suitableMaps.size() > 0) {
            id = suitableMaps.get(0).id;
        }
        return id != 0 && selectMap(id);
    }

    /**
     * Sets map if it available for current location.
     * @param id ID of a map to set
     * @return true if map was changed
     */
    public boolean selectMap(int id) {
        if (currentMap != null && currentMap.id == id)
            return false;

        BaseMap newMap = null;
        for (BaseMap map : suitableMaps) {
            if (map.id == id) {
                newMap = map;
                break;
            }
        }
        return setMap(newMap, true);
    }

    public boolean loadMap(BaseMap newMap) {
        boolean newmap = setMap(newMap, true);
        if (currentMap != null) {
            currentMap.getMapCenter(mapCenter);
            suitableMaps = maps.getMaps(mapCenter[0], mapCenter[1]);
            invalidCoveringMaps = true;
        }
        return newmap;
    }

    synchronized boolean setMap(final BaseMap newMap, boolean forced) {
        // TODO should override equals()?
        if (newMap != null && !newMap.equals(currentMap)) {
            double mpp = currentMap != null ? currentMap.getMPP() : newMap.getAbsoluteMPP();
            double ratio = newMap.getCoveringRatio(mpp);
            // IF current map scale is too different, use new map scale
            if (ratio > 10d || ratio < 0.01)
                mpp = newMap.getAbsoluteMPP();
            Log.w(TAG, "Set map: " + newMap.title + " " + mpp);
            if (mapHolder != null) {
                try {
                    newMap.activate(mapHolder, mpp, true);
                } catch (final Throwable e) {
                    e.printStackTrace();
                    uiHandler.post(new MapActivationError(newMap, e));
                    return false;
                }
            }
            if (currentMap != null) {
                currentMap.deactivate();
            }
            invalidCoveringMaps = true;
            currentMap = newMap;
            if (mapHolder != null)
                mapHolder.mapChanged(forced);
            if (currentMap instanceof OzfMap)
                overlayManager.initGrids((OzfMap) currentMap);
            return true;
        }
        return false;
    }

    public void setOnlineMaps(String providers) {
        if (onlineMaps == null || maps == null)
            return;

        List<String> selectedProviders = Arrays.asList(providers.split("\\|"));
        byte zoom = (byte) PreferenceManager.getDefaultSharedPreferences(this).getInt(
                getString(R.string.pref_onlinemapscale), getResources().getInteger(R.integer.def_onlinemapscale));

        for (TileProvider map : onlineMaps) {
            boolean s = currentMap == map.instance;
            if (map.instance != null && !selectedProviders.contains(map.code)) {
                maps.removeMap(map.instance);
                if (s) {
                    updateLocationMaps(true, true);
                    map.instance.deactivate();
                }
                map.instance = null;
                map.listener = null;
            }
            if (selectedProviders.contains(map.code) && map.instance == null) {
                OnlineMap onlineMap = new OnlineMap(map, zoom);
                onlineMap.setPrescaleFactor(onlineMapPrescaleFactor);
                maps.addMap(onlineMap);
                map.instance = onlineMap;
                map.listener = mapHolder;
            }
        }
    }

    /*
     * Clip map to corners
     * Draw corners
     * Show adjacent maps
     * Adjacent maps diff factor
     */

    private void updateCoveringMaps() {
        if (mapsHandler.hasMessages(1))
            mapsHandler.removeMessages(1);

        Message m = Message.obtain(mapsHandler, new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "updateCoveringMaps()");
                Bounds area = new Bounds();
                int[] xy = new int[2];
                double[] ll = new double[2];
                currentMap.getXYByLatLon(mapCenter[0], mapCenter[1], xy);
                currentMap.getLatLonByXY(xy[0] + (int) coveringScreen.left, xy[1] + (int) coveringScreen.top, ll);
                area.maxLat = ll[0];
                area.minLon = ll[1];
                currentMap.getLatLonByXY(xy[0] + (int) coveringScreen.right, xy[1] + (int) coveringScreen.bottom,
                        ll);
                area.minLat = ll[0];
                area.maxLon = ll[1];
                area.fix();
                List<BaseMap> cmr = new ArrayList<>();
                if (coveringMaps != null)
                    cmr.addAll(coveringMaps);
                List<BaseMap> cma = maps.getCoveringMaps(currentMap, area, coveredAll, coveringBestMap);
                Iterator<BaseMap> icma = cma.iterator();
                while (icma.hasNext()) {
                    BaseMap map = icma.next();
                    Log.i(TAG, "-> " + map.title);
                    try {
                        if (!map.activated())
                            map.activate(mapHolder, currentMap.getMPP(), false);
                        else
                            map.zoomTo(currentMap.getMPP());
                        cmr.remove(map);
                    } catch (Throwable e) {
                        icma.remove();
                        e.printStackTrace();
                    }
                }
                synchronized (Androzic.this) {
                    for (BaseMap map : cmr) {
                        if (map != currentMap)
                            map.deactivate();
                    }
                    coveringMaps = cma;
                    invalidCoveringMaps = false;
                }
                if (mapHolder != null)
                    mapHolder.refreshMap();
            }
        });
        m.what = 1;
        mapsHandler.sendMessage(m);
    }

    public void drawMap(Viewport viewport, boolean bestmap, Canvas c) {
        BaseMap cm = currentMap;

        if (cm != null) {
            if (adjacentMaps) {
                int l = -(viewport.width / 2 + viewport.lookAheadXY[0]);
                int t = -(viewport.height / 2 + viewport.lookAheadXY[1]);
                int r = l + viewport.width;
                int b = t + viewport.height;

                if (coveringMaps == null || invalidCoveringMaps || viewport.mapCenter[0] != coveringLoc[0]
                        || viewport.mapCenter[1] != coveringLoc[1] || coveringBestMap != bestmap
                        || l != coveringScreen.left || t != coveringScreen.top || r != coveringScreen.right
                        || b != coveringScreen.bottom) {
                    coveringScreen.left = l;
                    coveringScreen.top = t;
                    coveringScreen.right = r;
                    coveringScreen.bottom = b;
                    coveringLoc[0] = viewport.mapCenter[0];
                    coveringLoc[1] = viewport.mapCenter[1];
                    coveringBestMap = bestmap;
                    updateCoveringMaps();
                }
            }
            try {
                if (coveringMaps != null && !coveringMaps.isEmpty()) {
                    boolean drawn = false;
                    for (BaseMap map : coveringMaps) {
                        if (!drawn && coveringBestMap && map.getMPP() < cm.getMPP()) {
                            coveredAll = cm.drawMap(viewport, cropMapBorder, drawMapBorder, c);
                            drawn = true;
                        }
                        map.drawMap(viewport, cropMapBorder, drawMapBorder, c);
                    }
                    if (!drawn) {
                        coveredAll = cm.drawMap(viewport, cropMapBorder, drawMapBorder, c);
                    }
                } else {
                    coveredAll = cm.drawMap(viewport, cropMapBorder, drawMapBorder, c);
                }
            } catch (OutOfMemoryError err) {
                if (!memmsg && mapHolder != null)
                    uiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(Androzic.this, R.string.err_nomemory, Toast.LENGTH_LONG).show();
                        }
                    });
                memmsg = true;
                err.printStackTrace();
            }
        }
    }

    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.d(TAG, "Broadcast: " + action);
            if (action.equals(NavigationService.BROADCAST_NAVIGATION_STATE)) {
                int state = intent.getExtras().getInt("state");
                switch (state) {
                case NavigationService.STATE_STARTED:
                    if (overlayManager.navigationOverlay == null) {
                        overlayManager.navigationOverlay = new NavigationOverlay();
                        if (mapHolder != null)
                            overlayManager.navigationOverlay.onMapChanged();
                    }
                    break;
                case NavigationService.STATE_REACHED:
                    Toast.makeText(Androzic.this, R.string.arrived, Toast.LENGTH_LONG).show();
                case NavigationService.STATE_STOPED:
                    if (overlayManager.navigationOverlay != null) {
                        overlayManager.navigationOverlay.onBeforeDestroy();
                        overlayManager.navigationOverlay = null;
                    }
                    break;
                }
            }
        }
    };

    public boolean isLocating() {
        return locationService != null && locationService.isLocating();
    }

    public void enableLocating(boolean enable) {
        if (locationService == null)
            bindService(new Intent(this, LocationService.class), locationConnection, BIND_AUTO_CREATE);
        String action = enable ? LocationService.ENABLE_LOCATIONS : LocationService.DISABLE_LOCATIONS;
        startService(new Intent(this, LocationService.class).setAction(action));
        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putBoolean(getString(R.string.lc_locate), enable);
        editor.commit();
    }

    public ILocationService getLocationService() {
        return locationService;
    }

    public float getHDOP() {
        if (locationService != null)
            return locationService.getHDOP();
        else
            return Float.NaN;
    }

    public float getVDOP() {
        if (locationService != null)
            return locationService.getVDOP();
        else
            return Float.NaN;
    }

    private ServiceConnection locationConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder binder) {
            locationService = (ILocationService) binder;
            locationService.registerLocationCallback(locationListener);
            Log.d(TAG, "Location service connected");
        }

        public void onServiceDisconnected(ComponentName className) {
            locationService = null;
            Log.d(TAG, "Location service disconnected");
        }
    };

    private ILocationListener locationListener = new ILocationListener() {
        @Override
        public void onGpsStatusChanged(String provider, final int status, final int fsats, final int tsats) {
            if (LocationManager.GPS_PROVIDER.equals(provider)) {
                gpsStatus = status;
                gpsFSats = fsats;
                gpsTSats = tsats;
            }
        }

        @Override
        public void onLocationChanged(final Location location, final boolean continous, final boolean geoid,
                final float smoothspeed, final float avgspeed) {
            Log.d(TAG, "Location arrived");

            final long lastLocationMillis = location.getTime();

            if (angleMagnetic && lastLocationMillis - lastMagnetic >= magInterval) {
                GeomagneticField mag = new GeomagneticField((float) location.getLatitude(),
                        (float) location.getLongitude(), (float) location.getAltitude(),
                        System.currentTimeMillis());
                magneticDeclination = mag.getDeclination();
                lastMagnetic = lastLocationMillis;
            }

            Androzic.this.location[0] = location.getLatitude();
            Androzic.this.location[1] = location.getLongitude();

            shouldEnableFollowing = shouldEnableFollowing || lastKnownLocation == null;

            lastKnownLocation = location;
            gpsEnabled = gpsEnabled || LocationManager.GPS_PROVIDER.equals(location.getProvider());
            gpsContinous = continous;
            gpsGeoid = geoid;

            if (overlayManager.accuracyOverlay != null && location.hasAccuracy()) {
                overlayManager.accuracyOverlay.setAccuracy(location.getAccuracy());
            }
        }

        @Override
        public void onProviderChanged(String provider) {
        }

        @Override
        public void onProviderDisabled(String provider) {
            if (LocationManager.GPS_PROVIDER.equals(provider)) {
                Log.i(TAG, "GPS provider disabled");
                gpsEnabled = false;
            }
        }

        @Override
        public void onProviderEnabled(String provider) {
            if (LocationManager.GPS_PROVIDER.equals(provider)) {
                Log.i(TAG, "GPS provider enabled");
                gpsEnabled = true;
            }
        }
    };

    public boolean isTracking() {
        return locationService != null && locationService.isTracking();
    }

    public void enableTracking(boolean enable) {
        String action = enable ? LocationService.ENABLE_TRACK : LocationService.DISABLE_TRACK;
        startService(new Intent(this, LocationService.class).setAction(action));
        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putBoolean(getString(R.string.lc_track), enable);
        editor.commit();
    }

    public void expandCurrentTrack() {
        if (locationService != null) {
            Track track = locationService.getTrack();
            track.show = true;
            overlayManager.currentTrackOverlay.setTrack(track);
        }
    }

    public void clearCurrentTrack() {
        if (overlayManager.currentTrackOverlay != null)
            overlayManager.currentTrackOverlay.clear();
        if (locationService != null)
            locationService.clearTrack();
    }

    /**
     * Retrieves last known location without enabling location providers.
     * @return Most precise last known location or null if it is not available
     */
    public Location getLastKnownSystemLocation() {
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        List<String> providers = lm.getProviders(true);
        Location l = null;

        for (int i = providers.size() - 1; i >= 0; i--) {
            l = lm.getLastKnownLocation(providers.get(i));
            if (l != null)
                break;
        }

        return l;
    }

    public boolean isNavigating() {
        return navigationService != null && navigationService.isNavigating();
    }

    public boolean isNavigatingViaRoute() {
        return navigationService != null && navigationService.isNavigatingViaRoute();
    }

    public void initializeNavigation() {
        bindService(new Intent(this, NavigationService.class), navigationConnection, BIND_AUTO_CREATE);
        registerReceiver(broadcastReceiver, new IntentFilter(NavigationService.BROADCAST_NAVIGATION_STATUS));
        registerReceiver(broadcastReceiver, new IntentFilter(NavigationService.BROADCAST_NAVIGATION_STATE));

        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
        String navWpt = settings.getString(getString(R.string.nav_wpt), "");
        if (!"".equals(navWpt)) {
            Waypoint waypoint = new Waypoint();
            waypoint.name = navWpt;
            waypoint.latitude = (double) settings.getFloat(getString(R.string.nav_wpt_lat), 0);
            waypoint.longitude = (double) settings.getFloat(getString(R.string.nav_wpt_lon), 0);
            waypoint.proximity = settings.getInt(getString(R.string.nav_wpt_prx), 0);
            startNavigation(waypoint);
        }

        String navRoute = settings.getString(getString(R.string.nav_route), "");
        if (!"".equals(navRoute) && settings.getBoolean(getString(R.string.pref_navigation_loadlast),
                getResources().getBoolean(R.bool.def_navigation_loadlast))) {
            int ndir = settings.getInt(getString(R.string.nav_route_dir), 0);
            int nwpt = settings.getInt(getString(R.string.nav_route_wpt), -1);
            try {
                Route route = getRouteByFile(navRoute);
                if (route != null) {
                    route.show = true;
                } else {
                    File rtf = new File(navRoute);
                    // FIXME It's bad - it can be not a first route in a file
                    route = OziExplorerFiles.loadRoutesFromFile(rtf, charset).get(0);
                    addRoute(route);
                }
                startNavigation(route, ndir, nwpt);
            } catch (Exception e) {
                Log.e(TAG, "Failed to start navigation", e);
            }
        }
    }

    public void startNavigation(Waypoint waypoint) {
        Intent i = new Intent(this, NavigationService.class).setAction(NavigationService.NAVIGATE_MAPOBJECT);
        i.putExtra(NavigationService.EXTRA_NAME, waypoint.name);
        i.putExtra(NavigationService.EXTRA_LATITUDE, waypoint.latitude);
        i.putExtra(NavigationService.EXTRA_LONGITUDE, waypoint.longitude);
        i.putExtra(NavigationService.EXTRA_PROXIMITY, waypoint.proximity);
        startService(i);

        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putString(getString(R.string.nav_route), "");
        editor.putString(getString(R.string.nav_wpt), waypoint.name);
        editor.putInt(getString(R.string.nav_wpt_prx), waypoint.proximity);
        editor.putFloat(getString(R.string.nav_wpt_lat), (float) waypoint.latitude);
        editor.putFloat(getString(R.string.nav_wpt_lon), (float) waypoint.longitude);
        editor.commit();
    }

    public void startNavigation(MapObject mapObject) {
        Intent i = new Intent(this, NavigationService.class)
                .setAction(NavigationService.NAVIGATE_MAPOBJECT_WITH_ID);
        i.putExtra(NavigationService.EXTRA_ID, mapObject._id);
        startService(i);
    }

    public void startNavigation(Route route) {
        startNavigation(route, 0, -1);
    }

    public void startNavigation(Route route, int direction, int waypointIndex) {
        route.show = true;
        int rt = getRouteIndex(route);
        Intent i = new Intent(this, NavigationService.class).setAction(NavigationService.NAVIGATE_ROUTE);
        i.putExtra(NavigationService.EXTRA_ROUTE_INDEX, rt);
        i.putExtra(NavigationService.EXTRA_ROUTE_DIRECTION, direction);
        i.putExtra(NavigationService.EXTRA_ROUTE_START, waypointIndex);
        startService(i);

        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putString(getString(R.string.nav_wpt), "");
        editor.putString(getString(R.string.nav_route), "");
        if (route.filepath != null) {
            editor.putString(getString(R.string.nav_route), route.filepath);
            editor.putInt(getString(R.string.nav_route_idx), getRouteIndex(route));
            editor.putInt(getString(R.string.nav_route_dir), direction);
            editor.putInt(getString(R.string.nav_route_wpt), waypointIndex);
        }
        editor.commit();
    }

    public void stopNavigation() {
        navigationService.stopNavigation();

        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        editor.putString(getString(R.string.nav_wpt), "");
        editor.putString(getString(R.string.nav_route), "");
        editor.commit();
    }

    private ServiceConnection navigationConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            navigationService = ((NavigationService.LocalBinder) service).getService();
            Log.d(TAG, "Navigation service connected");
        }

        public void onServiceDisconnected(ComponentName className) {
            navigationService = null;
            Log.d(TAG, "Navigation service disconnected");
        }
    };

    public void setRootPath(String path) {
        rootPath = path;
    }

    @Override
    public File getCacheDir() {
        if (cacheDir != null)
            return cacheDir;

        File[] caches = ContextCompat.getExternalCacheDirs(this);
        cacheDir = caches[0];
        // Select the first really external (removable) storage if present
        for (int i = 1; i < caches.length; i++) {
            if (caches[i] != null) {
                cacheDir = caches[i];
                break;
            }
        }
        if (cacheDir != null)
            Log.i(TAG, "External cache: " + cacheDir.getAbsolutePath());
        return cacheDir;
    }

    public void setDataPath(int pathtype, String path) {
        if ((pathtype & PATH_DATA) > 0)
            dataPath = path;
        if ((pathtype & PATH_ICONS) > 0)
            iconPath = path;
        if ((pathtype & PATH_MARKERICONS) > 0)
            markerPath = path;
    }

    public boolean setMapPath(String path) {
        if (mapPath == null || !mapPath.equals(path)) {
            mapPath = path;
            if (mapsInited) {
                resetMaps();
                return true;
            }
        }
        return false;
    }

    public String getMapPath() {
        return mapPath;
    }

    public void initializeMaps() {
        initializeRenderTheme();
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
        boolean useIndex = settings.getBoolean(getString(R.string.pref_usemapindex),
                getResources().getBoolean(R.bool.def_usemapindex));
        maps = null;
        File indexFile = new File(rootPath, "maps.idx");
        if (useIndex && indexFile.exists()) {
            try {
                maps = MapIndex.loadIndex(indexFile);
                int hash = MapIndex.getMapsHash(mapPath);
                if (hash != maps.hashCode())
                    maps = null;
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        if (maps == null) {
            maps = new MapIndex(mapPath, charset);
            StringBuilder sb = new StringBuilder();
            for (BaseMap mp : maps.getMaps()) {
                if (mp.loadError != null) {
                    String fn = mp.path;
                    if (fn.startsWith(mapPath)) {
                        fn = fn.substring(mapPath.length() + 1);
                    }
                    sb.append("<b>");
                    sb.append(fn);
                    sb.append(":</b> ");
                    if (mp.loadError instanceof ProjectionException) {
                        sb.append("projection error: ");
                    }
                    sb.append(mp.loadError.getMessage());
                    sb.append("<br />\n");
                }
            }
            if (sb.length() > 0) {
                maps.cleanBadMaps();
                startActivity(new Intent(this, ErrorDialog.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        .putExtra("title", getString(R.string.badmaps)).putExtra("message", sb.toString()));
            }

            if (useIndex) {
                try {
                    MapIndex.saveIndex(maps, indexFile);
                }
                //FIXME We should fall back to old index then...
                catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }

        // Online maps
        onlineMaps = new ArrayList<>();
        TileProvider provider = new OpenStreetMapTileProvider();
        provider.tileExpiration = onlineMapTileExpiration;
        onlineMaps.add(provider);
        String[] om = getResources().getStringArray(R.array.online_maps);
        for (String s : om) {
            provider = TileProviderFactory.fromString(s);
            if (provider != null) {
                provider.tileExpiration = onlineMapTileExpiration;
                onlineMaps.add(provider);
            }
        }
        initializeOnlineMapProviders();
        File mapproviders = new File(rootPath, "providers.dat");
        if (mapproviders.exists()) {
            try {
                BufferedReader reader = new BufferedReader(new FileReader(mapproviders));
                String line;
                while ((line = reader.readLine()) != null) {
                    line = line.trim();
                    if (line.startsWith("#") || "".equals(line))
                        continue;
                    provider = TileProviderFactory.fromString(line);
                    if (provider != null) {
                        provider.tileExpiration = onlineMapTileExpiration;
                        onlineMaps.add(provider);
                    }
                }
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        setOnlineMaps(settings.getString(getString(R.string.pref_onlinemap),
                getResources().getString(R.string.def_onlinemap)));
        suitableMaps = new ArrayList<BaseMap>();
        coveredAll = false;
        coveringBestMap = true;
        mapsInited = true;
    }

    public void resetMaps() {
        File index = new File(rootPath, "maps.idx");
        if (index.exists())
            //noinspection ResultOfMethodCallIgnored
            index.delete();
        clearMaps();
        ForgeMap.reset();
        initializeMaps();
        updateLocationMaps(true, true);
    }

    public void moveTileCache() {
        File oldTilesCache = new File(rootPath, "tiles");
        if (!oldTilesCache.isDirectory())
            return;

        File newCache = getCacheDir();

        Pattern p = Pattern.compile("(\\d+)-(\\d+)");
        Matcher m;

        for (File providerDir : oldTilesCache.listFiles()) {
            if (!providerDir.isDirectory()) {
                providerDir.delete();
                continue;
            }
            String provider = providerDir.getName();
            for (File zoom : providerDir.listFiles()) {
                if (!zoom.isDirectory()) {
                    zoom.delete();
                    continue;
                }
                byte z;
                try {
                    z = Byte.valueOf(zoom.getName());
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                    zoom.delete();
                    continue;
                }
                for (File tile : zoom.listFiles()) {
                    m = p.matcher(tile.getName());
                    if (m.find()) {
                        int x = Integer.parseInt(m.group(1));
                        int y = Integer.parseInt(m.group(2));
                        File newTile = TileFactory.getTileFile(newCache, provider, x, y, z);
                        try {
                            FileUtils.copyFile(tile, newTile);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    tile.delete();
                }
                zoom.delete();
            }
            providerDir.delete();
        }
        oldTilesCache.delete();
    }

    /**
     * Copies file assets from installation package to filesystem.
     */
    public void copyAssets(String folder, File path) {
        Log.i(TAG, "CopyAssets(" + folder + ", " + path + ")");
        AssetManager assetManager = getAssets();
        String[] files = null;
        try {
            files = assetManager.list(folder);
        } catch (IOException e) {
            Log.e("Androzic", "Failed to get assets list", e);
            return;
        }
        for (String file : files) {
            try {
                InputStream in = assetManager.open(folder + "/" + file);
                OutputStream out = new FileOutputStream(new File(path, file));
                byte[] buffer = new byte[1024];
                int read;
                while ((read = in.read(buffer)) != -1) {
                    out.write(buffer, 0, read);
                }
                in.close();
                out.flush();
                out.close();
            } catch (Exception e) {
                Log.e("Androzic", "Asset copy error", e);
            }
        }
    }

    void installData() {
        defWaypointSet = new WaypointSet(dataPath + File.separator + "myWaypoints.wpt", "myWaypoints");
        waypointSets.add(defWaypointSet);

        File icons = new File(iconPath, "icons.dat");
        if (icons.exists()) {
            try {
                BufferedReader reader = new BufferedReader(new FileReader(icons));
                String[] fields = CSV.parseLine(reader.readLine());
                if (fields.length == 3) {
                    iconsEnabled = true;
                    iconX = Integer.parseInt(fields[0]);
                    iconY = Integer.parseInt(fields[1]);
                }
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        File datums = new File(rootPath, "datums.dat");
        if (datums.exists()) {
            try {
                OziExplorerFiles.loadDatums(datums);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        File cursor = new File(rootPath, "cursor.png");
        if (cursor.exists()) {
            try {
                customCursor = new BitmapDrawable(getResources(), cursor.getAbsolutePath());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //installRawResource(R.raw.datums, "datums.xml");
    }

    void installRawResource(final int id, final String path) {
        try {
            // TODO Needs versioning
            openFileInput(path).close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            InputStream in = getResources().openRawResource(id);
            FileOutputStream out;

            try {
                out = openFileOutput(path, MODE_PRIVATE);

                int size = in.available();

                byte[] buffer = new byte[size];
                in.read(buffer);
                in.close();

                out.write(buffer);
                out.close();

            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    private void initializeRenderTheme() {
        try {
            AndroidSvgBitmapStore.clear();
            xmlRenderTheme = new BufferedAssetsRenderTheme(this, "", "renderthemes/rendertheme-v4.xml", this);
        } catch (IOException e) {
            e.printStackTrace();
            xmlRenderTheme = InternalRenderTheme.OSMARENDER;
        }
        ForgeMap.onRenderThemeChanged();
    }

    /**
     * Load default and selected waypoint files.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void initializeWaypoints() {
        if (waypoints.size() > 0)
            return;

        File wptFile = new File(dataPath, "myWaypoints.wpt");
        if (wptFile.exists() && wptFile.canRead()) {
            try {
                addWaypoints(OziExplorerFiles.loadWaypointsFromFile(wptFile, charset));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // load selected waypoint sets
            SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
            Set<String> sets = settings.getStringSet(getString(R.string.wpt_sets), new HashSet<String>());
            for (String path : sets) {
                File file = new File(path);
                try {
                    if (file.exists() && file.canRead())
                        WaypointFileHelper.loadFile(file);
                } catch (Exception e) {
                    // We ignore all exceptions on this stage
                    e.printStackTrace();
                }
            }
        }
    }

    public void initializePlugins() {
        PackageManager packageManager = getPackageManager();
        List<ResolveInfo> plugins;
        Intent initializationIntent = new Intent("com.androzic.plugins.action.INITIALIZE");

        // enumerate initializable plugins
        plugins = packageManager.queryBroadcastReceivers(initializationIntent, 0);
        for (ResolveInfo plugin : plugins) {
            // send initialization broadcast, we send it directly instead of sending
            // one broadcast for all plugins to wake up stopped plugins:
            // http://developer.android.com/about/versions/android-3.1.html#launchcontrols
            Intent intent = new Intent();
            intent.setClassName(plugin.activityInfo.packageName, plugin.activityInfo.name);
            intent.setAction(initializationIntent.getAction());
            sendBroadcast(intent);
        }

        // enumerate plugins with preferences
        plugins = packageManager.queryIntentActivities(new Intent("com.androzic.plugins.preferences"), 0);
        for (ResolveInfo plugin : plugins) {
            Intent intent = new Intent();
            intent.setClassName(plugin.activityInfo.packageName, plugin.activityInfo.name);
            pluginPreferences.put(plugin.activityInfo.loadLabel(packageManager).toString(), intent);
        }

        // enumerate plugins with views
        plugins = packageManager.queryIntentActivities(new Intent("com.androzic.plugins.view"), 0);
        for (ResolveInfo plugin : plugins) {
            // get menu icon
            Drawable icon = null;
            try {
                Resources resources = packageManager
                        .getResourcesForApplication(plugin.activityInfo.applicationInfo);
                int id = resources.getIdentifier("ic_menu_view", "drawable", plugin.activityInfo.packageName);
                if (id != 0)
                    icon = resources.getDrawable(id);
            } catch (Resources.NotFoundException e) {
                e.printStackTrace();
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }

            Intent intent = new Intent();
            intent.setClassName(plugin.activityInfo.packageName, plugin.activityInfo.name);
            Pair<Drawable, Intent> pair = new Pair<>(icon, intent);
            pluginViews.put(plugin.activityInfo.loadLabel(packageManager).toString(), pair);
        }
    }

    public void initializeOnlineMapProviders() {
        PackageManager packageManager = getPackageManager();

        Intent initializationIntent = new Intent("com.androzic.map.online.provider.action.INITIALIZE");
        // enumerate online map providers
        List<ResolveInfo> providers = packageManager.queryBroadcastReceivers(initializationIntent, 0);
        for (ResolveInfo provider : providers) {
            // send initialization broadcast, we send it directly instead of sending
            // one broadcast for all plugins to wake up stopped plugins:
            // http://developer.android.com/about/versions/android-3.1.html#launchcontrols
            Intent intent = new Intent();
            intent.setClassName(provider.activityInfo.packageName, provider.activityInfo.name);
            intent.setAction(initializationIntent.getAction());
            sendBroadcast(intent);

            List<TileProvider> tileProviders = TileProviderFactory.fromPlugin(packageManager, provider);
            for (TileProvider tileProvider : tileProviders) {
                tileProvider.tileExpiration = onlineMapTileExpiration;
            }
            onlineMaps.addAll(tileProviders);
        }
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        Resources resources = getResources();

        if (getString(R.string.pref_folder_data).equals(key)) {
            setDataPath(Androzic.PATH_DATA,
                    sharedPreferences.getString(key, resources.getString(R.string.def_folder_data)));
        } else if (getString(R.string.pref_folder_icon).equals(key)) {
            setDataPath(Androzic.PATH_ICONS,
                    sharedPreferences.getString(key, resources.getString(R.string.def_folder_icon)));
        } else if (getString(R.string.pref_unitcoordinate).equals(key)) {
            StringFormatter.coordinateFormat = Integer.parseInt(sharedPreferences.getString(key, "0"));
        } else if (getString(R.string.pref_unitdistance).equals(key)) {
            int distanceIdx = Integer.parseInt(sharedPreferences.getString(key, "0"));
            StringFormatter.distanceFactor = Double
                    .parseDouble(resources.getStringArray(R.array.distance_factors)[distanceIdx]);
            StringFormatter.distanceAbbr = resources.getStringArray(R.array.distance_abbrs)[distanceIdx];
            StringFormatter.distanceShortFactor = Double
                    .parseDouble(resources.getStringArray(R.array.distance_factors_short)[distanceIdx]);
            StringFormatter.distanceShortAbbr = resources.getStringArray(R.array.distance_abbrs_short)[distanceIdx];
        } else if (getString(R.string.pref_unitspeed).equals(key)) {
            int speedIdx = Integer.parseInt(sharedPreferences.getString(key, "0"));
            StringFormatter.speedFactor = Double
                    .parseDouble(resources.getStringArray(R.array.speed_factors)[speedIdx]);
            StringFormatter.speedAbbr = resources.getStringArray(R.array.speed_abbrs)[speedIdx];
        } else if (getString(R.string.pref_unitelevation).equals(key)) {
            int elevationIdx = Integer.parseInt(sharedPreferences.getString(key, "0"));
            StringFormatter.elevationFactor = Double
                    .parseDouble(resources.getStringArray(R.array.elevation_factors)[elevationIdx]);
            StringFormatter.elevationAbbr = resources.getStringArray(R.array.elevation_abbrs)[elevationIdx];
        } else if (getString(R.string.pref_unitangle).equals(key)) {
            int angleIdx = Integer.parseInt(sharedPreferences.getString(key, "0"));
            StringFormatter.angleFactor = Double
                    .parseDouble(resources.getStringArray(R.array.angle_factors)[angleIdx]);
            StringFormatter.angleAbbr = resources.getStringArray(R.array.angle_abbrs)[angleIdx];
        } else if (getString(R.string.pref_unitanglemagnetic).equals(key)) {
            angleMagnetic = sharedPreferences.getBoolean(key, resources.getBoolean(R.bool.def_unitanglemagnetic));
        } else if (getString(R.string.pref_unitsunrise).equals(key)) {
            sunriseType = Integer.parseInt(sharedPreferences.getString(key, "0"));
        } else if (getString(R.string.pref_unitprecision).equals(key)) {
            boolean precision = sharedPreferences.getBoolean(key, resources.getBoolean(R.bool.def_unitprecision));
            StringFormatter.precisionFormat = precision ? "%.1f" : "%.0f";
        } else if (getString(R.string.pref_grid_mapshow).equals(key)) {
            overlayManager.mapGrid = sharedPreferences.getBoolean(key, false);
            if (currentMap instanceof OzfMap)
                overlayManager.initGrids((OzfMap) currentMap);
        } else if (getString(R.string.pref_grid_usershow).equals(key)) {
            overlayManager.userGrid = sharedPreferences.getBoolean(key, false);
            if (currentMap instanceof OzfMap)
                overlayManager.initGrids((OzfMap) currentMap);
        } else if (getString(R.string.pref_grid_preference).equals(key)) {
            overlayManager.gridPrefer = Integer.parseInt(sharedPreferences.getString(key, "0"));
            if (currentMap instanceof OzfMap)
                overlayManager.initGrids((OzfMap) currentMap);
        } else if (getString(R.string.pref_grid_userscale).equals(key)
                || getString(R.string.pref_grid_userunit).equals(key)
                || getString(R.string.pref_grid_usermpp).equals(key)) {
            if (currentMap instanceof OzfMap)
                overlayManager.initGrids((OzfMap) currentMap);
        } else if (getString(R.string.pref_vectormap_theme).equals(key)
                || getString(R.string.pref_vectormap_poi).equals(key)) {
            initializeRenderTheme();
            ForgeMap.onRenderThemeChanged();
        } else if (getString(R.string.pref_vectormap_textscale).equals(key)) {
            ForgeMap.textScale = Float
                    .parseFloat(sharedPreferences.getString(getString(R.string.pref_vectormap_textscale), "1.0"));
            ForgeMap.onRenderThemeChanged();
        } else if (getString(R.string.pref_onlinemap).equals(key)
                || getString(R.string.pref_onlinemapscale).equals(key)) {
            setOnlineMaps(sharedPreferences.getString(getString(R.string.pref_onlinemap),
                    resources.getString(R.string.def_onlinemap)));
        } else if (getString(R.string.pref_mapadjacent).equals(key)) {
            adjacentMaps = sharedPreferences.getBoolean(key, resources.getBoolean(R.bool.def_mapadjacent));
        } else if (getString(R.string.pref_onlinemapprescalefactor).equals(key)) {
            onlineMapPrescaleFactor = sharedPreferences.getInt(key,
                    resources.getInteger(R.integer.def_onlinemapprescalefactor));
            if (maps != null)
                for (BaseMap map : maps.getMaps())
                    if (map instanceof OnlineMap)
                        ((OnlineMap) map).setPrescaleFactor(onlineMapPrescaleFactor);
            // Hack to recalculate cache and mpp
            if (currentMap != null && currentMap instanceof OnlineMap)
                currentMap.setZoom(currentMap.getZoom());
        } else if (getString(R.string.pref_onlinemapexpiration).equals(key)) {
            // in weeks
            onlineMapTileExpiration = sharedPreferences.getInt(key,
                    resources.getInteger(R.integer.def_onlinemapexpiration));
            // in milliseconds
            onlineMapTileExpiration *= 1000 * 3600 * 24 * 7;
            if (onlineMaps != null) {
                for (TileProvider provider : onlineMaps)
                    provider.tileExpiration = onlineMapTileExpiration;
            }
        } else if (getString(R.string.pref_mapcropborder).equals(key)) {
            cropMapBorder = sharedPreferences.getBoolean(key, resources.getBoolean(R.bool.def_mapcropborder));
        } else if (getString(R.string.pref_mapdrawborder).equals(key)) {
            drawMapBorder = sharedPreferences.getBoolean(key, resources.getBoolean(R.bool.def_mapdrawborder));
        } else if (getString(R.string.pref_showwaypoints).equals(key)) {
            overlayManager.setWaypointsOverlayEnabled(sharedPreferences.getBoolean(key, true));
        } else if (getString(R.string.pref_showcurrenttrack).equals(key)) {
            overlayManager.setCurrentTrackOverlayEnabled(sharedPreferences.getBoolean(key, true));
        } else if (getString(R.string.pref_showaccuracy).equals(key)) {
            overlayManager.setAccuracyOverlayEnabled(sharedPreferences.getBoolean(key, true));
        } else if (getString(R.string.pref_showdistance_int).equals(key)) {
            int showDistance = Integer
                    .parseInt(sharedPreferences.getString(key, getString(R.string.def_showdistance)));
            overlayManager.setDistanceOverlayEnabled(showDistance > 0);
        }
        overlayManager.onPreferencesChanged(sharedPreferences);
        if (mapHolder != null)
            mapHolder.refreshMap();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (locale != null) {
            newConfig.locale = locale;
            Locale.setDefault(locale);
            getBaseContext().getResources().updateConfiguration(newConfig,
                    getBaseContext().getResources().getDisplayMetrics());
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.e(TAG, "Application onCreate()");
        onCreateEx();
    }

    public void onCreateEx() {
        try {
            (new File(Environment.getExternalStorageDirectory(), WordManager.FOLDER)).mkdirs();
            (new File(Environment.getExternalStorageDirectory(), WordManager.FOLDER + "/ghiam")).mkdirs();
        } catch (Throwable e) {
        }

        if (initialized)
            return;

        AndroidGraphicFactory.createInstance(this);
        try {
            OzfDecoder.useNativeCalls();
        } catch (UnsatisfiedLinkError e) {
            Toast.makeText(Androzic.this, "Failed to initialize native library: " + e.getMessage(),
                    Toast.LENGTH_LONG).show();
        }

        Resources resources = getResources();
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
        Configuration config = resources.getConfiguration();

        renderingThread = new HandlerThread("RenderingThread");
        renderingThread.start();

        longOperationsThread = new HandlerThread("LongOperationsThread");
        longOperationsThread.setPriority(Thread.MIN_PRIORITY);
        longOperationsThread.start();

        uiHandler = new Handler();
        mapsHandler = new Handler(longOperationsThread.getLooper());

        // We silently initialize data uri to let location service restart after crash
        File datadir = new File(
                settings.getString(getString(R.string.pref_folder_data), Environment.getExternalStorageDirectory()
                        + File.separator + resources.getString(R.string.def_folder_data)));
        setDataPath(Androzic.PATH_DATA, datadir.getAbsolutePath());

        setInstance(this);

        String intentToCheck = "com.androzic.donate";
        String myPackageName = getPackageName();
        PackageManager pm = getPackageManager();
        PackageInfo pi;
        try {
            pi = pm.getPackageInfo(intentToCheck, 0);
            isPaid = (pm.checkSignatures(myPackageName, pi.packageName) == PackageManager.SIGNATURE_MATCH);
        } catch (NameNotFoundException e) {
            //         e.printStackTrace();
        }

        File sdcard = Environment.getExternalStorageDirectory();
        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this, sdcard.getAbsolutePath()));

        DisplayMetrics displayMetrics = new DisplayMetrics();

        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        if (wm != null) {
            wm.getDefaultDisplay().getMetrics(displayMetrics);
        } else {
            displayMetrics.setTo(resources.getDisplayMetrics());
        }
        BaseMap.viewportWidth = displayMetrics.widthPixels;
        BaseMap.viewportHeight = displayMetrics.heightPixels;

        charset = settings.getString(getString(R.string.pref_charset), "UTF-8");
        String lang = settings.getString(getString(R.string.pref_locale), "");
        if (!"".equals(lang) && !config.locale.getLanguage().equals(lang)) {
            locale = new Locale(lang);
            Locale.setDefault(locale);
            config.locale = locale;
            resources.updateConfiguration(config, resources.getDisplayMetrics());
        }

        magInterval = resources.getInteger(R.integer.def_maginterval) * 1000;

        overlayManager = new OverlayManager(longOperationsThread.getLooper());
        TooltipManager.initialize(this);

        onSharedPreferenceChanged(settings, getString(R.string.pref_unitcoordinate));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitdistance));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitspeed));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitelevation));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitangle));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitanglemagnetic));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitprecision));
        onSharedPreferenceChanged(settings, getString(R.string.pref_unitsunrise));
        onSharedPreferenceChanged(settings, getString(R.string.pref_mapadjacent));
        onSharedPreferenceChanged(settings, getString(R.string.pref_vectormap_textscale));
        onSharedPreferenceChanged(settings, getString(R.string.pref_onlinemapprescalefactor));
        onSharedPreferenceChanged(settings, getString(R.string.pref_onlinemapexpiration));
        onSharedPreferenceChanged(settings, getString(R.string.pref_mapcropborder));
        onSharedPreferenceChanged(settings, getString(R.string.pref_mapdrawborder));
        onSharedPreferenceChanged(settings, getString(R.string.pref_showwaypoints));
        onSharedPreferenceChanged(settings, getString(R.string.pref_showcurrenttrack));
        onSharedPreferenceChanged(settings, getString(R.string.pref_showaccuracy));
        onSharedPreferenceChanged(settings, getString(R.string.pref_showdistance_int));

        settings.registerOnSharedPreferenceChangeListener(this);

        initialized = true;
    }

    private void clearMaps() {
        setOnlineMaps("");
        ForgeMap.clear();
        if (coveringMaps != null) {
            for (BaseMap map : coveringMaps)
                map.deactivate();
            coveringMaps.clear();
            coveringMaps = null;
        }
        if (currentMap != null)
            currentMap.deactivate();
        suitableMaps.clear();
        maps.clear();
        onlineMaps = null;
        mapHolder = null;
        currentMap = null;
        suitableMaps = null;
        maps = null;
        mapsInited = false;
    }

    @SuppressLint("NewApi")
    public void clear() {
        Log.e(TAG, "clear()");
        mapsHandler.removeMessages(1);
        longOperationsThread.interrupt();

        Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();

        // save last location
        editor.putString(getString(R.string.loc_last),
                StringFormatter.coordinates(0, " ", mapCenter[0], mapCenter[1]));
        editor.commit();

        Log.w(TAG, "  stopping plugins...");
        // send finalization broadcast
        sendBroadcast(new Intent("com.androzic.plugins.action.FINALIZE"));

        // clear services
        unregisterReceiver(broadcastReceiver);

        Log.w(TAG, "  clearing overlays...");
        overlayManager.clear();

        Log.w(TAG, "  stopping services...");
        if (navigationService != null) {
            if (navigationService.isNavigatingViaRoute() && navigationService.navRoute.filepath != null) {
                // save active route point
                editor.putInt(getString(R.string.nav_route_wpt), navigationService.navCurrentRoutePoint);
                editor.commit();
            }
            unbindService(navigationConnection);
            navigationService = null;
        }

        if (locationService != null) {
            locationService.unregisterLocationCallback(locationListener);
            unbindService(locationConnection);
            locationService = null;
        }

        stopService(new Intent(this, NavigationService.class));
        stopService(new Intent(this, LocationService.class));

        Log.w(TAG, "  saving data...");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // save opened waypoint sets
            HashSet<String> sets = new HashSet<>();
            for (int i = 1; i < waypointSets.size(); i++) {
                WaypointSet set = waypointSets.get(i);
                if (set.path != null)
                    sets.add(set.path);
            }
            editor.putStringSet(getString(R.string.wpt_sets), sets);
            editor.commit();
        }

        Log.w(TAG, "  clearing data...");
        // clear data
        clearRoutes();
        clearTracks();
        clearWaypoints();
        clearWaypointSets();
        clearMapObjects();

        Log.w(TAG, "  clearing maps...");
        clearMaps();

        Log.w(TAG, "  stopping threads...");
        uiHandler.removeCallbacksAndMessages(null);
        mapsHandler.removeCallbacksAndMessages(null);
        longOperationsThread.quit();
        longOperationsThread = null;

        memmsg = false;
        cacheDir = null;
        initialized = false;

        Log.w(TAG, "  finished clearing");
    }

    @Override
    public Set<String> getCategories(XmlRenderThemeStyleMenu menuStyle) {
        Log.e(TAG, "RenderTheme getCategories()");
        xmlRenderThemeStyleMenu = menuStyle;

        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);

        String id = settings.getString(getString(R.string.pref_vectormap_theme),
                xmlRenderThemeStyleMenu.getDefaultValue());
        Log.e(TAG, "id: " + id);
        XmlRenderThemeStyleLayer baseLayer = menuStyle.getLayer(id);
        if (baseLayer == null) {
            Log.e(TAG, "Invalid forgemap style: " + id);
            return null;
        }
        Set<String> result = baseLayer.getCategories();

        List<String> selectedPlaces;
        String places = settings.getString(getString(R.string.pref_vectormap_poi), "---");
        Log.e(TAG, "Places: " + places);
        if ("---".equals(places)) {
            selectedPlaces = new ArrayList<>();
            for (XmlRenderThemeStyleLayer overlay : baseLayer.getOverlays()) {
                if (overlay.isEnabled())
                    selectedPlaces.add(overlay.getId());
            }
        } else {
            selectedPlaces = Arrays.asList(places.split("\\|"));
        }
        Log.e(TAG, "Selected places: " + Arrays.toString(selectedPlaces.toArray()));

        // add the categories from overlays that are enabled
        for (XmlRenderThemeStyleLayer overlay : baseLayer.getOverlays()) {
            if (selectedPlaces.contains(overlay.getId()))
                result.addAll(overlay.getCategories());
        }

        return result;
    }

    private class MapActivationError implements Runnable {
        private BaseMap map;
        private Throwable e;

        MapActivationError(BaseMap map, Throwable e) {
            this.map = map;
            this.e = e;
        }

        @Override
        public void run() {
            Toast.makeText(Androzic.this, map.path + ": " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

}