/*
 * 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.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;
	protected Route editingRoute = null;
	protected Track editingTrack = null;
	protected 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;
	protected 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;
	}

	protected 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()
	{
		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();
		}
	}
}