public interface ILocationService
{
	void registerLocationCallback(ILocationListener callback);
	void unregisterLocationCallback(ILocationListener callback);
	void registerTrackingCallback(ITrackingListener callback);
	void unregisterTrackingCallback(ITrackingListener callback);
	boolean isLocating();
	boolean isTracking();
	float getHDOP();
	float getVDOP();
	Track getTrack();
	Track getTrack(long start, long end);
	void clearTrack();
	long getTrackStartTime();
	long getTrackEndTime();
} public class LocationService extends BaseLocationService implements LocationListener, NmeaListener, GpsStatus.Listener, OnSharedPreferenceChangeListener
{
	private static final String TAG = "Location";
	private static final int NOTIFICATION_ID = 24161;
	private static final boolean DEBUG_ERRORS = false;

	/**
	 * Intent action to enable locating
	 */
	public static final String ENABLE_LOCATIONS = "enableLocations";
	/**
	 * Intent action to disable locating
	 */
	public static final String DISABLE_LOCATIONS = "disableLocations";

	public static final String ENABLE_TRACK = "enableTrack";
	public static final String DISABLE_TRACK = "disableTrack";

	public static final String BROADCAST_TRACKING_STATUS = "com.androzic.trackingStatusChanged";

	private boolean locationsEnabled = false;
	//FIXME Use this to permanently loose location
	private int gpsLocationTimeout = 120000;
	// Used for test purposes
	private static final boolean enableMockLocations = false;
	private Handler mockCallback = new Handler();

	private LocationManager locationManager = null;

	private int gpsStatus = GPS_OFF;

	private float[] speed = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
	private float[] speedav = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
	private float[] speedavex = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

	private long lastLocationMillis = 0;
	private long tics = 0;
	private int pause = 1;

	private Location lastKnownLocation = null;
	private boolean isContinous = false;
	private boolean justStarted = true;
	private float smoothSpeed = 0.0f;
	private float avgSpeed = 0.0f;
	private float nmeaGeoidHeight = Float.NaN;
	private float HDOP = Float.NaN;
	private float VDOP = Float.NaN;

	private SQLiteDatabase trackDB = null;
	private boolean trackingEnabled = false;
	private String errorMsg = "";
	private long errorTime = 0;

	private Location lastWritenLocation = null;
	private Location lastLocation = null;
	private double distanceFromLastWriting = 0;
	private long timeFromLastWriting = 0;

	private long minTime = 2000; // 2 seconds (default)
	private long maxTime = 300000; // 5 minutes
	private int minDistance = 3; // 3 meters (default)

	private final Binder binder = new LocalBinder();
	private final RemoteCallbackList<ILocationCallback> locationRemoteCallbacks = new RemoteCallbackList<ILocationCallback>();
	private final Set<ILocationListener> locationCallbacks = new HashSet<ILocationListener>();
	private final RemoteCallbackList<ITrackingCallback> trackingRemoteCallbacks = new RemoteCallbackList<ITrackingCallback>();
	private final Set<ITrackingListener> trackingCallbacks = new HashSet<ITrackingListener>();

	@Override
	public void onCreate()
	{
		super.onCreate();

		lastKnownLocation = new Location("unknown");

		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
		// Location preferences
		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_loc_gpstimeout));
		// Tracking preferences
		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_tracking_mintime));
		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_tracking_mindistance));

		sharedPreferences.registerOnSharedPreferenceChangeListener(this);

		Log.i(TAG, "Service started");
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		if (intent == null || intent.getAction() == null)
			return 0;

		if (intent.getAction().equals(ENABLE_LOCATIONS) && !locationsEnabled)
		{
			locationsEnabled = true;
			connect();
			sendBroadcast(new Intent(BROADCAST_LOCATING_STATUS));
			if (trackingEnabled)
			{
				sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
			}
		}
		if (intent.getAction().equals(DISABLE_LOCATIONS) && locationsEnabled)
		{
			locationsEnabled = false;
			disconnect();
			updateProvider(LocationManager.GPS_PROVIDER, false);
			sendBroadcast(new Intent(BROADCAST_LOCATING_STATUS));
			if (trackingEnabled)
			{
				closeDatabase();
				sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
			}
		}
		if (intent.getAction().equals(ENABLE_TRACK) && !trackingEnabled)
		{
			errorMsg = "";
			errorTime = 0;
			trackingEnabled = true;
			isContinous = false;
			openDatabase();
			sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
		}
		if (intent.getAction().equals(DISABLE_TRACK) && trackingEnabled)
		{
			trackingEnabled = false;
			closeDatabase();
			errorMsg = "";
			errorTime = 0;
			sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
		}
		updateNotification();

		return START_REDELIVER_INTENT | START_STICKY;
	}

	@Override
	public void onDestroy()
	{
		super.onDestroy();
		PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
		disconnect();
		closeDatabase();
		Log.i(TAG, "Service stopped");
	}

	private final ILocationRemoteService.Stub locationRemoteBinder = new ILocationRemoteService.Stub() {
		public void registerCallback(ILocationCallback cb)
		{
			Log.i(TAG, "Register callback");
			if (cb != null)
				locationRemoteCallbacks.register(cb);
		}

		public void unregisterCallback(ILocationCallback cb)
		{
			if (cb != null)
				locationRemoteCallbacks.unregister(cb);
		}

		public boolean isLocating()
		{
			return locationsEnabled;
		}
	};

	private final ITrackingRemoteService.Stub trackingRemoteBinder = new ITrackingRemoteService.Stub() {
		public void registerCallback(ITrackingCallback cb)
		{
			Log.i(TAG, "Register callback");
			if (cb != null)
				trackingRemoteCallbacks.register(cb);
		}

		public void unregisterCallback(ITrackingCallback cb)
		{
			if (cb != null)
				trackingRemoteCallbacks.unregister(cb);
		}
	};

	@Override
	public IBinder onBind(Intent intent)
	{
		if (ANDROZIC_LOCATION_SERVICE.equals(intent.getAction()) || ILocationRemoteService.class.getName().equals(intent.getAction()))
		{
			return locationRemoteBinder;
		}
		if ("com.androzic.tracking".equals(intent.getAction()) || ITrackingRemoteService.class.getName().equals(intent.getAction()))
		{
			return trackingRemoteBinder;
		}
		else
		{
			return binder;
		}
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
	{
		if (getString(R.string.pref_loc_gpstimeout).equals(key))
		{
			gpsLocationTimeout = 1000 * sharedPreferences.getInt(key, getResources().getInteger(R.integer.def_loc_gpstimeout));
		}
		else if (getString(R.string.pref_tracking_mintime).equals(key))
		{
			try
			{
				minTime = Integer.parseInt(sharedPreferences.getString(key, "500"));
			}
			catch (NumberFormatException e)
			{
			}
		}
		else if (getString(R.string.pref_tracking_mindistance).equals(key))
		{
			try
			{
				minDistance = Integer.parseInt(sharedPreferences.getString(key, "5"));
			}
			catch (NumberFormatException e)
			{
			}
		}
		else if (getString(R.string.pref_folder_data).equals(key))
		{
			closeDatabase();
			openDatabase();
		}
	}

	private void connect()
	{
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		if (locationManager != null)
		{
			lastLocationMillis = 0;
			pause = 1;
			isContinous = false;
			justStarted = true;
			smoothSpeed = 0.0f;
			avgSpeed = 0.0f;
			locationManager.addGpsStatusListener(this);
			try
			{
				locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
				locationManager.addNmeaListener(this);
				Log.d(TAG, "Gps provider set");
			}
			catch (IllegalArgumentException e)
			{
				Log.d(TAG, "Cannot set gps provider, likely no gps on device");
			}
			startForeground(NOTIFICATION_ID, getNotification());
		}
		if (enableMockLocations)
		{
			updateProvider(LocationManager.GPS_PROVIDER, true);
			mockCallback.post(sendMockLocation);
		}
	}

	private void disconnect()
	{
		if (locationManager != null)
		{
			locationManager.removeNmeaListener(this);
			locationManager.removeUpdates(this);
			locationManager.removeGpsStatusListener(this);
			locationManager = null;
			stopForeground(true);
		}
		if (enableMockLocations)
		{
			mockCallback.removeCallbacks(sendMockLocation);
			updateProvider(LocationManager.GPS_PROVIDER, false);
		}
	}

	@SuppressWarnings("unused")
	private Notification getNotification()
	{
		int msgId = R.string.notif_loc_started;
		int ntfId = R.drawable.ic_stat_locating;
		if (trackingEnabled)
		{
			msgId = R.string.notif_trk_started;
			ntfId = R.drawable.ic_stat_tracking;
		}
		if (gpsStatus != LocationService.GPS_OK)
		{
			msgId = R.string.notif_loc_waiting;
			ntfId = R.drawable.ic_stat_waiting;
		}
		if (gpsStatus == LocationService.GPS_OFF)
		{
			ntfId = R.drawable.ic_stat_off;
		}
		if (errorTime > 0)
		{
			msgId = R.string.notif_trk_failure;
			ntfId = R.drawable.ic_stat_failure;
		}

		NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
		builder.setWhen(errorTime);
		builder.setSmallIcon(ntfId);
		Intent intent = new Intent(Intent.ACTION_MAIN);
		intent.addCategory(Intent.CATEGORY_LAUNCHER);
		intent.setComponent(new ComponentName(getApplicationContext(), Splash.class));
		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
		builder.setContentIntent(PendingIntent.getActivity(this, NOTIFICATION_ID, intent, 0));
		builder.setContentTitle(getText(R.string.notif_loc_short));
		builder.setGroup("androzic");
		builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
		builder.setPriority(NotificationCompat.PRIORITY_LOW);
		builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
		builder.setColor(getResources().getColor(R.color.theme_accent_color));
		if (errorTime > 0 && DEBUG_ERRORS)
			builder.setContentText(errorMsg);
		else
			builder.setContentText(getText(msgId));
		builder.setOngoing(true);

		return builder.build();
	}

	private void updateNotification()
	{
		if (locationManager != null)
		{
			NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
			notificationManager.notify(NOTIFICATION_ID, getNotification());
		}
	}

	private void openDatabase()
	{
		Androzic application = Androzic.getApplication();
		if (application.dataPath == null)
		{
			Log.e(TAG, "Data path is null");
			errorMsg = "Data path is null";
			errorTime = System.currentTimeMillis();
			updateNotification();
			return;
		}
		File dir = new File(application.dataPath);
		if (!dir.exists() && !dir.mkdirs())
		{
			Log.e(TAG, "Failed to create data folder");
			errorMsg = "Failed to create data folder";
			errorTime = System.currentTimeMillis();
			updateNotification();
			return;
		}
		File path = new File(dir, "myTrack.db");
		try
		{
			trackDB = SQLiteDatabase.openDatabase(path.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
			Cursor cursor = trackDB.rawQuery("SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name = 'track'", null);
			if (cursor.getCount() == 0)
			{
				trackDB.execSQL("CREATE TABLE track (_id INTEGER PRIMARY KEY, latitude REAL, longitude REAL, code INTEGER, elevation REAL, speed REAL, track REAL, accuracy REAL, datetime INTEGER)");
			}
			cursor.close();
		}
		catch (SQLiteException e)
		{
			trackDB = null;
			Log.e(TAG, "openDatabase", e);
			errorMsg = "Failed to open DB";
			errorTime = System.currentTimeMillis();
			updateNotification();
		}
	}

	private void closeDatabase()
	{
		if (trackDB != null)
		{
			trackDB.close();
			trackDB = null;
		}
	}

	public Track getTrack()
	{
		return getTrack(0);
	}
	
	public Track getTrack(long limit)
	{
		if (trackDB == null)
			openDatabase();
		Track track = new Track();
		if (trackDB == null)
			return track;
		String limitStr = limit > 0 ? " LIMIT " + limit : "";
		Cursor cursor = trackDB.rawQuery("SELECT * FROM track ORDER BY _id DESC" + limitStr, null);
		for (boolean hasItem = cursor.moveToLast(); hasItem; hasItem = cursor.moveToPrevious())
		{
			double latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
			double longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
			double elevation = cursor.getDouble(cursor.getColumnIndex("elevation"));
			double speed = cursor.getDouble(cursor.getColumnIndex("speed"));
			double bearing = cursor.getDouble(cursor.getColumnIndex("track"));
			double accuracy = cursor.getDouble(cursor.getColumnIndex("accuracy"));
			int code = cursor.getInt(cursor.getColumnIndex("code"));
			long time = cursor.getLong(cursor.getColumnIndex("datetime"));
			track.addPoint(code == 0, latitude, longitude, elevation, speed, bearing, accuracy, time);
		}
		cursor.close();
		return track;
	}

	public Track getTrack(long start, long end)
	{
		if (trackDB == null)
			openDatabase();
		Track track = new Track();
		if (trackDB == null)
			return track;
		Cursor cursor = trackDB.rawQuery("SELECT * FROM track WHERE datetime >= ? AND datetime <= ? ORDER BY _id DESC", new String[] {String.valueOf(start), String.valueOf(end)});
		for (boolean hasItem = cursor.moveToLast(); hasItem; hasItem = cursor.moveToPrevious())
		{
			double latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
			double longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
			double elevation = cursor.getDouble(cursor.getColumnIndex("elevation"));
			double speed = cursor.getDouble(cursor.getColumnIndex("speed"));
			double bearing = cursor.getDouble(cursor.getColumnIndex("track"));
			double accuracy = cursor.getDouble(cursor.getColumnIndex("accuracy"));
			int code = cursor.getInt(cursor.getColumnIndex("code"));
			long time = cursor.getLong(cursor.getColumnIndex("datetime"));
			track.addPoint(code == 0, latitude, longitude, elevation, speed, bearing, accuracy, time);
		}
		cursor.close();
		return track;
	}

	public long getTrackStartTime()
	{
		long res = Long.MIN_VALUE;
		if (trackDB == null)
			openDatabase();
		if (trackDB == null)
			return res;
		Cursor cursor = trackDB.rawQuery("SELECT MIN(datetime) FROM track WHERE datetime > 0", null);
		if (cursor.moveToFirst())
			res = cursor.getLong(0);
		cursor.close();
		return res;
	}

	public long getTrackEndTime()
	{
		long res = Long.MAX_VALUE;
		if (trackDB == null)
			openDatabase();
		if (trackDB == null)
			return res;
		Cursor cursor = trackDB.rawQuery("SELECT MAX(datetime) FROM track", null);
		if (cursor.moveToFirst())
			res = cursor.getLong(0);
		cursor.close();
		return res;
	}

	public void clearTrack()
	{
		if (trackDB == null)
			openDatabase();
		if (trackDB != null)
			trackDB.execSQL("DELETE FROM track");
	}

	public void addPoint(boolean continous, double latitude, double longitude, double elevation, float speed, float bearing, float accuracy, long time)
	{
		if (trackDB == null)
		{
			openDatabase();
			if (trackDB == null)
				return;
		}

		ContentValues values = new ContentValues();
		values.put("latitude", latitude);
		values.put("longitude", longitude);
		values.put("code", continous ? 0 : 1);
		values.put("elevation", elevation);
		values.put("speed", speed);
		values.put("track", bearing);
		values.put("accuracy", accuracy);
		values.put("datetime", time);

		try
		{
			trackDB.insertOrThrow("track", null, values);
		}
		catch (SQLException e)
		{
			Log.e(TAG, "addPoint", e);
			errorMsg = e.getMessage();
			errorTime = System.currentTimeMillis();
			updateNotification();
			closeDatabase();
		}
	}

	private void writeLocation(final Location loc, final boolean continous)
	{
		Log.d(TAG, "Fix needs writing");
		lastWritenLocation = loc;
		distanceFromLastWriting = 0;
		addPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());

		for (ITrackingListener callback : trackingCallbacks)
		{
			callback.onNewPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());
		}

		final int n = trackingRemoteCallbacks.beginBroadcast();
		for (int i = 0; i < n; i++)
		{
			final ITrackingCallback callback = trackingRemoteCallbacks.getBroadcastItem(i);
			try
			{
				callback.onNewPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());
			}
			catch (RemoteException e)
			{
				Log.e(TAG, "Point broadcast error", e);
			}
		}
		trackingRemoteCallbacks.finishBroadcast();
	}

	private void writeTrack(Location loc, boolean continous, boolean geoid, float smoothspeed, float avgspeed)
	{
		boolean needsWrite = false;
		if (lastLocation != null)
		{
			distanceFromLastWriting += loc.distanceTo(lastLocation);
		}
		if (lastWritenLocation != null)
			timeFromLastWriting = loc.getTime() - lastWritenLocation.getTime();

		if (lastLocation == null || lastWritenLocation == null || !continous || timeFromLastWriting > maxTime || distanceFromLastWriting > minDistance && timeFromLastWriting > minTime)
		{
			needsWrite = true;
		}

		lastLocation = loc;

		if (needsWrite)
			writeLocation(loc, continous);
	}

	private void tearTrack()
	{
		if (lastLocation != null && (lastWritenLocation == null || !lastLocation.toString().equals(lastWritenLocation.toString())))
			writeLocation(lastLocation, isContinous);
		isContinous = false;
	}

	private void updateLocation()
	{
		final Location location = lastKnownLocation;
		final boolean continous = isContinous;
		final boolean geoid = !Float.isNaN(nmeaGeoidHeight);
		final float smoothspeed = smoothSpeed;
		final float avgspeed = avgSpeed;

		final Handler handler = new Handler();

		if (trackingEnabled)
		{
			handler.post(new Runnable() {
				@Override
				public void run()
				{
					writeTrack(location, continous, geoid, smoothspeed, avgspeed);
				}
			});
		}
		for (final ILocationListener callback : locationCallbacks)
		{
			handler.post(new Runnable() {
				@Override
				public void run()
				{
					callback.onLocationChanged(location, continous, geoid, smoothspeed, avgspeed);
				}
			});
		}
		final int n = locationRemoteCallbacks.beginBroadcast();
		for (int i = 0; i < n; i++)
		{
			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
			try
			{
				callback.onLocationChanged(location, continous, geoid, smoothspeed, avgspeed);
			}
			catch (RemoteException e)
			{
				Log.e(TAG, "Location broadcast error", e);
			}
		}
		locationRemoteCallbacks.finishBroadcast();
		Log.d(TAG, "Location dispatched: " + (locationCallbacks.size() + n));
	}

	private void updateLocation(final ILocationListener callback)
	{
		if (!"unknown".equals(lastKnownLocation.getProvider()))
			callback.onLocationChanged(lastKnownLocation, isContinous, !Float.isNaN(nmeaGeoidHeight), smoothSpeed, avgSpeed);
	}

	private void updateProvider(final String provider, final boolean enabled)
	{
		if (LocationManager.GPS_PROVIDER.equals(provider))
			updateNotification();
		final Handler handler = new Handler();
		for (final ILocationListener callback : locationCallbacks)
		{
			handler.post(new Runnable() {
				@Override
				public void run()
				{
					if (enabled)
						callback.onProviderEnabled(provider);
					else
						callback.onProviderDisabled(provider);
				}
			});
		}
		final int n = locationRemoteCallbacks.beginBroadcast();
		for (int i = 0; i < n; i++)
		{
			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
			try
			{
				if (enabled)
					callback.onProviderEnabled(provider);
				else
					callback.onProviderDisabled(provider);
			}
			catch (RemoteException e)
			{
				Log.e(TAG, "Provider broadcast error", e);
			}
		}
		locationRemoteCallbacks.finishBroadcast();
		Log.d(TAG, "Provider status dispatched: " + (locationCallbacks.size() + n));
	}

	private void updateProvider(final ILocationListener callback)
	{
		if (gpsStatus == GPS_OFF)
			callback.onProviderDisabled(LocationManager.GPS_PROVIDER);
		else
			callback.onProviderEnabled(LocationManager.GPS_PROVIDER);
	}

	private void updateGpsStatus(final int status, final int fsats, final int tsats)
	{
		gpsStatus = status;
		updateNotification();
		final Handler handler = new Handler();
		for (final ILocationListener callback : locationCallbacks)
		{
			handler.post(new Runnable() {
				@Override
				public void run()
				{
					callback.onGpsStatusChanged(LocationManager.GPS_PROVIDER, status, fsats, tsats);
				}
			});
		}
		final int n = locationRemoteCallbacks.beginBroadcast();
		for (int i = 0; i < n; i++)
		{
			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
			try
			{
				callback.onGpsStatusChanged(LocationManager.GPS_PROVIDER, status, fsats, tsats);
			}
			catch (RemoteException e)
			{
				Log.e(TAG, "Status broadcast error", e);
			}
		}
		locationRemoteCallbacks.finishBroadcast();
		Log.d(TAG, "GPS status dispatched: " + (locationCallbacks.size() + n));
	}

	@Override
	public void onLocationChanged(final Location location)
	{
		if (enableMockLocations)
			return;
		
		tics++;

		boolean sendUpdate = false;

		long time = SystemClock.elapsedRealtime();

		// Log.i(TAG, "Location arrived: "+location.toString());

		Log.d(TAG, "Fix arrived");

		long prevLocationMillis = lastLocationMillis;
		float prevSpeed = lastKnownLocation.getSpeed();
		float prevTrack = lastKnownLocation.getBearing();

		lastKnownLocation = location;

		if (lastKnownLocation.getSpeed() == 0 && prevTrack != 0)
		{
			lastKnownLocation.setBearing(prevTrack);
		}

		lastLocationMillis = time;
		sendUpdate = true;

		if (!Float.isNaN(nmeaGeoidHeight))
		{
			lastKnownLocation.setAltitude(lastKnownLocation.getAltitude() + nmeaGeoidHeight);
		}

		if (justStarted)
		{
			justStarted = prevSpeed == 0;
		}
		else if (lastKnownLocation.getSpeed() > 0)
		{
			// filter speed outrages
			double a = 2 * 9.8 * (lastLocationMillis - prevLocationMillis) / 1000;
			if (Math.abs(lastKnownLocation.getSpeed() - prevSpeed) > a)
				lastKnownLocation.setSpeed(prevSpeed);
		}

		// smooth speed
		float smoothspeed = 0;
		float curspeed = lastKnownLocation.getSpeed();
		for (int i = speed.length - 1; i > 1; i--)
		{
			smoothspeed += speed[i];
			speed[i] = speed[i - 1];
		}
		smoothspeed += speed[1];
		if (speed[1] < speed[0] && speed[0] > curspeed)
		{
			speed[0] = (speed[1] + curspeed) / 2;
		}
		smoothspeed += speed[0];
		speed[1] = speed[0];
		lastKnownLocation.setSpeed(speed[1]);
		speed[0] = curspeed;
		if (speed[0] == 0 && speed[1] == 0)
			smoothspeed = 0;
		else
			smoothspeed = smoothspeed / speed.length;

		// average speed
		float avspeed = 0;
		for (int i = speedav.length - 1; i >= 0; i--)
		{
			avspeed += speedav[i];
		}
		avspeed = avspeed / speedav.length;
		if (tics % pause == 0)
		{
			if (avspeed > 0)
			{
				float diff = curspeed / avspeed;
				if (0.95 < diff && diff < 1.05)
				{
					for (int i = speedav.length - 1; i > 0; i--)
					{
						speedav[i] = speedav[i - 1];
					}
					speedav[0] = curspeed;
				}
			}
			float fluct = 0;
			for (int i = speedavex.length - 1; i > 0; i--)
			{
				fluct += speedavex[i] / curspeed;
				speedavex[i] = speedavex[i - 1];
			}
			fluct += speedavex[0] / curspeed;
			speedavex[0] = curspeed;
			fluct = fluct / speedavex.length;
			if (0.95 < fluct && fluct < 1.05)
			{
				for (int i = speedav.length - 1; i >= 0; i--)
				{
					speedav[i] = speedavex[i];
				}
				if (pause < 5)
					pause++;
			}
		}

		smoothSpeed = smoothspeed;
		avgSpeed = avspeed;

		if (sendUpdate)
			updateLocation();

		isContinous = true;
	}

	@Override
	public void onNmeaReceived(long timestamp, String nmea)
	{
		if (nmea.indexOf('\n') == 0)
			return;
		if (nmea.indexOf('\n') > 0)
		{
			nmea = nmea.substring(0, nmea.indexOf('\n') - 1);
		}
		int len = nmea.length();
		if (len < 9)
		{
			return;
		}
		if (nmea.charAt(len - 3) == '*')
		{
			nmea = nmea.substring(0, len - 3);
		}
		String[] tokens = nmea.split(",");
		String sentenceId = tokens[0].length() > 5 ? tokens[0].substring(3, 6) : "";

		try
		{
			if (sentenceId.equals("GGA") && tokens.length > 11)
			{
				// String time = tokens[1];
				// String latitude = tokens[2];
				// String latitudeHemi = tokens[3];
				// String longitude = tokens[4];
				// String longitudeHemi = tokens[5];
				// String fixQuality = tokens[6];
				// String numSatellites = tokens[7];
				// String horizontalDilutionOfPrecision = tokens[8];
				// String altitude = tokens[9];
				// String altitudeUnits = tokens[10];
				String heightOfGeoid = tokens[11];
				if (!"".equals(heightOfGeoid))
					nmeaGeoidHeight = Float.parseFloat(heightOfGeoid);
				// String heightOfGeoidUnits = tokens[12];
				// String timeSinceLastDgpsUpdate = tokens[13];
			}
			else if (sentenceId.equals("GSA") && tokens.length > 17)
			{
				// String selectionMode = tokens[1]; // m=manual, a=auto 2d/3d
				// String mode = tokens[2]; // 1=no fix, 2=2d, 3=3d
				@SuppressWarnings("unused")
				String pdop = tokens[15];
				String hdop = tokens[16];
				String vdop = tokens[17];
				if (!"".equals(hdop))
					HDOP = Float.parseFloat(hdop);
				if (!"".equals(vdop))
					VDOP = Float.parseFloat(vdop);
			}
		}
		catch (NumberFormatException e)
		{
			Log.e(TAG, "NFE", e);
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			Log.e(TAG, "AIOOBE", e);
		}
	}

	@Override
	public void onProviderDisabled(String provider)
	{
		updateProvider(provider, false);
	}

	@Override
	public void onProviderEnabled(String provider)
	{
		updateProvider(provider, true);
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras)
	{
		if (enableMockLocations)
			return;

		if (LocationManager.GPS_PROVIDER.equals(provider))
		{
			switch (status)
			{
				case LocationProvider.TEMPORARILY_UNAVAILABLE:
				case LocationProvider.OUT_OF_SERVICE:
					tearTrack();
					updateNotification();
					break;
			}
		}
	}

	@Override
	public void onGpsStatusChanged(int event)
	{
		if (enableMockLocations)
			return;

		switch (event)
		{
			case GpsStatus.GPS_EVENT_STARTED:
				updateProvider(LocationManager.GPS_PROVIDER, true);
				updateGpsStatus(GPS_SEARCHING, 0, 0);
				break;
			case GpsStatus.GPS_EVENT_FIRST_FIX:
				isContinous = false;
				break;
			case GpsStatus.GPS_EVENT_STOPPED:
				tearTrack();
				updateGpsStatus(GPS_OFF, 0, 0);
				updateProvider(LocationManager.GPS_PROVIDER, false);
				break;
			case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
				if (locationManager == null)
					return;
				GpsStatus gpsStatus = locationManager.getGpsStatus(null);
				Iterator<GpsSatellite> it = gpsStatus.getSatellites().iterator();
				int tSats = 0;
				int fSats = 0;
				while (it.hasNext())
				{
					tSats++;
					GpsSatellite sat = (GpsSatellite) it.next();
					if (sat.usedInFix())
						fSats++;
				}
				if (SystemClock.elapsedRealtime() - lastLocationMillis < 3000)
				{
					updateGpsStatus(GPS_OK, fSats, tSats);
				}
				else
				{
					tearTrack();
					updateGpsStatus(GPS_SEARCHING, fSats, tSats);
				}
				break;
		}
	}

	public class LocalBinder extends Binder implements ILocationService
	{
		@Override
		public void registerLocationCallback(ILocationListener callback)
		{
			updateProvider(callback);
			updateLocation(callback);
			locationCallbacks.add(callback);
		}

		@Override
		public void unregisterLocationCallback(ILocationListener callback)
		{
			locationCallbacks.remove(callback);
		}

		@Override
		public void registerTrackingCallback(com.androzic.location.ITrackingListener callback)
		{
			trackingCallbacks.add(callback);
		}

		@Override
		public void unregisterTrackingCallback(com.androzic.location.ITrackingListener callback)
		{
			trackingCallbacks.remove(callback);
		}

		@Override
		public boolean isLocating()
		{
			return locationsEnabled;
		}

		@Override
		public boolean isTracking()
		{
			return trackingEnabled;
		}

		@Override
		public float getHDOP()
		{
			return HDOP;
		}

		@Override
		public float getVDOP()
		{
			return VDOP;
		}

		@Override
		public Track getTrack()
		{
			return LocationService.this.getTrack();
		}

		@Override
		public Track getTrack(long start, long end)
		{
			return LocationService.this.getTrack(start, end);
		}

		@Override
		public void clearTrack()
		{
			LocationService.this.clearTrack();
		}

		@Override
		public long getTrackStartTime()
		{
			return LocationService.this.getTrackStartTime();
		}

		@Override
		public long getTrackEndTime()
		{
			return LocationService.this.getTrackEndTime();
		}
	}
	
	/**
	 * Mock location generator used for application testing. Locations are generated
	 * by logic required for particular test.
	 */
	final private Runnable sendMockLocation = new Runnable() {
		public void run()
		{
			mockCallback.postDelayed(this, 1000);

			updateGpsStatus(GPS_OK, 5, 25);

			lastKnownLocation.setProvider(LocationManager.GPS_PROVIDER);
			lastKnownLocation.setTime(System.currentTimeMillis());
			lastKnownLocation.setSpeed(20);
			lastKnownLocation.setBearing(323);
			lastKnownLocation.setAltitude(39);
			lastKnownLocation.setLatitude(34.865792);
			lastKnownLocation.setLongitude(32.351646);
			lastKnownLocation.setBearing((System.currentTimeMillis() / 166) % 360);
			//lastKnownLocation.setAltitude(169);
			//lastKnownLocation.setLatitude(55.852527);
			//lastKnownLocation.setLongitude(29.451150);
			nmeaGeoidHeight = 0;
			smoothSpeed = 19;
			avgSpeed = 14;

			updateLocation();
			isContinous = true;
		}
	};
} public class MapIndex implements Serializable
{
	private static final long serialVersionUID = 9L;
	
	private HashSet<Integer>[][] maps;
	private HashMap<Integer,BaseMap> mapIndex;
	private int hashCode;
	private transient Comparator<BaseMap> comparator = new MapComparator();

	@SuppressWarnings("unused")
	MapIndex()
	{
	}

	@SuppressLint("UseSparseArrays")
	@SuppressWarnings("unchecked")
	public MapIndex(String path, String charset)
	{
		maps = new HashSet[181][361];
		mapIndex = new HashMap<>();
		File root = new File(path);
		List<File> files = FileList.getFileListing(root, new MapFilenameFilter());
		for (File file: files)
		{
			try
			{
				BaseMap map = MapLoader.load(file, charset);
				addMap(map);
			}
			catch (IOException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		hashCode = getMapsHash(files);
	}
	
	public static int getMapsHash(String path)
	{
		File root = new File(path);
		List<File> files = FileList.getFileListing(root, new MapFilenameFilter());
		return getMapsHash(files);
	}

	private static int getMapsHash(List<File> files)
	{
		int result = 13;
		for (File file: files)
		{
			result = 31 * result + file.getAbsolutePath().hashCode();
		}
		return result;
	}

	@Override
	public int hashCode()
	{
		return hashCode;
	}

	public void addMap(BaseMap map)
	{
		if (mapIndex.containsKey(map.id))
			return;

		mapIndex.put(map.id, map);
		if (map.loadError != null)
			return;

		map.initialize();
		// Error can be raised during initialization.
		if (map.loadError != null)
			return;

		// TODO Use corner markers instead
		Bounds bounds = map.getBounds();
		int minLat = (int) Math.floor(bounds.minLat);
		int maxLat = (int) Math.ceil(bounds.maxLat);
		int minLon = (int) Math.floor(bounds.minLon);
		int maxLon = (int) Math.ceil(bounds.maxLon);
		for (int lat = minLat; lat <= maxLat; lat++)
		{
			for (int lon = minLon; lon <= maxLon; lon++)
			{
				HashSet<Integer> lli;
				try
				{
					lli = maps[lat + 90][lon + 180];
				}
				catch (IndexOutOfBoundsException e)
				{
					e.printStackTrace();
					map.loadError = e;
					return;
				}
				if (lli == null)
				{
					lli = new HashSet<>();
					maps[lat + 90][lon + 180] = lli;
				}
				lli.add(map.id);
			}
		}
	}

	public void removeMap(BaseMap map)
	{
		mapIndex.remove(map.id);
		if (map.loadError != null)
			return;
		map.destroy();
		// TODO Use corner markers instead
		Bounds bounds = map.getBounds();
		int minLat = (int) Math.floor(bounds.minLat);
		int maxLat = (int) Math.ceil(bounds.maxLat);
		int minLon = (int) Math.floor(bounds.minLon);
		int maxLon = (int) Math.ceil(bounds.maxLon);
		for (int lat = minLat; lat <= maxLat; lat++)
		{
			for (int lon = minLon; lon <= maxLon; lon++)
			{
				HashSet<Integer> lli = maps[lat+90][lon+180];
				if (lli != null)
					lli.remove(map.id);
			}
		}
	}

	public List<BaseMap> getCoveringMaps(BaseMap refMap, Bounds area, boolean covered, boolean bestmap)
	{
		List<BaseMap> llmaps = new ArrayList<>();
		Set<BaseMap> llmapsidx = new HashSet<>();

		int minLat = (int) Math.floor(area.minLat);
		int maxLat = (int) Math.ceil(area.maxLat);
		int minLon = (int) Math.floor(area.minLon);
		int maxLon = (int) Math.ceil(area.maxLon);
		
		for (int lat = minLat; lat <= maxLat; lat++)
		{
			for (int lon = minLon; lon <= maxLon; lon++)
			{
				HashSet<Integer> lli = maps[lat+90][lon+180];
				if (lli != null)
				{
					for (Integer id : lli)
					{
						BaseMap map = mapIndex.get(id);
						if (llmapsidx.contains(map))
							continue;
						if (map.equals(refMap))
							continue;
						double ratio = map.getCoveringRatio(refMap.mpp);
						// If map has smaller scale and reference map covers all screen do not use it
						if ((covered || ratio < 0.2) && ratio < 0.99)
							continue;

						// If map has bigger scale but best map is not enabled do not use it
						if ((! bestmap && covered) || ratio > 5d)
							continue;

						// If map does not cover referencing area do not use it
						// We put it at last place because this check is expensive
						if (!map.containsArea(area))
							continue;

						llmaps.add(map);
						llmapsidx.add(map);
					}
				}
			}
		}

		Collections.sort(llmaps, comparator);
		Collections.reverse(llmaps);

		return llmaps;
	}

	public List<BaseMap> getMaps(double latitude, double longitude)
	{
		List<BaseMap> llmaps = new ArrayList<>();
		
		int minLat = (int) Math.floor(latitude);
		int maxLat = (int) Math.ceil(latitude);
		int minLon = (int) Math.floor(longitude);
		int maxLon = (int) Math.ceil(longitude);

		for (int lat = minLat; lat <= maxLat; lat++)
		{
			for (int lon = minLon; lon <= maxLon; lon++)
			{
				HashSet<Integer> lli = maps[lat+90][lon+180];
				if (lli != null)
				{
					for (Integer id : lli)
					{
						BaseMap map = mapIndex.get(id);
						if (!llmaps.contains(map) && map.coversLatLon(latitude, longitude))
						{
							llmaps.add(map);
						}
					}
				}
			}
		}
		
		Collections.sort(llmaps, comparator);

		return llmaps;
	}

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

	public void cleanBadMaps()
	{
		HashSet<BaseMap> badMaps = new HashSet<>();
		
		for (Integer id : mapIndex.keySet())
		{
			BaseMap map = mapIndex.get(id);
			if (map.loadError != null)
			{
				badMaps.add(map);
			}
		}
		for (BaseMap map : badMaps)
		{
			removeMap(map);
		}		
	}

	public void clear()
	{
		for (BaseMap map : mapIndex.values())
			map.destroy();
		mapIndex.clear();
		for (int lat = 0; lat < 181; lat++)
		{
			for (int lon = 0; lon < 361; lon++)
			{
				HashSet<Integer> lli = maps[lat][lon];
				if (lli != null)
				{
					lli.clear();
					maps[lat][lon] = null;
				}
			}
		}
		maps = null;
	}

	public static MapIndex loadIndex(File file) throws Throwable
	{
		//com.esotericsoftware.minlog.Log.DEBUG();
		Kryo kryo = new Kryo();
		kryo.register(MapIndex.class);
		kryo.register(BaseMap.class);
		kryo.register(OzfMap.class);
		kryo.register(ForgeMap.class);
		kryo.register(Grid.class);
		kryo.register(MapPoint.class);
		kryo.register(MapPoint[].class);
		kryo.register(Projection.class);
		kryo.register(Integer.class);
		kryo.register(String.class);
		kryo.register(ArrayList.class);
		kryo.register(HashSet.class);
		kryo.register(HashMap.class);
		Input input = new Input(new FileInputStream(file));
		MapIndex index = kryo.readObject(input, MapIndex.class);
		input.close();
		for (BaseMap map : index.getMaps())
			map.initialize();
		return index;
	}

	public static void saveIndex(MapIndex index, File file) throws Throwable
	{
		Kryo kryo = new Kryo();
		kryo.register(MapIndex.class);
		kryo.register(BaseMap.class);
		kryo.register(OzfMap.class);
		kryo.register(ForgeMap.class);
		kryo.register(Grid.class);
		kryo.register(MapPoint.class);
		kryo.register(MapPoint[].class);
		kryo.register(Projection.class);
		kryo.register(Integer.class);
		kryo.register(String.class);
		kryo.register(ArrayList.class);
		kryo.register(HashSet.class);
		kryo.register(HashMap.class);
		Output output = new Output(new FileOutputStream(file));
		kryo.writeObject(output, index);
		output.close();
	}

	private class MapComparator implements Comparator<BaseMap>, Serializable
    {
		private static final long serialVersionUID = 3L;

		@Override
        public int compare(BaseMap o1, BaseMap o2)
        {
        	int res = Double.compare(o1.getAbsoluteMPP(), o2.getAbsoluteMPP());
	        if (res != 0)
		        return res;
	        res = o1.getPriority() - o2.getPriority();
	        if (res != 0)
		        return res;
	        return o1.title.compareTo(o2.title);
        }
    }
} public class MockMap extends BaseMap
{
	private static final long serialVersionUID = 2L;
	
	private static MockMap currentMap;

	private int lat;
	private int lon;
	
	public MockMap(int lat, int lon)
	{
		super("//_mock_map_//");
		title = "-no map-";
		datum = "WGS84";
		mpp = 0.1;
		this.lat = lat;
		this.lon = lon;
	}

	@Override
	public void initialize()
	{
	}

	@Override
	public void destroy()
	{
	}

	@Override
	public void activate(OnMapTileStateChangeListener listener, double mpp, boolean current)
	{
	}

	@Override
	public void deactivate()
	{
	}
	
	public boolean activated()
	{
		return true;
	}

	@Override
	public boolean coversLatLon(double lat, double lon)
	{
		return false;
	}

	@Override
	public int getScaledWidth()
	{
		return (int) (50000 * zoom);
	}

	@Override
	public int getScaledHeight()
	{
		return (int) (50000 * zoom);
	}

	@Override
	public boolean drawMap(Viewport viewport, boolean cropBorder, boolean drawBorder, Canvas c) throws OutOfMemoryError
	{
		return false;
	}

	@Override
	public int getPriority()
	{
		return 99;
	}

	@Override
	public List<String> info()
	{
		return null;
	}

	@Override
	public boolean getLatLonByXY(int x, int y, double[] ll)
	{
		ll[0] = 90 - (y * 1.0 / (50000 * zoom) + lat);
		ll[1] = x * 1.0 / (50000 * zoom) + lon - 180;
		return true;
	}

	@Override
	public boolean getXYByLatLon(double lat, double lon, int[] xy)
	{
		lat = 90 - lat;
		lon = 180 + lon;
		xy[1] = (int) ((lat - this.lat) * 50000 * zoom);
		xy[0] = (int) ((lon - this.lon) * 50000 * zoom);
		return true;
	}

	@Override
	public void getMapCenter(double[] center)
	{
		center[0] = lat;
		center[1] = lon;
	}

	@Override
	public double getMPP()
	{
		return 0;
	}

	@Override
	public double getCoveringRatio(double refMpp)
	{
		return Double.MIN_VALUE;
	}

	@Override
	public double getNextZoom()
	{
		if (zoom >= 10)
			return 0.0;
		return zoom + 0.1;
	}

	@Override
	public double getPrevZoom()
	{
		if (zoom <= 0.001)
			return 0.0;
		return zoom - 0.1;
	}

	@Override
	public double getZoom()
	{
		return zoom;
	}

	@Override
	public void setZoom(double zoom)
	{
		this.zoom = Math.floor(zoom * 1000) / 1000;
		if (this.zoom > 10) this.zoom = 10;
		if (this.zoom < 0.001) this.zoom = 0.001;
	}

	public static BaseMap getMap(double lat, double lon)
	{
		int ilat = (int) (90 - lat);
		int ilon = (int) (180 + lon);
		if (currentMap == null)
		{
			currentMap = new MockMap(ilat, ilon);
		}
		else if (currentMap.lat != ilat || currentMap.lon != ilon)
		{
			double s = currentMap.zoom; 
			currentMap = new MockMap(ilat, ilon);
			currentMap.zoom = s;
		}
		return currentMap;
	}
} public class ForgeMap extends TileMap
{
	private static final long serialVersionUID = 1L;

	@SuppressWarnings("SpellCheckingInspection")
	public static final byte[] MAGIC = "mapsforge binary OSM".getBytes();

	public static float textScale = 1f;

	private transient static Androzic application;
	private transient static RenderThemeFuture renderTheme;
	private transient static DisplayModel displayModel = new DisplayModel();
	private transient static MapViewPosition mapViewPosition;
	private transient static MutableTwoLevelTileCache tileCache;
	private transient static TileCache memoryTileCache;
	private transient static TileCache fileSystemTileCache;
	private transient static MultiMapDataStore mapDataStore = new MultiMapDataStore(MultiMapDataStore.DataPolicy.RETURN_ALL);
	private transient static DatabaseRenderer databaseRenderer;
	private transient static JobQueue<RendererJob> jobQueue;
	private transient static MapWorker mapWorker;
	private transient static MapRedrawer mapRedrawer = new MapRedrawer();
	private transient static int activeCount = 0;

	private transient MapFile mapFile;
	private transient MapFileInfo mapInfo;
	private transient LatLong mapCenter;


	private transient int[] minCR;
	private transient int[] maxCR;

	static
	{
		displayModel.setMaxTextWidthFactor(1f);
		mapViewPosition = new MapViewPosition(displayModel);
		mapViewPosition.setZoomLevelMin((byte) 0);
		mapViewPosition.setZoomLevelMax((byte) 22);
	}

	protected ForgeMap()
	{
	}

	public ForgeMap(String path)
	{
		super(path);
	}

	@Override
	public void initialize()
	{
		minCR = new int[2];
		maxCR = new int[2];

		File file = new File(path);
		try
		{
			mapFile = new MapFile(file);
			mapInfo = mapFile.getMapFileInfo();
		}
		catch (MapFileException e)
		{
			loadError = e;
			return;
		}

		// Remove extension
		name = file.getName();
		int e = name.lastIndexOf(".map");
		if (e > 0)
			name = name.substring(0, e);

		// And capitalize first letter
		StringBuilder nameSb = new StringBuilder(name.toLowerCase());
		nameSb.setCharAt(0, Character.toUpperCase(nameSb.charAt(0)));
		name = nameSb.toString();

		initializeZooms((byte) 0, (byte) 22, mapInfo.startZoomLevel);

		setCornersAmount(4);
		cornerMarkers[0].lat = mapInfo.boundingBox.maxLatitude;
		cornerMarkers[0].lon = mapInfo.boundingBox.minLongitude;
		cornerMarkers[1].lat = mapInfo.boundingBox.maxLatitude;
		cornerMarkers[1].lon = mapInfo.boundingBox.maxLongitude;
		cornerMarkers[2].lat = mapInfo.boundingBox.minLatitude;
		cornerMarkers[2].lon = mapInfo.boundingBox.maxLongitude;
		cornerMarkers[3].lat = mapInfo.boundingBox.minLatitude;
		cornerMarkers[3].lon = mapInfo.boundingBox.minLongitude;
		int[] xy = new int[2];
		for (int i = 0; i < 4; i++)
		{
			getXYByLatLon(cornerMarkers[i].lat, cornerMarkers[i].lon, xy);
			cornerMarkers[i].x = xy[0];
			cornerMarkers[i].y = xy[1];
		}

		mapCenter = mapInfo.startPosition;

		if (application == null)
			application = Androzic.getApplication();

		if (renderTheme == null)
			compileRenderTheme(application.xmlRenderTheme);

		mapDataStore.addMapDataStore(mapFile, false, false);

		updateTitle();
	}

	@Override
	public void destroy()
	{
		mapFile.close();
	}

	public static void reset()
	{
		mapDataStore = new MultiMapDataStore(MultiMapDataStore.DataPolicy.RETURN_ALL);
	}

	public static void clear()
	{
		if (fileSystemTileCache != null)
		{
			fileSystemTileCache.destroy();
			fileSystemTileCache = null;
		}
		mapDataStore.close();
		reset();
		application = null;
	}

	@Override
	public synchronized void activate(OnMapTileStateChangeListener listener, double mpp, boolean current) throws Throwable
	{
		Log.e("FM", "activate(): " + name);
		synchronized (MAGIC)
		{
			mapRedrawer.setListener(listener);

			if (tileCache == null)
			{
				tileCache = new MutableTwoLevelTileCache();
				tileCache.setSecondLevelCache(getSecondLevelCache());
			}

			if (databaseRenderer == null)
				databaseRenderer = new DatabaseRenderer(mapDataStore, AndroidGraphicFactory.INSTANCE, tileCache);

			if (jobQueue == null)
				jobQueue = new JobQueue<>(mapViewPosition, displayModel);

			if (mapWorker == null)
			{
				mapWorker = new MapWorker(tileCache, jobQueue, databaseRenderer, new ForgeLayer(mapRedrawer));
				mapWorker.start();
			}

			activeCount++;
			if (Math.abs(1 - mpp / getMPP()) < 0.1)
				mpp = getMPP();
			super.activate(listener, mpp, current);
		}
	}

	@Override
	public synchronized void deactivate()
	{
		Log.e("FM", "deactivate(): " + name);
		synchronized (MAGIC)
		{
			super.deactivate();
			activeCount--;

			if (activeCount > 0)
				return;

			try
			{
				Log.w("FM", "  stop mapworker thread");
				mapWorker.interrupt();
				mapWorker.join();
			}
			catch (InterruptedException ignore)
			{
			}
			finally
			{
				mapWorker = null;
				databaseRenderer = null;
				tileCache = null;
				if (memoryTileCache != null)
				{
					memoryTileCache.destroy();
					memoryTileCache = null;
				}
				jobQueue = null;
			}
		}
	}

	@Override
	public synchronized boolean drawMap(Viewport viewport, boolean cropBorder, boolean drawBorder, Canvas c) throws OutOfMemoryError
	{
		if (!isActive)
			return false;

		lastLatitude = viewport.mapCenter[0];
		recalculateMPP();

		if (!isCurrent)
			return false;

		mapViewPosition.setCenter(new LatLong(viewport.mapCenter[0], viewport.mapCenter[1]));

		int[] map_xy = new int[2];
		getXYByLatLon(viewport.mapCenter[0], viewport.mapCenter[1], map_xy);
		map_xy[0] -= viewport.lookAheadXY[0];
		map_xy[1] -= viewport.lookAheadXY[1];

		Path clipPath = new Path();

		if (cropBorder || drawBorder)
			mapClipPath.offset(-map_xy[0] + viewport.canvasWidth / 2, -map_xy[1] + viewport.canvasHeight / 2, clipPath);

		float tile_wh = (float) (tileSize * dynZoom);

		int osm_x = (int) (map_xy[0] / tile_wh);
		int osm_y = (int) (map_xy[1] / tile_wh);

		int tiles_per_x = Math.round(viewport.canvasWidth * 1.f / tile_wh / 2 + .5f);
		int tiles_per_y = Math.round(viewport.canvasHeight * 1.f / tile_wh / 2 + .5f);

		int c_min = osm_x - tiles_per_x;
		int c_max = osm_x + tiles_per_x + 1;

		int r_min = osm_y - tiles_per_y;
		int r_max = osm_y + tiles_per_y + 1;

		boolean result = true;

		if (c_min < minCR[0])
		{
			c_min = minCR[0];
			result = false;
		}
		if (r_min < minCR[1])
		{
			r_min = minCR[1];
			result = false;
		}
		if (c_max > maxCR[0])
		{
			c_max = maxCR[0];
			result = false;
		}
		if (r_max > maxCR[1])
		{
			r_max = maxCR[1];
			result = false;
		}

		float w2mx = viewport.canvasWidth / 2 - map_xy[0];
		float h2my = viewport.canvasHeight / 2 - map_xy[1];
		int twh = Math.round(tile_wh);

		List<TilePosition> tilePositions = new ArrayList<>();
		for (int i = r_min; i <= r_max; i++)
			for (int j = c_min; j <= c_max; j++)
				tilePositions.add(new TilePosition(new Tile(j, i, srcZoom, tileSize), new Point(j, i)));

		Set<Job> jobs = new HashSet<>();
		for (TilePosition tilePosition : tilePositions)
			jobs.add(getJob(tilePosition.tile));
		synchronized (MAGIC)
		{
			tileCache.setWorkingSet(jobs);
		}

		for (int k = tilePositions.size() - 1; k >= 0; k--)
		{
			TilePosition tilePosition = tilePositions.get(k);
			Point point = tilePosition.point;
			Tile mapTile = tilePosition.tile;

			Bitmap tile = getTile(mapTile);
			if (tile != null && !tile.isRecycled())
			{
				if (tile.getWidth() != twh)
					tile = Bitmap.createScaledBitmap(tile, twh, twh, true);
				float tx = w2mx + (float) (point.x) * tile_wh;
				float ty = h2my + (float) (point.y) * tile_wh;
				c.drawBitmap(tile, tx, ty, null);
			}
		}

		if (drawBorder && borderPaint != null)
			c.drawPath(clipPath, borderPaint);

		return result;
	}

	@Override
	public int getPriority()
	{
		return 2;
	}

	@Override
	protected Bitmap getTile(int x, int y) throws OutOfMemoryError
	{
		return null;
	}

	public Bitmap getTile(Tile tile) throws OutOfMemoryError
	{
		org.mapsforge.core.graphics.Bitmap bitmap = loadTile(tile);
		Bitmap tileBitmap = null;
		if (bitmap != null)
			tileBitmap = AndroidGraphicFactory.getBitmap(bitmap);

		if (tileBitmap == null)
			tileBitmap = generateTile(tile);

		if (tileBitmap != null)
		{
			if (dynZoom != 1.0)
			{
				int ss = (int) (dynZoom * tileSize);
				tileBitmap = Bitmap.createScaledBitmap(tileBitmap, ss, ss, true);
			}
		}
		return tileBitmap;
	}

	public org.mapsforge.core.graphics.Bitmap loadTile(Tile tile)
	{
		RendererJob job = getJob(tile);
		org.mapsforge.core.graphics.Bitmap bitmap = tileCache.getImmediately(job);
		if (bitmap == null && !tileCache.containsKey(job))
			jobQueue.add(job);
		jobQueue.notifyWorkers();
		return bitmap;
	}

	public Bitmap generateTile(Tile tile)
	{
		byte parentTileZoom = (byte) (tile.zoomLevel - 1);
		int parentTileX = tile.tileX / 2, parentTileY = tile.tileY / 2, scale = 2;

		// Search for parent tile
		for (; parentTileZoom >= 0; parentTileZoom--, parentTileX /= 2, parentTileY /= 2, scale *= 2)
		{
			Tile parentTile;
			try
			{
				parentTile = new Tile(parentTileX, parentTileY, parentTileZoom, tileSize);
			} catch (IllegalArgumentException e)
			{
				//TODO Check X,Y values for limits
				e.printStackTrace();
				continue;
			}
			org.mapsforge.core.graphics.Bitmap bitmap = tileCache.getImmediately(getJob(parentTile));
			if (bitmap == null)
				continue;
			Bitmap tileBitmap = AndroidGraphicFactory.getBitmap(bitmap);
			if (tileBitmap != null && scale <= tileBitmap.getWidth() && scale <= tileBitmap.getHeight())
			{
				Matrix matrix = new Matrix();
				matrix.postScale(scale, scale);

				int miniTileWidth = tileBitmap.getWidth() / scale;
				int miniTileHeight = tileBitmap.getHeight() / scale;
				int fromX = (tile.tileX % scale) * miniTileWidth;
				int fromY = (tile.tileY % scale) * miniTileHeight;

				// Create mini bitmap which will be stretched to tile
				Bitmap miniTileBitmap = Bitmap.createBitmap(tileBitmap, fromX, fromY, miniTileWidth, miniTileHeight);

				// Create tile bitmap from mini bitmap
				tileBitmap = Bitmap.createBitmap(miniTileBitmap, 0, 0, miniTileWidth, miniTileHeight, matrix, false);
				miniTileBitmap.recycle();
				return tileBitmap;
			}
		}
		return null;
	}

	private static RendererJob getJob(Tile tile)
	{
		return new RendererJob(tile, mapDataStore, renderTheme, displayModel, textScale, false, false);
	}

	@Override
	public synchronized void setZoom(double z)
	{
		super.setZoom(z);
		if (isCurrent && mapViewPosition.getZoomLevel() != srcZoom)
			mapViewPosition.setZoomLevel(srcZoom);
	}

	@Override
	public void recalculateCache()
	{
		if (!isCurrent)
			return;

		int nx = (int) Math.ceil(viewportWidth * 1. / (tileSize * dynZoom)) + 4;
		int ny = (int) Math.ceil(viewportHeight * 1. / (tileSize * dynZoom)) + 4;

		BoundingBox boundingBox = mapDataStore.boundingBox();

		getTileXYByLatLon(boundingBox.maxLatitude, boundingBox.minLongitude, minCR);
		getTileXYByLatLon(boundingBox.minLatitude, boundingBox.maxLongitude, maxCR);

		int mnx = maxCR[0] - minCR[0] + 2;
		int mny = maxCR[1] - minCR[1] + 2;
		if (nx > mnx)
			nx = mnx;
		if (ny > mny)
			ny = mny;
		int cacheSize = (int) Math.ceil(nx * ny * 1.2);
		Log.i("ForgeMap", "Cache size: " + cacheSize);
		Log.i("ForgeMap", "Capacity: " + tileCache.getCapacityFirstLevel());

		if (cacheSize > tileCache.getCapacityFirstLevel())
		{
			TileCache oldCache = memoryTileCache;
			memoryTileCache = new InMemoryTileCache(cacheSize);
			tileCache.setFirstLevelCache(memoryTileCache);
			if (oldCache != null)
				oldCache.destroy();
		}
	}

	private static TileCache getSecondLevelCache()
	{
		if (fileSystemTileCache != null)
			return fileSystemTileCache;

		BaseApplication application = BaseApplication.getApplication();
		if (application == null)
			return null;

		File cache = application.getCacheDir();
		if (cache == null) // cache is not available now
			return null;

		File cacheDirectory = new File(cache, "mapsforge");
		if (!cacheDirectory.exists() && !cacheDirectory.mkdirs())
			return null;

		int tileCacheFiles = 2000; //estimateSizeOfFileSystemCache(cacheDirectoryName, firstLevelSize, tileSize);
		if (! cacheDirectory.canWrite() || tileCacheFiles == 0)
			return null;

		try
		{
			fileSystemTileCache = new FileSystemTileCache(tileCacheFiles, cacheDirectory, AndroidGraphicFactory.INSTANCE, false);
			return fileSystemTileCache;
		}
		catch (IllegalArgumentException e)
		{
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public void getMapCenter(double[] center)
	{
		center[0] = mapCenter.latitude;
		center[1] = mapCenter.longitude;
	}

	public static void onRenderThemeChanged()
	{
		Androzic application = Androzic.getApplication();
		compileRenderTheme(application.xmlRenderTheme);
		if (tileCache != null)
			tileCache.purge();
		else if (fileSystemTileCache != null)
			fileSystemTileCache.purge();
	}

	private static void compileRenderTheme(XmlRenderTheme xmlRenderTheme)
	{
		renderTheme = new RenderThemeFuture(AndroidGraphicFactory.INSTANCE, xmlRenderTheme, displayModel);
		new Thread(renderTheme).run();
	}

	public List<String> info()
	{
		ArrayList<String> info = new ArrayList<>();

		info.add("title: " + name);
		info.add("path: " + path);
		info.add("minimum zoom: " + minZoom);
		info.add("maximum zoom: " + maxZoom);
		info.add("start zoom: " + mapInfo.startZoomLevel);
		if (projection != null)
		{
			info.add("projection: " + projection.getName() + " (" + projection.getEPSGCode() + ")");
			info.add("\t" + projection.getPROJ4Description());
		}
		info.add("datum: " + datum);
		info.add("scale (mpp): " + mpp);
		info.add("map projection: " + mapInfo.projectionName);
		info.add("bounding box: " + mapInfo.boundingBox.toString());
		if (mapInfo.startPosition != null)
			info.add("start position: " + mapInfo.startPosition.toString());
		info.add("language: " + mapInfo.languagePreference);
		info.add("creation date: " + DateFormat.getDateFormat(Androzic.getApplication()).format(mapInfo.mapDate));
		info.add("created by: " + mapInfo.createdBy);
		if (mapInfo.comment != null)
			info.add("comment: " + mapInfo.comment);

		return info;
	}

	private static class MapRedrawer implements Redrawer
	{
		private OnMapTileStateChangeListener listener;

		protected void setListener(OnMapTileStateChangeListener listener)
		{
			this.listener = listener;
		}

		@Override
		public void redrawLayers()
		{
			if (listener != null)
				listener.onTileObtained();
		}
	}
} public class OpenStreetMapTileProvider extends TileProvider
{
	private String[] servers = {"a", "b", "c"};
	private int nextServer = 0;

	public OpenStreetMapTileProvider()
	{
		name = "OpenStreetMap";
		code = "osm";
		license = "\u00a9 <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors";
	}

	@Override
	public void activate()
	{
	}

	@Override
	public void deactivate()
	{
	}

	@Nullable
	@Override
	public String getTileUri(int x, int y, byte z)
	{
		nextServer++;
		if (servers.length <= nextServer)
			nextServer = 0;
		return String.format("http://%s.tile.openstreetmap.org/%d/%d/%d.png", servers[nextServer], z, x, y);
	}
} public class NavigationService extends BaseNavigationService implements OnSharedPreferenceChangeListener
{
    private static final String TAG = "Navigation";
	private static final int NOTIFICATION_ID = 24163;
	
	private Androzic application;
	
	private ILocationService locationService = null;
	protected Location lastKnownLocation;
	
	private PendingIntent contentIntent;
	
	private int routeProximity = 200;
	private boolean useTraverse = true;

	/**
	 * Active route waypoint
	 */
	public MapObject navWaypoint = null;
	/**
	 * Previous route waypoint
	 */
	public MapObject prevWaypoint = null;
	/**
	 * Active route
	 */
	public Route navRoute = null;

	public int navDirection = 0;
	/**
	 * Active route waypoint index
	 */
	public int navCurrentRoutePoint = -1;
	private double navRouteDistance = -1;

	/**
	 * Distance to active waypoint
	 */
	public int navProximity = 0;
	public double navDistance = 0.0;
	public double navBearing = 0.0;
	public long navTurn = 0;
	public double navVMG = 0.0;
	public int navETE = Integer.MAX_VALUE;
	public double navCourse = 0.0;
	public double navXTK = Double.NEGATIVE_INFINITY;

	private long tics = 0;
	private float[] vmgav = null;
	private double avvmg = 0.0;

	@Override
	public void onCreate()
	{
		application = Androzic.getApplication();
		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_navigation_proximity));
		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_navigation_traverse));
		sharedPreferences.registerOnSharedPreferenceChangeListener(this);

		Log.i(TAG, "Service started");
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		if (intent != null)
		{
			Intent activity = new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
			String action = intent.getAction();
			if (action == null)
				return 0;
			Bundle extras = intent.getExtras();
			if (action.equals(NAVIGATE_MAPOBJECT))
			{
				MapObject mo = new MapObject();
				mo.name = extras.getString(EXTRA_NAME);
				mo.latitude = extras.getDouble(EXTRA_LATITUDE);
				mo.longitude = extras.getDouble(EXTRA_LONGITUDE);
				mo.proximity = extras.getInt(EXTRA_PROXIMITY);
				activity.putExtra(MainActivity.LAUNCH_ACTIVITY, HSIActivity.class);
				contentIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, activity, PendingIntent.FLAG_CANCEL_CURRENT);
				navigateTo(mo);
			}
			if (action.equals(NAVIGATE_MAPOBJECT_WITH_ID))
			{
				long id = extras.getLong(EXTRA_ID);
				MapObject mo = application.getMapObject(id);
				if (mo == null)
					return 0;
				activity.putExtra(MainActivity.LAUNCH_ACTIVITY, HSIActivity.class);
				contentIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, activity, PendingIntent.FLAG_CANCEL_CURRENT);
				navigateTo(mo);
			}
			if (action.equals(NAVIGATE_ROUTE))
			{
				int index = extras.getInt(EXTRA_ROUTE_INDEX);
				int dir = extras.getInt(EXTRA_ROUTE_DIRECTION, DIRECTION_FORWARD);
				int start = extras.getInt(EXTRA_ROUTE_START, -1);
				activity.putExtra(MainActivity.SHOW_FRAGMENT, RouteDetails.class);
				activity.putExtra("index", index);
				contentIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, activity, PendingIntent.FLAG_CANCEL_CURRENT);
				Route route = application.getRoute(index);
				if (route == null)
					return 0;
				navigateTo(route, dir);
				if (start != -1)
					setRouteWaypoint(start);
			}
		}
		return START_STICKY;
	}
	
	@Override
	public void onDestroy()
	{
		disconnect();
		clearNavigation();
		PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
		super.onDestroy();
		Log.i(TAG, "Service stopped");
	}

	private final IBinder binder = new LocalBinder();

	public class LocalBinder extends Binder
	{
		public NavigationService getService()
		{
			return NavigationService.this;
		}
	}

	@Override
	public IBinder onBind(Intent intent)
	{
		return binder;
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
	{
		if (getString(R.string.pref_navigation_proximity).equals(key))
		{
			routeProximity = Integer.parseInt(sharedPreferences.getString(key, getString(R.string.def_navigation_proximity)));
		}
		if (getString(R.string.pref_navigation_traverse).equals(key))
		{
			useTraverse = sharedPreferences.getBoolean(key, getResources().getBoolean(R.bool.def_navigation_traverse));
		}
	}

	private void connect()
	{
		bindService(new Intent(this, LocationService.class), locationConnection, BIND_AUTO_CREATE);
	}
	
	private void disconnect()
	{
		if (locationService != null)
		{
			locationService.unregisterLocationCallback(locationListener);
			unbindService(locationConnection);
			locationService = null;
		}
	}
	
	public void stopNavigation()
	{
		clearNavigation();
		updateNavigationState(STATE_STOPED);
		stopForeground(true);
		disconnect();
	}
	
	private void clearNavigation()
	{
		navWaypoint = null;
		prevWaypoint = null;
		navRoute = null;

		navDirection = 0;
		navCurrentRoutePoint = -1;		

		navProximity = routeProximity;
		navDistance = 0.0;
		navBearing = 0.0;
		navTurn = 0;
		navVMG = 0.0;
		navETE = Integer.MAX_VALUE;
		navCourse = 0.0;
		navXTK = Double.NEGATIVE_INFINITY;

		vmgav = null;
		avvmg = 0.0;
	}

	public boolean isNavigating()
	{
		return navWaypoint != null;
	}

	public boolean isNavigatingViaRoute()
	{
		return navRoute != null;
	}

	private void navigateTo(final MapObject waypoint)
	{
		clearNavigation();
		connect();
		startForeground(NOTIFICATION_ID, getNotification());

		vmgav = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

		navWaypoint = waypoint;
		navProximity = navWaypoint.proximity > 0 ? navWaypoint.proximity : routeProximity;
		updateNavigationState(STATE_STARTED);
		if (lastKnownLocation != null)
			calculateNavigationStatus(lastKnownLocation, 0, 0);
	}

	private void navigateTo(final Route route, final int direction)
	{
		clearNavigation();
		connect();
		startForeground(NOTIFICATION_ID, getNotification());

		vmgav = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

		navRoute = route;
		navDirection = direction;
		navCurrentRoutePoint = navDirection == 1 ? 1 : navRoute.length()-2;

		navWaypoint = navRoute.getWaypoint(navCurrentRoutePoint);
		prevWaypoint = navRoute.getWaypoint(navCurrentRoutePoint - navDirection);
		navProximity = navWaypoint.proximity > 0 ? navWaypoint.proximity : routeProximity;
		navRouteDistance = -1;
		navCourse = Geo.bearing(prevWaypoint.latitude, prevWaypoint.longitude, navWaypoint.latitude, navWaypoint.longitude);
		updateNavigationState(STATE_STARTED);
		if (lastKnownLocation != null)
			calculateNavigationStatus(lastKnownLocation, 0, 0);
	}

	public void setRouteWaypoint(int waypoint)
	{
		navCurrentRoutePoint = waypoint;
		navWaypoint = navRoute.getWaypoint(navCurrentRoutePoint);
		int prev = navCurrentRoutePoint - navDirection;
		if (prev >= 0 && prev < navRoute.length())
			prevWaypoint = navRoute.getWaypoint(prev);
		else
			prevWaypoint = null;
		navProximity = navWaypoint.proximity > 0 ? navWaypoint.proximity : routeProximity;
		navRouteDistance = -1;
		navCourse = prevWaypoint == null ? 0.0 : Geo.bearing(prevWaypoint.latitude, prevWaypoint.longitude, navWaypoint.latitude, navWaypoint.longitude);
		updateNavigationState(STATE_NEXTWPT);
	}

	public MapObject getNextRouteWaypoint()
	{
		try
		{
			return navRoute.getWaypoint(navCurrentRoutePoint + navDirection);
		}
		catch (IndexOutOfBoundsException e)
		{
			return null;
		}
	}

	public void nextRouteWaypoint() throws IndexOutOfBoundsException
	{
		navCurrentRoutePoint += navDirection;
		navWaypoint = navRoute.getWaypoint(navCurrentRoutePoint);
		prevWaypoint = navRoute.getWaypoint(navCurrentRoutePoint - navDirection);
		navProximity = navWaypoint.proximity > 0 ? navWaypoint.proximity : routeProximity;
		navRouteDistance = -1;
		navCourse = Geo.bearing(prevWaypoint.latitude, prevWaypoint.longitude, navWaypoint.latitude, navWaypoint.longitude);
		updateNavigationState(STATE_NEXTWPT);
	}

	public void prevRouteWaypoint() throws IndexOutOfBoundsException
	{
		navCurrentRoutePoint -= navDirection;
		navWaypoint = navRoute.getWaypoint(navCurrentRoutePoint);
		int prev = navCurrentRoutePoint - navDirection;
		if (prev >= 0 && prev < navRoute.length())
			prevWaypoint = navRoute.getWaypoint(prev);
		else
			prevWaypoint = null;
		navProximity = navWaypoint.proximity > 0 ? navWaypoint.proximity : routeProximity;
		navRouteDistance = -1;
		navCourse = prevWaypoint == null ? 0.0 : Geo.bearing(prevWaypoint.latitude, prevWaypoint.longitude, navWaypoint.latitude, navWaypoint.longitude);
		updateNavigationState(STATE_NEXTWPT);
	}

	public boolean hasNextRouteWaypoint()
	{
		if (navRoute == null)
			return false;
		boolean hasNext = false;
		if (navDirection == DIRECTION_FORWARD)
			hasNext = (navCurrentRoutePoint + navDirection) < navRoute.length();
		if (navDirection == DIRECTION_REVERSE)
			hasNext = (navCurrentRoutePoint + navDirection) >= 0;
		return hasNext;
	}

	public boolean hasPrevRouteWaypoint()
	{
		if (navRoute == null)
			return false;
		boolean hasPrev = false;
		if (navDirection == DIRECTION_FORWARD)
			hasPrev = (navCurrentRoutePoint - navDirection) >= 0;
		if (navDirection == DIRECTION_REVERSE)
			hasPrev = (navCurrentRoutePoint - navDirection) < navRoute.length();
		return hasPrev;
	}
	
	public int navRouteCurrentIndex()
	{
		return navDirection == DIRECTION_FORWARD ? navCurrentRoutePoint : navRoute.length() - navCurrentRoutePoint - 1;
	}
	
	/**
	 * Calculates distance between current route waypoint and last route waypoint.
	 * @return distance left
	 */
	public double navRouteDistanceLeft()
	{
		if (navRouteDistance < 0)
		{
			navRouteDistance = navRouteDistanceLeftTo(navRoute.length() - 1);
		}
		return navRouteDistance;
	}

	/**
	 * Calculates distance between current route waypoint and route waypoint with specified index.
	 * Method honors navigation direction.
	 * @param index
	 * @return distance left
	 */
	public double navRouteDistanceLeftTo(int index)
	{
		int current = navRouteCurrentIndex();
		int progress = index - current;
		
		if (progress <= 0)
			return 0.0;
		
		double distance = 0.0;
		if (navDirection == DIRECTION_FORWARD)
			distance = navRoute.distanceBetween(navCurrentRoutePoint, index);
		if (navDirection == DIRECTION_REVERSE)
			distance = navRoute.distanceBetween(navRoute.length() - index - 1, navCurrentRoutePoint);

		return distance;
	}
	
	public int navRouteWaypointETE(int index)
	{
		if (index == 0)
			return 0;
		int ete = Integer.MAX_VALUE;
		if (avvmg > 0)
		{
			int i = navDirection == DIRECTION_FORWARD ? index : navRoute.length() - index - 1;
			int j = i - navDirection;
			MapObject w1 = navRoute.getWaypoint(i);
			MapObject w2 = navRoute.getWaypoint(j);
			double distance = Geo.distance(w1.latitude, w1.longitude, w2.latitude, w2.longitude);
			ete = (int) Math.round(distance / avvmg / 60);
		}
		return ete;
	}

	/**
	 * Calculates route ETE.
	 * @param distance route distance
	 * @return route ETE
	 */
	public int navRouteETE(double distance)
	{
		int eta = Integer.MAX_VALUE;
		if (avvmg > 0)
		{
			eta = (int) Math.round(distance / avvmg / 60);
		}
		return eta;
	}

	public int navRouteETETo(int index)
	{
		double distance = navRouteDistanceLeftTo(index);
		if (distance <= 0.0)
			return 0;

		return navRouteETE(distance);
	}
	
	private void calculateNavigationStatus(Location loc, float smoothspeed, float avgspeed)
	{
		double distance = Geo.distance(loc.getLatitude(), loc.getLongitude(), navWaypoint.latitude, navWaypoint.longitude);
		double bearing = Geo.bearing(loc.getLatitude(), loc.getLongitude(), navWaypoint.latitude, navWaypoint.longitude);
		double track = loc.getBearing();

		// turn
		long turn = Math.round(bearing - track);
		if (Math.abs(turn) > 180)
		{
			turn = turn - (long)(Math.signum(turn))*360;
		}
		
		// vmg
		double vmg = Geo.vmg(smoothspeed, Math.abs(turn));

		// ete
		float curavvmg = (float) Geo.vmg(avgspeed, Math.abs(turn));
		if (avvmg == 0.0 || tics % 10 == 0)
		{
			for (int i = vmgav.length - 1; i > 0; i--)
			{
				avvmg += vmgav[i];
				vmgav[i] = vmgav[i - 1];
			}
			avvmg += curavvmg;
			vmgav[0] = curavvmg;
			avvmg = avvmg / vmgav.length;
		}

		int ete = Integer.MAX_VALUE;
		if (avvmg > 0)
			ete = (int) Math.round(distance / avvmg / 60);
		
		double xtk = Double.NEGATIVE_INFINITY;

		if (navRoute != null)
		{
			boolean hasNext = hasNextRouteWaypoint();
			if (distance < navProximity)
			{
				if (hasNext)
				{
					nextRouteWaypoint();
					return;
				}
				else
				{
					updateNavigationState(STATE_REACHED);
					stopNavigation();
					return;
				}
			}

			if (prevWaypoint != null)
			{
				double dtk = Geo.bearing(prevWaypoint.latitude, prevWaypoint.longitude, navWaypoint.latitude, navWaypoint.longitude);
				xtk = Geo.xtk(distance, dtk, bearing);

				if (xtk == Double.NEGATIVE_INFINITY)
				{
					if (useTraverse && hasNext)
					{
						double cxtk2 = Double.NEGATIVE_INFINITY;
						MapObject nextWpt = getNextRouteWaypoint();
						if (nextWpt != null)
						{
							double dtk2 = Geo.bearing(nextWpt.latitude, nextWpt.longitude, navWaypoint.latitude, navWaypoint.longitude);
							cxtk2 = Geo.xtk(0, dtk2, bearing);
						}

						if (cxtk2 != Double.NEGATIVE_INFINITY)
						{
							nextRouteWaypoint();
							return;
						}
					}
				}
			}
		}

		tics++;

		if (distance != navDistance || bearing != navBearing || turn != navTurn || vmg != navVMG || ete != navETE || xtk != navXTK)
		{
			navDistance = distance;
			navBearing = bearing;
			navTurn = turn;
			navVMG = vmg;
			navETE = ete;
			navXTK = xtk;
			updateNavigationStatus();
		}
	}
	
	private Notification getNotification()
	{
		NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
		builder.setContentIntent(contentIntent);
		builder.setSmallIcon(R.drawable.ic_stat_navigation);
		builder.setContentTitle(getText(R.string.notif_nav_short));
		if (navWaypoint != null)
		{
			builder.setWhen(System.currentTimeMillis());
			builder.setContentText(String.format((String) getText(R.string.notif_nav_to), navWaypoint.name));
		}
		else
		{
			builder.setWhen(0);
			builder.setContentText(getText(R.string.notif_nav_started));
		}
		builder.setGroup("androzic");
		builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
		builder.setPriority(NotificationCompat.PRIORITY_LOW);
		builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
		builder.setColor(getResources().getColor(R.color.theme_accent_color));
		return builder.build();
	}

	private void updateNavigationState(final int state)
	{
		if (state != STATE_STOPED && state != STATE_REACHED)
		{
			NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
			nm.notify(NOTIFICATION_ID, getNotification());
		}
		sendBroadcast(new Intent(BROADCAST_NAVIGATION_STATE).putExtra("state", state));
		Log.d(TAG, "State dispatched");
	}

	private void updateNavigationStatus()
	{
		sendBroadcast(new Intent(BROADCAST_NAVIGATION_STATUS));
		Log.d(TAG, "Status dispatched");
	}
	
	private ServiceConnection locationConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName className, IBinder service)
		{
			locationService = (ILocationService) service;
			locationService.registerLocationCallback(locationListener);
			Log.i(TAG, "Location service connected");
		}

		public void onServiceDisconnected(ComponentName className)
		{
			locationService = null;
			Log.i(TAG, "Location service disconnected");
		}
	};
	
	private ILocationListener locationListener = new ILocationListener()
	{
		@Override
		public void onGpsStatusChanged(String provider, int status, int fsats, int tsats)
		{
		}

		@Override
		public void onLocationChanged(Location loc, boolean continous, boolean geoid, float smoothspeed, float avgspeed)
		{
			Log.d(TAG, "Location arrived");
			lastKnownLocation = loc;
			
			if (navWaypoint != null)
				calculateNavigationStatus(loc, smoothspeed, avgspeed);			
		}

		@Override
		public void onProviderChanged(String provider)
		{
		}

		@Override
		public void onProviderDisabled(String provider)
		{
		}

		@Override
		public void onProviderEnabled(String provider)
		{
		}
	};
} public class NavigationOverlay extends MapOverlay
{
	private Paint paint;
	
	private int proximity = 0;
	private double mpp;
	private boolean drawCircle = false;

    public NavigationOverlay()
    {
        super();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(application.getResources().getColor(R.color.navigationline));

        onPreferencesChanged(PreferenceManager.getDefaultSharedPreferences(application));
    	mpp = 0;
        
        enabled = true;
    }

	@Override
	public synchronized void onMapChanged()
	{
		mpp = 0;
    	BaseMap map = application.getCurrentMap();
    	if (map == null)
    		return;
    	
    	mpp = map.getMPP();
	}
	
	@Override
	public void onPrepareBuffer(final Viewport viewport, final Canvas c)
	{
		if (application.navigationService.navWaypoint == null)
			return;
		
		final int[] cxy = viewport.mapCenterXY;

		int[] xy = application.getXYbyLatLon(application.navigationService.navWaypoint.latitude, application.navigationService.navWaypoint.longitude);

        if (!Double.isNaN(viewport.location[0]))
        {
            final int[] lxy = viewport.locationXY;
        	c.drawLine(lxy[0] - cxy[0], lxy[1] - cxy[1], xy[0] - cxy[0], xy[1] - cxy[1], paint);
        }
        if (drawCircle && application.navigationService.navRoute != null)
        {
	        List<Waypoint> waypoints = application.navigationService.navRoute.getWaypoints();
	        synchronized (waypoints)
	        {  
		        for (Waypoint wpt : waypoints)
		        {
		        	int radius = wpt.proximity > 0 ? wpt.proximity : proximity;
		        	radius /= mpp;
		        	if (radius > 0)
		        	{
		        		xy = application.getXYbyLatLon(wpt.latitude, wpt.longitude);
		        		c.drawCircle(xy[0] - cxy[0], xy[1] - cxy[1], radius, paint);
		        	}
		        }
	        }
        }
	}

	@Override
	public void onPrepareBufferEx(final Viewport viewport, final Canvas c)
	{
	}

	@Override
	public void onPreferencesChanged(SharedPreferences settings)
	{
		Resources resources = application.getResources();
		proximity = Integer.parseInt(settings.getString(application.getString(R.string.pref_navigation_proximity), application.getString(R.string.def_navigation_proximity)));
		drawCircle = settings.getBoolean(application.getString(R.string.pref_navigation_proximitycircle), resources.getBoolean(R.bool.def_navigation_proximitycircle));
		paint.setColor(settings.getInt(application.getString(R.string.pref_navigation_linecolor), resources.getColor(R.color.navigationline)));
        paint.setStrokeWidth(settings.getInt(application.getString(R.string.pref_navigation_linewidth), resources.getInteger(R.integer.def_navigation_linewidth)));
	}

} public class OverlayManager
{
	public static final int ORDER_SHOW_PREFERENCE = 0;
	public static final int ORDER_DRAW_PREFERENCE = 1;

	private static final int UPDATE_MESSAGE = 1;

	private Androzic application;
	private Handler executionHandler;

	public LatLonGridOverlay llGridOverlay;
	public OtherGridOverlay grGridOverlay;
	public CurrentTrackOverlay currentTrackOverlay;
	public NavigationOverlay navigationOverlay;
	public MapObjectsOverlay mapObjectsOverlay;
	public WaypointsOverlay waypointsOverlay;
	public DistanceOverlay distanceOverlay;
	public AccuracyOverlay accuracyOverlay;
	public List<TrackOverlay> fileTrackOverlays = new ArrayList<TrackOverlay>();
	public List<RouteOverlay> routeOverlays = new ArrayList<RouteOverlay>();

	public boolean mapGrid = false;
	public boolean userGrid = false;
	public int gridPrefer = 0;

	public OverlayManager(Looper looper)
	{
		application = Androzic.getApplication();
		executionHandler = new Handler(looper);
		createOverlays();
	}

	public void init()
	{
		if (waypointsOverlay == null)
			createOverlays();
		waypointsOverlay.setWaypoints(application.getWaypoints());
	}

	private void createOverlays()
	{
		mapObjectsOverlay = new MapObjectsOverlay();
		waypointsOverlay = new WaypointsOverlay();
	}

	public void onMapObjectsChanged()
	{
		mapObjectsOverlay.clearBitmapCache();
	}

	public void onWaypointsChanged()
	{
		waypointsOverlay.clearBitmapCache();
	}

	public void setWaypointsOverlayEnabled(boolean enabled)
	{
		waypointsOverlay.setEnabled(enabled);
	}

	public void setNavigationOverlayEnabled(boolean enabled)
	{
		if (enabled && navigationOverlay == null)
		{
			navigationOverlay = new NavigationOverlay();
		}
		else if (!enabled && navigationOverlay != null)
		{
			navigationOverlay.onBeforeDestroy();
			navigationOverlay = null;
		}
	}

	public void setCurrentTrackOverlayEnabled(boolean enabled)
	{
		if (enabled && currentTrackOverlay == null)
		{
			currentTrackOverlay = new CurrentTrackOverlay();
		}
		else if (!enabled && currentTrackOverlay != null)
		{
			currentTrackOverlay.onBeforeDestroy();
			currentTrackOverlay = null;
		}
	}

	public void setAccuracyOverlayEnabled(boolean enabled)
	{
		if (enabled && accuracyOverlay == null)
		{
			accuracyOverlay = new AccuracyOverlay();
			accuracyOverlay.setAccuracy(application.getLocationAsLocation().getAccuracy());
		}
		else if (!enabled && accuracyOverlay != null)
		{
			accuracyOverlay.onBeforeDestroy();
			accuracyOverlay = null;
		}
	}

	public void setDistanceOverlayEnabled(boolean enabled)
	{
		if (enabled && distanceOverlay == null)
		{
			distanceOverlay = new DistanceOverlay();
		}
		else if (!enabled && distanceOverlay != null)
		{
			distanceOverlay.onBeforeDestroy();
			distanceOverlay = null;
		}
	}

	public List<MapOverlay> getOverlays(int order)
	{
		List<MapOverlay> overlays = new ArrayList<MapOverlay>();
		if (order == ORDER_DRAW_PREFERENCE)
		{
			if (llGridOverlay != null)
				overlays.add(llGridOverlay);
			if (grGridOverlay != null)
				overlays.add(grGridOverlay);
			if (accuracyOverlay != null)
				overlays.add(accuracyOverlay);
			overlays.addAll(fileTrackOverlays);
			if (currentTrackOverlay != null)
				overlays.add(currentTrackOverlay);
			overlays.addAll(routeOverlays);
			if (navigationOverlay != null)
				overlays.add(navigationOverlay);
			if (waypointsOverlay != null)
				overlays.add(waypointsOverlay);
			if (mapObjectsOverlay != null)
				overlays.add(mapObjectsOverlay);
			if (distanceOverlay != null)
				overlays.add(distanceOverlay);
		}
		else
		{
			if (accuracyOverlay != null)
				overlays.add(accuracyOverlay);
			if (distanceOverlay != null)
				overlays.add(distanceOverlay);
			if (navigationOverlay != null)
				overlays.add(navigationOverlay);
			if (currentTrackOverlay != null)
				overlays.add(currentTrackOverlay);
			overlays.addAll(routeOverlays);
			if (waypointsOverlay != null)
				overlays.add(waypointsOverlay);
			overlays.addAll(fileTrackOverlays);
			if (mapObjectsOverlay != null)
				overlays.add(mapObjectsOverlay);
			if (grGridOverlay != null)
				overlays.add(grGridOverlay);
			if (llGridOverlay != null)
				overlays.add(llGridOverlay);
		}
		return overlays;
	}

	public void notifyOverlays()
	{
		if (!executionHandler.hasMessages(UPDATE_MESSAGE))
		{
			Message msg = Message.obtain(executionHandler, new Runnable() {
				@Override
				public void run()
				{
					executionHandler.removeMessages(UPDATE_MESSAGE);
					
					List<MapOverlay> overlays = getOverlays(ORDER_SHOW_PREFERENCE);
	
					for (MapOverlay mo : overlays)
					{
						boolean enabled = mo.setEnabled(false);
						mo.onMapChanged();
						mo.setEnabled(enabled);
						application.getMapHolder().refreshMap();
					}
				}
			});
			msg.what = UPDATE_MESSAGE;
			executionHandler.sendMessage(msg);
		}
	}

	public void onPreferencesChanged(final SharedPreferences settings)
	{
		for (TrackOverlay to : fileTrackOverlays)
		{
			to.onPreferencesChanged(settings);
		}
		for (RouteOverlay ro : routeOverlays)
		{
			ro.onPreferencesChanged(settings);
		}
		if (waypointsOverlay != null)
		{
			waypointsOverlay.onPreferencesChanged(settings);
		}
		if (navigationOverlay != null)
		{
			navigationOverlay.onPreferencesChanged(settings);
		}
		if (mapObjectsOverlay != null)
		{
			mapObjectsOverlay.onPreferencesChanged(settings);
		}
		if (distanceOverlay != null)
		{
			distanceOverlay.onPreferencesChanged(settings);
		}
		if (accuracyOverlay != null)
		{
			accuracyOverlay.onPreferencesChanged(settings);
		}
		if (currentTrackOverlay != null)
		{
			currentTrackOverlay.onPreferencesChanged(settings);
		}
	}

	public void initGrids(OzfMap currentMap)
	{
		llGridOverlay = null;
		grGridOverlay = null;
		if (mapGrid && currentMap != null && currentMap.llGrid != null && currentMap.llGrid.enabled)
		{
			LatLonGridOverlay llgo = new LatLonGridOverlay();
			llgo.setGrid(currentMap.llGrid);
			llGridOverlay = llgo;
		}
		if (mapGrid && currentMap != null && currentMap.grGrid != null && currentMap.grGrid.enabled && (!userGrid || gridPrefer == 0))
		{
			OtherGridOverlay ogo = new OtherGridOverlay();
			ogo.setGrid(currentMap.grGrid);
			grGridOverlay = ogo;
		}
		else if (userGrid && currentMap != null)
		{
			SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(application);
			OtherGridOverlay ogo = new OtherGridOverlay();
			Grid grid = new Grid();
			grid.color1 = 0xFF0000FF;
			grid.color2 = 0xFF0000FF;
			grid.color3 = 0xFF0000FF;
			grid.enabled = true;
			// TODO Optimize this
			grid.spacing = Integer.parseInt(settings.getString(application.getString(R.string.pref_grid_userscale), application.getResources().getString(R.string.def_grid_userscale)));
			int distanceIdx = Integer.parseInt(settings.getString(application.getString(R.string.pref_grid_userunit), "0"));
			grid.spacing *= Double.parseDouble(application.getResources().getStringArray(R.array.distance_factors_short)[distanceIdx]);
			grid.maxMPP = Integer.parseInt(settings.getString(application.getString(R.string.pref_grid_usermpp), application.getResources().getString(R.string.def_grid_usermpp)));
			ogo.setGrid(grid);
			grGridOverlay = ogo;
		}
	}

	public void clear()
	{
		setNavigationOverlayEnabled(false);
		setCurrentTrackOverlayEnabled(false);
		setAccuracyOverlayEnabled(false);
		setDistanceOverlayEnabled(false);

		if (llGridOverlay != null)
			llGridOverlay.onBeforeDestroy();
		if (grGridOverlay != null)
			grGridOverlay.onBeforeDestroy();

		llGridOverlay = null;
		grGridOverlay = null;

		for (TrackOverlay to : fileTrackOverlays)
		{
			to.onBeforeDestroy();
		}
		fileTrackOverlays.clear();
		for (RouteOverlay ro : routeOverlays)
		{
			ro.onBeforeDestroy();
		}
		routeOverlays.clear();

		mapObjectsOverlay.onBeforeDestroy();
		waypointsOverlay.onBeforeDestroy();

		mapObjectsOverlay = null;
		waypointsOverlay = null;

		executionHandler.removeCallbacksAndMessages(null);
		executionHandler = null;
	}
} public class RouteOverlay extends MapOverlay
{
	private Paint linePaint;
	private Paint borderPaint;
	private Paint fillPaint;
	private Paint textPaint;
	private Paint textFillPaint;
	private Route route;
	private Map<Waypoint, Bitmap> bitmaps;

	private int pointWidth = 10;
	private int routeWidth = 2;
	private boolean showNames;

	private boolean preserveWidth = false;

	public RouteOverlay(final Route route)
	{
		super();

		this.route = route;
		bitmaps = new WeakHashMap<Waypoint, Bitmap>();

		Resources resources = application.getResources();

		linePaint = new Paint();
		linePaint.setAntiAlias(true);
		linePaint.setStrokeWidth(routeWidth);
		linePaint.setStyle(Paint.Style.STROKE);
		linePaint.setColor(resources.getColor(R.color.routeline));
		fillPaint = new Paint();
		fillPaint.setAntiAlias(false);
		fillPaint.setStrokeWidth(1);
		fillPaint.setStyle(Paint.Style.FILL_AND_STROKE);
		fillPaint.setColor(resources.getColor(R.color.routewaypoint));
		borderPaint = new Paint();
		borderPaint.setAntiAlias(false);
		borderPaint.setStrokeWidth(1);
		borderPaint.setStyle(Paint.Style.STROKE);
		borderPaint.setColor(resources.getColor(R.color.routeline));
		textPaint = new Paint();
		textPaint.setAntiAlias(true);
		textPaint.setStrokeWidth(2);
		textPaint.setStyle(Paint.Style.FILL);
		textPaint.setTextAlign(Align.LEFT);
		textPaint.setTextSize(pointWidth * 1.5f);
		textPaint.setTypeface(Typeface.SANS_SERIF);
		textPaint.setColor(resources.getColor(R.color.routewaypointtext));
		textFillPaint = new Paint();
		textFillPaint.setAntiAlias(false);
		textFillPaint.setStrokeWidth(1);
		textFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);
		textFillPaint.setColor(resources.getColor(R.color.routeline));

		onPreferencesChanged(PreferenceManager.getDefaultSharedPreferences(application));

		enabled = true;

		if (route.width <= 0)
			route.width = (int) linePaint.getStrokeWidth();

		if (route.lineColor == -1)
			route.lineColor = linePaint.getColor();
		
		onRoutePropertiesChanged();
	}

	private void initRouteColors()
	{
		linePaint.setColor(route.lineColor);
		linePaint.setAlpha(0xAA);
		borderPaint.setColor(route.lineColor);
		textFillPaint.setColor(route.lineColor);
		textFillPaint.setAlpha(0x88);
		double Y = getLuminance(route.lineColor);
		if (Y <= .5)
			textPaint.setColor(Color.WHITE);
		else
			textPaint.setColor(Color.BLACK);
	}

	private double adjustValue(int cc)
	{
		double val = cc;
		val = val / 255;
		if (val <= 0.03928)
			val = val / 12.92;
		else
			val = Math.pow(((val + 0.055) / 1.055), 2.4);
		return val;
	}

	private double getLuminance(int rgb)
	{
		// http://www.w3.org/TR/WCAG20/relative-luminance.xml
		int R = (rgb & 0x00FF0000) >>> 16;
		int G = (rgb & 0x0000FF00) >>> 8;
		int B = (rgb & 0x000000FF);
		return 0.2126 * adjustValue(R) + 0.7152 * adjustValue(G) + 0.0722 * adjustValue(B);
	}

	public void onRoutePropertiesChanged()
	{
		if (linePaint.getStrokeWidth() != route.width)
		{
			routeWidth = route.width;
			preserveWidth = true;
		}
		if (linePaint.getColor() != route.lineColor)
		{
			initRouteColors();
		}
		if (route.editing)
		{
			linePaint.setPathEffect(new DashPathEffect(new float[] { 5, 2 }, 0));
			linePaint.setStrokeWidth(routeWidth * 3);
		}
		else
		{
			linePaint.setPathEffect(null);
			linePaint.setStrokeWidth(routeWidth);
		}
		bitmaps.clear();
	}

	public void onBeforeDestroy()
	{
		super.onBeforeDestroy();
		bitmaps.clear();
	}

	public Route getRoute()
	{
		return route;
	}

	@Override
	public boolean onSingleTap(MotionEvent e, Rect mapTap, MapView mapView)
	{
		if (! route.show)
			return false;

		List<Waypoint> waypoints = route.getWaypoints();
		synchronized (waypoints)
		{
			for (int i = waypoints.size() - 1; i >= 0; i--)
			{
				Waypoint wpt = waypoints.get(i);
				int[] pointXY = application.getXYbyLatLon(wpt.latitude, wpt.longitude);
				if (mapTap.contains(pointXY[0], pointXY[1]))
				{
					return application.getMapHolder().routeWaypointTapped(route, i, (int) e.getX(), (int) e.getY());
				}
			}
		}
		return false;
	}

	@Override
	public void onPrepareBuffer(final Viewport viewport, final Canvas c)
	{
		if (!route.show)
			return;

		final int[] cxy = viewport.mapCenterXY;

		final Path path = new Path();
		int i = 0;
		int lastX = 0, lastY = 0;
		List<Waypoint> waypoints = route.getWaypoints();
		synchronized (waypoints)
		{
			for (Waypoint wpt : waypoints)
			{
				int[] xy = application.getXYbyLatLon(wpt.latitude, wpt.longitude);

				if (i == 0)
				{
					path.setLastPoint(xy[0] - cxy[0], xy[1] - cxy[1]);
					lastX = xy[0];
					lastY = xy[1];
				}
				else
				{
					if (Math.abs(lastX - xy[0]) > 2 || Math.abs(lastY - xy[1]) > 2)
					{
						path.lineTo(xy[0] - cxy[0], xy[1] - cxy[1]);
						lastX = xy[0];
						lastY = xy[1];
					}
				}
				i++;
			}
		}
		c.drawPath(path, linePaint);
	}

	@Override
	public void onPrepareBufferEx(final Viewport viewport, final Canvas c)
	{
		if (!route.show)
			return;

		final int[] cxy = viewport.mapCenterXY;

		final int half = Math.round(pointWidth / 2);

		List<Waypoint> waypoints = route.getWaypoints();

		synchronized (waypoints)
		{
			for (Waypoint wpt : waypoints)
			{
				Bitmap bitmap = bitmaps.get(wpt);
				if (bitmap == null)
				{
					int width = pointWidth;
					int height = pointWidth + 2;

					if (showNames)
					{
						Rect bounds = new Rect();
						textPaint.getTextBounds(wpt.name, 0, wpt.name.length(), bounds);
						bounds.inset(-2, -4);
						width += 5 + bounds.width();
						if (height < bounds.height())
							height = bounds.height();
					}

					bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
					Canvas bc = new Canvas(bitmap);

					bc.translate(half, half);
					if (showNames)
						bc.translate(0, 2);
					bc.drawCircle(0, 0, half, fillPaint);
					bc.drawCircle(0, 0, half, borderPaint);

					if (showNames)
					{
						Rect rect = new Rect();
						textPaint.getTextBounds(wpt.name, 0, wpt.name.length(), rect);
						rect.inset(-2, -4);
						rect.offset(+half + 5, +half - 3);
						bc.drawRect(rect, textFillPaint);
						bc.drawText(wpt.name, +half + 6, +half, textPaint);
					}
					bitmaps.put(wpt, bitmap);
				}
				int[] xy = application.getXYbyLatLon(wpt.latitude, wpt.longitude);

				Matrix matrix = new Matrix();
				if (viewport.mapHeading != 0f)
					matrix.preRotate(viewport.mapHeading, half, half);
				matrix.postTranslate(xy[0] - half - cxy[0], xy[1] - half - cxy[1]);
				c.drawBitmap(bitmap, matrix, null);
			}
		}
	}

	@Override
	public void onPreferencesChanged(SharedPreferences settings)
	{
		Resources resources = application.getResources();

		if (!preserveWidth)
			routeWidth = settings.getInt(application.getString(R.string.pref_route_linewidth), resources.getInteger(R.integer.def_route_linewidth));
		pointWidth = settings.getInt(application.getString(R.string.pref_route_pointwidth), resources.getInteger(R.integer.def_route_pointwidth));
		showNames = settings.getBoolean(application.getString(R.string.pref_route_showname), true);

		if (!route.editing)
		{
			linePaint.setStrokeWidth(routeWidth);
		}
		textPaint.setTextSize(pointWidth * 1.5f);
		bitmaps.clear();
	}

} public class TrackOverlay extends MapOverlay
{
	Paint paint;
	Track track;

	private boolean preserveWidth = false;
	private boolean preserveColor = false;

	public TrackOverlay()
	{
		super();

		track = new Track();

		paint = new Paint();
		paint.setAntiAlias(true);
		paint.setStrokeWidth(3);
		paint.setStyle(Paint.Style.STROKE);

		onPreferencesChanged(PreferenceManager.getDefaultSharedPreferences(application));

		enabled = true;
	}

	public TrackOverlay(final Track aTrack)
	{
		this();

		track = aTrack;
		if (track.width > 0)
		{
			paint.setStrokeWidth(track.width);
			preserveWidth = true;
		}
		else
		{
			track.width = (int) paint.getStrokeWidth();
		}
		if (track.color != -1)
		{
			paint.setColor(track.color);
			preserveColor = true;
		}
		else
		{
			track.color = paint.getColor();
		}
	}

	public void onTrackPropertiesChanged()
	{
		if (paint.getStrokeWidth() != track.width)
		{
			paint.setStrokeWidth(track.width);
			preserveWidth = true;
		}
		if (paint.getColor() != track.color)
		{
			paint.setColor(track.color);
			preserveColor = true;
		}
	}

	public void setTrack(Track track)
	{
		this.track = track;
		onTrackPropertiesChanged();
	}

	public Track getTrack()
	{
		return track;
	}

	@Override
	public void onMapChanged()
	{
		synchronized (track)
		{
			for (Track.TrackSegment segment : track.getSegments())
			{
				synchronized (segment)
				{
					List<Track.TrackPoint> points = segment.getPoints();
					for (Track.TrackPoint tp : points)
					{
						tp.dirty = true;
					}
				}
			}
		}
		application.getMapHolder().refreshMap();
	}

	@Override
	public void onPrepareBuffer(final Viewport viewport, final Canvas c)
	{
		if (!track.show)
			return;

		final int[] cxy = viewport.mapCenterXY;

		int w2 = viewport.canvasWidth / 2;
		int h2 = viewport.canvasHeight / 2;
		int left = cxy[0] - w2;
		int right = cxy[0] + w2;
		int top = cxy[1] - h2;
		int bottom = cxy[1] + h2;

		final Path path = new Path();
		boolean first = true;
		boolean skipped = false;
		int lastX = 0, lastY = 0;
		int[] xy = new int[2];
		
		synchronized (track)
		{
			for (Track.TrackSegment segment : track.getSegments())
			{
				if (! viewport.mapArea.intersects(segment.bounds))
					continue;
				synchronized (segment)
				{
					List<Track.TrackPoint> points = segment.getPoints();
					if (points.size() == 0)
						continue;
					for (Track.TrackPoint tp : points)
					{
						if (tp.dirty)
						{
							application.getXYbyLatLon(tp.latitude, tp.longitude, xy);
							tp.x = xy[0];
							tp.y = xy[1];
							tp.dirty = false;
						}
						else
						{
							xy[0] = tp.x;
							xy[1] = tp.y;
						}
		
						if (first)
						{
							path.setLastPoint(xy[0] - cxy[0], xy[1] - cxy[1]);
							lastX = xy[0];
							lastY = xy[1];
							first = false;
							continue;
						}
						if ((lastX == xy[0] && lastY == xy[1]) ||
							lastX < left && cxy[0] < left ||
							lastX > right && cxy[0] > right ||
							lastY < top && cxy[1] < top ||
							lastY > bottom && cxy[1] > bottom)
						{
							lastX = xy[0];
							lastY = xy[1];
							skipped = true;
							continue;
						}
						if (skipped)
						{
							path.moveTo(lastX - cxy[0], lastY - cxy[1]);
							skipped = false;
						}
						if (tp.continous)
							path.lineTo(xy[0] - cxy[0], xy[1] - cxy[1]);
						else
							path.moveTo(xy[0] - cxy[0], xy[1] - cxy[1]);
						lastX = xy[0];
						lastY = xy[1];
					}
				}
			}
		}
		c.drawPath(path, paint);
	}

	@Override
	public void onPrepareBufferEx(final Viewport viewport, final Canvas c)
	{
	}

	@Override
	public void onPreferencesChanged(SharedPreferences settings)
	{
		Resources resources = application.getResources();		
		if (!preserveWidth)
			paint.setStrokeWidth(settings.getInt(application.getString(R.string.pref_tracking_linewidth), resources.getInteger(R.integer.def_track_linewidth)));
		if (!preserveColor)
			paint.setColor(settings.getInt(application.getString(R.string.pref_tracking_currentcolor), resources.getColor(R.color.currenttrack)));
	}

} public class TooltipManager
{
	private static long currentState = 0;
	private static Context context;
	private static TooltipPopup popup;

	/**
	 * Delay before first tooltip should be displayed after user opens the screen
	 */
	public static final long TOOLTIP_DELAY = 10000; // 10 seconds
	/**
	 * Delay before first tooltip should be displayed after user opens the screen
	 * on which she does not stay for long time
	 */
	public static final long TOOLTIP_DELAY_SHORT = 1000; // 1 second
	/**
	 * Pause between first and subsequent tooltips for the same screen
	 */
	public static final long TOOLTIP_PERIOD = 30000; // 30 seconds

	public static final long TOOLTIP_MAP_BUTTONS                    = 0x0000000000000001L;
	public static final long TOOLTIP_QUICK_ZOOM                     = 0x0000000000000002L;
	public static final long TOOLTIP_SWITCH_MAP                     = 0x0000000000000004L;
	public static final long TOOLTIP_LOAD_MAP                       = 0x0000000000000008L;
	public static final long TOOLTIP_CURRENT_LOCATION               = 0x0000000000000010L;
	public static final long TOOLTIP_UTM_ZONE                       = 0x0000000000000020L;
	public static final long TOOLTIP_DATA_LIST                      = 0x0000000000000040L;
	public static final long TOOLTIP_WAYPOINT_COORDINATES           = 0x0000000000000080L;
	public static final long TOOLTIP_BUSY                           = 0x8000000000000000L;

	@Nullable
	private static String getTooltipString(long tooltip)
	{
		Resources resources = context.getResources();
		if (tooltip == TOOLTIP_MAP_BUTTONS)
			return resources.getString(R.string.showcase_map_buttons);
		if (tooltip == TOOLTIP_QUICK_ZOOM)
			return resources.getString(R.string.showcase_quick_zoom);
		if (tooltip == TOOLTIP_SWITCH_MAP)
			return resources.getString(R.string.showcase_switch_map);
		if (tooltip == TOOLTIP_LOAD_MAP)
			return resources.getString(R.string.showcase_load_map);
		if (tooltip == TOOLTIP_CURRENT_LOCATION)
			return resources.getString(R.string.showcase_current_location);
		if (tooltip == TOOLTIP_UTM_ZONE)
			return resources.getString(R.string.showcase_utm_zone);
		if (tooltip == TOOLTIP_DATA_LIST)
			return resources.getString(R.string.showcase_data_list);
		if (tooltip == TOOLTIP_WAYPOINT_COORDINATES)
			return resources.getString(R.string.showcase_waypoint_coordinates);
		return null;
	}

	public static void initialize(Context context)
	{
		TooltipManager.context = context;
		SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
		currentState = settings.getLong(context.getResources().getString(R.string.ui_tooltips_state), 0L);
	}

	/**
	 * Returns next unseen tooltip for specified screen
	 * @param screen UI screen tag
	 * @return tooltip ID or 0 if there are no more tooltips or busy flag
	 * if some tooltip is already displayed
	 */
	public static long getTooltip(String screen)
	{
		if (context == null)
			throw new IllegalStateException("TooltipManager not initialized");
		if (popup != null)
			return TOOLTIP_BUSY;
		if (MapFragment.TAG.equals(screen))
		{
			if ((currentState & TOOLTIP_MAP_BUTTONS) == 0L)
				return TOOLTIP_MAP_BUTTONS;
			if ((currentState & TOOLTIP_SWITCH_MAP) == 0L)
				return TOOLTIP_SWITCH_MAP;
			if ((currentState & TOOLTIP_CURRENT_LOCATION) == 0L)
				return TOOLTIP_CURRENT_LOCATION;
			if ((currentState & TOOLTIP_QUICK_ZOOM) == 0L)
				return TOOLTIP_QUICK_ZOOM;
		}
		else if (SuitableMapsList.TAG.equals(screen))
		{
			if ((currentState & TOOLTIP_LOAD_MAP) == 0L)
				return TOOLTIP_LOAD_MAP;
		}
		else if (WaypointDetails.TAG.equals(screen))
		{
			if ((currentState & TOOLTIP_WAYPOINT_COORDINATES) == 0L)
				return TOOLTIP_WAYPOINT_COORDINATES;
		}
		else if (WaypointProperties.TAG.equals(screen))
		{
			if ((currentState & TOOLTIP_UTM_ZONE) == 0L)
				return TOOLTIP_UTM_ZONE;
		}
		else if (WaypointList.TAG.equals(screen))
		{
			if ((currentState & TOOLTIP_DATA_LIST) == 0L)
				return TOOLTIP_DATA_LIST;
		}
		return 0L;
	}

	/**
	 * Shows tooltip at the specified anchor view. State is saved on tooltip dismiss.
	 * @param tooltip tooltip ID
	 * @param anchor anchor view
	 */
	public static void showTooltip(final long tooltip, View anchor)
	{
		if (context == null)
			throw new IllegalStateException("TooltipManager not initialized");
		popup = new TooltipPopup(context, getTooltipString(tooltip));
		popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
			@Override
			public void onDismiss()
			{
				currentState |= tooltip;
				SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
				editor.putLong(context.getResources().getString(R.string.ui_tooltips_state), currentState);
				editor.commit();
				popup = null;
			}
		});
		popup.show(anchor);
	}

	public static void dismiss()
	{
		if (popup != null)
		{
			popup.setOnDismissListener(null);
			popup.dismiss();
			popup = null;
		}
	}
} public enum Zenith
{
    /** Astronomical sunrise/set is when the sun is 18 degrees below the horizon. */
    ASTRONOMICAL(108d),

    /** Nautical sunrise/set is when the sun is 12 degrees below the horizon. */
    NAUTICAL(102d),

    /** Civil sunrise/set (dawn/dusk) is when the sun is 6 degrees below the horizon. */
    CIVIL(96d),

    /** Official sunrise/set is when the sun is 50' below the horizon. */
    OFFICIAL(90.8333d); // 90deg, 50'

    private final double degrees;

    private Zenith(double degrees)
    {
        this.degrees = degrees;
    }

    public double degrees()
    {
        return degrees;
    }
} public class OziExplorerFiles
{
	final static DecimalFormat numFormat = new DecimalFormat("* ###0");
	final static DecimalFormat coordFormat = new DecimalFormat("* ###0.000000", new DecimalFormatSymbols(Locale.ENGLISH));
	
	/**
	 * Loads waypoints from file
	 * 
	 * @param file valid <code>File</code> with waypoints
	 * @return <code>List</code> of <code>Waypoint</code>s
	 * @throws IOException 
	 */
	public static List<Waypoint> loadWaypointsFromFile(final File file, final String charset) throws IOException
	{
		List<Waypoint> waypoints = new ArrayList<Waypoint>();

	    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset));
	    
	    String line = null;

	    // OziExplorer Waypoint File Version 1.0
		reader.readLine();
	    // WGS 84
		reader.readLine();
	    // Reserved 2
		reader.readLine();
	    // Reserved 3
		reader.readLine();
		//21,PTRS          , -26.636541, 152.449640,35640.91155, 0, 1, 3,  16777215,  16711935,Peach Trees Camping area                , 0, 0
	    while ((line = reader.readLine()) != null)
		{
	    	try
	    	{
	    		String[] fields = CSV.parseLine(line);
		    	if (fields.length >= 11)
		    	{
			    	if ("".equals(fields[1]))
			    		fields[1] = "WPT"+fields[0];
			    	
		    		Waypoint waypoint = new Waypoint(fields[1].replace((char) 209, ','), Entities.XML.unescape(fields[10]), Double.parseDouble(fields[2]), Double.parseDouble(fields[3]));
	
		    		if (! "".equals(fields[4]))
		    		{
		    			try 
		    			{
		    				waypoint.date = TDateTime.dateFromDateTime(Double.parseDouble(fields[4]));
		    			}
			    		catch (NumberFormatException e)
			    		{
			    			e.printStackTrace();
			    		}
		    		}
		    		
			    	if (! "".equals(fields[8]))
			    	{
			    		try
			    		{
			    			int fgcolor = Integer.parseInt(fields[8]);
			    			if (fgcolor != 0)
			    				waypoint.textcolor = bgr2rgb(fgcolor);
			    		}
			    		catch (NumberFormatException e)
			    		{
			    		}
			    	}
			    	if (! "".equals(fields[9]))
			    	{
			    		try
			    		{
			    			int bgcolor = Integer.parseInt(fields[9]);
				    		if (bgcolor != 65535)
				    			waypoint.backcolor = bgr2rgb(bgcolor);
			    		}
			    		catch (NumberFormatException e)
			    		{
			    		}
			    	}
	
		    		if (fields.length >= 14 && ! "".equals(fields[13]))
		    		{
			    		try
			    		{
			    			waypoint.proximity = Integer.parseInt(fields[13]);
			    		}
			    		catch (NumberFormatException e)
			    		{
			    		}
			    	}
	
		    		if (fields.length >= 15 && ! "".equals(fields[14]))
		    		{
			    		try
			    		{
			    			int alt = Integer.parseInt(fields[14]);
			    			waypoint.altitude = alt == -777 ? Integer.MIN_VALUE : alt;
		    			}
			    		catch (NumberFormatException e)
			    		{
			    		}
			    	}
	
		    		if (fields.length >= 22 && ! "".equals(fields[21]))
		    		{
		    			waypoint.marker = fields[21];
			    	}
		    		waypoints.add(waypoint);
		    	}
	    	}
	    	catch (IllegalArgumentException e)
	    	{
	    		//TODO Show error to user
	    		e.printStackTrace();
	    	}
	    }
		reader.close();

		return waypoints;
	}
	
	/**
	 * Saves waypoints to file.
	 * 
	 * @param file valid <code>File</code>
	 * @param waypoints <code>List</code> of <code>Waypoint</code>s to save
	 * @throws IOException
	 */
	public static void saveWaypointsToFile(final File file, final String charset, final List<Waypoint> waypoints) throws IOException
	{
	    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), charset));

		writer.write("OziExplorer Waypoint File Version 1.1\n" +
				  "WGS 84\n" +
				  "Reserved 2\n" +
				  "Reserved 3\n");
		
	    //*  One line per waypoint
	    //* each field separated by a comma
	    //* comma's not allowed in text fields, character 209 can be used instead and a comma will be substituted.
	    //* non essential fields need not be entered but comma separators must still be used (example ,,)
	    //  defaults will be used for empty fields
	    //* Any number of the last fields in a data line need not be included at all not even the commas.

	    //Field 1 : Number - this is the location in the array (max 1000), must be unique, usually start at 1 and increment. Can be set to -1 (minus 1) and the number will be auto generated.
		//Field 2 : Name - the waypoint name, use the correct length name to suit the GPS type.
		//Field 3 : Latitude - decimal degrees
		//Field 4 : Longitude - decimal degrees
		//Field 5 : Date - see Date Format below, if blank a preset date will be used
		//Field 6 : Symbol - 0 to number of symbols in GPS
		//Field 7 : Status - always set to 1
		//Field 8 : Map Display Format
		//Field 9 : Foreground Color (RGB value)
		//Field 10 : Background Color (RGB value)
		//Field 11 : Description (max 40), no commas
		//Field 12 : Pointer Direction
		//Field 13 : Garmin Display Format
		//Field 14 : Proximity Distance - 0 is off any other number is valid
		//Field 15 : Altitude - in feet (-777 if not valid)
		//Field 16 : Font Size - in points
		//Field 17 : Font Style - 0 is normal, 1 is bold.
		//Field 18 : Symbol Size - 17 is normal size
		//Field 19 : Proximity Symbol Position
		//Field 20 : Proximity Time
		//Field 21 : Proximity or Route or Both
		//Field 22 : File Attachment Name
		//Field 23 : Proximity File Attachment Name
		//Field 24 : Proximity Symbol Name 
	
        synchronized (waypoints)
        {
	        for (Waypoint wpt : waypoints)
	        {
	        	writer.write("-1,");
	        	writer.write(wpt.name.replace(',', (char) 209)+",");
	        	writer.write(coordFormat.format(wpt.latitude)+","+coordFormat.format(wpt.longitude)+",");
	        	writer.write((wpt.date == null ? "" : TDateTime.toDateTime(wpt.date))+",");
	        	writer.write("0,1,3,");
	        	writer.write((wpt.textcolor != Integer.MIN_VALUE ? rgb2bgr(wpt.textcolor) : "") + ",");
	        	writer.write((wpt.backcolor != Integer.MIN_VALUE ? rgb2bgr(wpt.backcolor) : "") + ",");
	        	writer.write(Entities.XML.escape(wpt.description) + ",");
	        	writer.write("2,0,");
	        	writer.write(wpt.proximity + ",");
	        	writer.write((wpt.altitude == Integer.MIN_VALUE ? -777 : wpt.altitude) + ",");
	        	writer.write(",,,,,,");
	        	writer.write(wpt.marker+",,");
	        	writer.write("\n");
	        }
        }
		writer.close();
	}
	
	/**
	 * Loads track from file.
	 * 
	 * @param file valid <code>File</code> with track points
	 * @return <code>Track</code> with track points
	 * @throws IOException on file read error
	 * @throws IllegalArgumentException if file format is not plt
	 */
	public static Track loadTrackFromFile(final File file, final String charset) throws IllegalArgumentException, IOException
	{
		return loadTrackFromFile(file, charset, 0);
	}
	
	/**
	 * Loads track from file.
	 * 
	 * @param file valid <code>File</code> with track points
	 * @param lines number of last lines to read
	 * @return <code>Track</code> with track points
	 * @throws IOException on file read error
	 * @throws IllegalArgumentException if file format is not plt
	 */
	public static Track loadTrackFromFile(final File file, final String charset, final long lines) throws IllegalArgumentException, IOException
	{
		Track track = new Track();
		
		long skip = 0;
		if (lines > 0)
		{
			skip = file.length() - 35 * lines; // 35 - average line length in conventional track file
		}

	    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset));

	    String line = null;

	    // OziExplorer Track Point File Version 2.0
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
	    // WGS 84
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
	    // Altitude is in Feet
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
	    // Reserved 3
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
	    // 0,2,255,OziCE Track Log File,1
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
		String[] fields = CSV.parseLine(line);
	    if (fields.length < 4)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
		track.width=Integer.parseInt(fields[1]);
		track.color=bgr2rgb(Integer.parseInt(fields[2]));
		track.name=fields[3];
	    // 0
	    if ((line = reader.readLine()) == null)
	    {
	    	reader.close();
	    	throw new IllegalArgumentException("Bad track file");
	    }
	    skip -= line.length();
	    skip -= 12; // new line characters
		
		if (skip > 0)
		{
			reader.skip(skip);
			reader.readLine(); // skip broken line
		}

	    //   55.6384683,  37.3516133,0,    583.0,    0.0000000 ,290705,185332.996
	    while ((line = reader.readLine()) != null)
		{
			fields = CSV.parseLine(line);
			long time = fields.length > 4 ? TDateTime.fromDateTime(Double.parseDouble(fields[4])): 0L;
			double elevation = fields.length > 3 ? Double.parseDouble(fields[3]) * 0.3048: 0;
			if (fields.length >= 3)
				track.addPoint("0".equals(fields[2]) ? true : false, Double.parseDouble(fields[0]), Double.parseDouble(fields[1]), elevation, 0.0, 0.0, 0.0, time);
	    }
		reader.close();
		
		track.show = true;
		track.filepath = file.getCanonicalPath();
		if ("".equals(track.name))
			track.name = track.filepath;

		return track;
	}

	/**
	 * Saves track to file.
	 * 
	 * @param file valid <code>File</code>
	 * @param charset the string describing the desired character encoding
	 * @param track <code>Track</code> object containing the list of track points to save
	 * @throws IOException
	 */
	public static void saveTrackToFile(final File file, final String charset, final Track track) throws IOException
	{
	    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), charset));

	    writer.write("OziExplorer Track Point File Version 2.1\n" +
				"WGS 84\n" +
				"Altitude is in Feet\n" +
				"Reserved 3\n");

		// Field 1 : always zero (0)
		// Field 2 : width of track plot line on screen - 1 or 2 are usually the best
		// Field 3 : track color (RGB)
		// Field 4 : track description (no commas allowed)
		// Field 5 : track skip value - reduces number of track points plotted, usually set to 1
		// Field 6 : track type - 0 = normal , 10 = closed polygon , 20 = Alarm Zone
		// Field 7 : track fill style - 0 =bsSolid; 1 =bsClear; 2 =bsBdiagonal; 3 =bsFdiagonal; 4 =bsCross;
		// 5 =bsDiagCross; 6 =bsHorizontal; 7 =bsVertical;
		// Field 8 : track fill color (RGB)
		writer.write("0,"+String.valueOf(track.width)+","+
				String.valueOf(rgb2bgr(track.color))+","+
	        	track.name.replace(',', (char) 209)+",0,0\n"+
				"0\n");
	
		//Field 1 : Latitude - decimal degrees
		//Field 2 : Longitude - decimal degrees
		//Field 3 : Code - 0 if normal, 1 if break in track line
		//Field 4 : Altitude in feet (-777 if not valid)
		//Field 5 : Date - see Date Format below, if blank a preset date will be used
		//Field 6 : Date as a string
		//Field 7 : Time as a string
		// Note that OziExplorer reads the Date/Time from field 5, the date and time in fields 6 & 7 are ignored.
	
		//-27.350436, 153.055540,1,-777,36169.6307194, 09-Jan-99, 3:08:14 
	
        List<TrackPoint> trackPoints = track.getAllPoints();
        synchronized (trackPoints)
        {  
	        for (TrackPoint tp : trackPoints)
	        {
	        	writer.write(coordFormat.format(tp.latitude)+","+coordFormat.format(tp.longitude)+",");
	        	if (tp.continous)
	        		writer.write("0");
	        	else
	        		writer.write("1");
	        	writer.write(","+String.valueOf(Math.round(tp.elevation * 3.2808399)));
	        	if (tp.time > 0)
	        	{
		        	writer.write(","+String.valueOf(TDateTime.toDateTime(tp.time)));
	        	}
	        	writer.write("\n");
	        }
        }
        writer.close();
	}
	
	/**
	 * Loads routes from file.
	 * 
	 * @param file valid <code>File</code> with route waypoints
	 * @return <code>List<Route></code> the list of routes
	 * @throws IOException on file read error
	 * @throws IllegalArgumentException if file format is not rt2 or rte
	 */
	public static List<Route> loadRoutesFromFile(final File file, final String charset) throws IOException, IllegalArgumentException
	{
		List<Route> routes = new ArrayList<Route>();
		
	    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset));
	    
	    String line = reader.readLine();
		String[] fields = CSV.parseLine(line);
	    
		if ("H1".equals(fields[0]))
		{
			// rt2 format

			// H1,OziExplorer CE Route2 File Version 1.0
			// H2,WGS 84
		    line = reader.readLine();
			fields = CSV.parseLine(line);
			if (! "H2".equals(fields[0]))
			{
				reader.close();
				throw new IllegalArgumentException("Bad rt2 header");
			}
			// H3,My route,,0
		    line = reader.readLine();
			fields = CSV.parseLine(line);
			if (! "H3".equals(fields[0]))
			{
				reader.close();
				throw new IllegalArgumentException("Bad rt2 header");
			}
			Route route = new Route();
			routes.add(route);
			route.name = fields[1].replace((char) 209, ',');
    		try
    		{
    			int width = Integer.parseInt(fields[2]);
    			if (width > 0)
    				route.width = width;
    		}
    		catch (NumberFormatException e)
    		{
    		}
    		try
    		{
    			int color = Integer.parseInt(fields[3]);
    			if (color != 0)
    				route.lineColor = bgr2rgb(color);
    		}
    		catch (NumberFormatException e)
    		{
    		}
			// W,Tsapelka,  58.0460242,  28.9465437,0
		    while ((line = reader.readLine()) != null)
			{
				fields = CSV.parseLine(line);
				if (! "W".equals(fields[0]))
					continue;
				route.addWaypoint(fields[1].replace((char) 209, ','), Double.parseDouble(fields[2]), Double.parseDouble(fields[3]));
	        	// Format extension (probably not compatible with OziExplorer)
	        	if (fields.length > 5)
	        	{
	        		try
	        		{
	        			int proximity = Integer.parseInt(fields[5]);
	        			if (proximity > 0)
	        				route.getWaypoint(route.length() - 1).proximity = proximity;
	        		}
	        		catch (NumberFormatException e)
	        		{
	        		}
	        	}

		    }
			reader.close();
			
			route.show = true;
			route.filepath = file.getCanonicalPath();
			if ("".equals(route.name))
				route.name = route.filepath;
		}
		else if ("OziExplorer Route File Version 1.0".equals(fields[0]))
		{
			// rte format

			//OziExplorer Route File Version 1.0
			//WGS 84
		    line = reader.readLine();
			//Reserved 1
		    line = reader.readLine();
			//Reserved 2
		    line = reader.readLine();
		    Route route = null;
		    int routeNum = -1;
		    int wptNum = 0;
			//R,  0,ROUTE 1         ,Description,255
			//W,  0,  1, 29,29              , -26.568702, 152.369428,35640.9202400, 0, 1, 0,   8388608,     65535,, 0, 0
		    //W,  1,  2, 35,35              , -26.550290, 152.416844,35641.5077900, 0, 1, 0,   8388608,     65535,, 0, 0
		    while ((line = reader.readLine()) != null)
			{
				fields = CSV.parseLine(line);
				int rtn = Integer.valueOf(fields[1]);
				if ("R".equals(fields[0]))
				{
					if (rtn == routeNum + 1)
					{
						if (route != null)
						{
							if (route.length() > 0)
							{
								route.show = true;
								if (routeNum == 0)
									route.filepath = file.getCanonicalPath();
								if ("".equals(route.name))
									route.name = "R"+routeNum;
								routes.add(route);
							}
							route = null;							
						}
						route = new Route();
						route.name = fields[2].replace((char) 209, ',');
						route.description = fields[3].replace((char) 209, ',');
						route.show = true;
						route.filepath = file.getCanonicalPath();
			    		try
			    		{
			    			int color = Integer.parseInt(fields[4]);
			    			if (color != 0)
			    				route.lineColor = bgr2rgb(color);
			    		}
			    		catch (NumberFormatException e)
			    		{
			    		}
						routeNum = rtn;
						wptNum = 0;
					}
					else
					{
						reader.close();
						throw new IllegalArgumentException("Bad route file");
					}
				}
				else if ("W".equals(fields[0]))
				{
					if (rtn != routeNum)
					{
						reader.close();
						throw new IllegalArgumentException("Bad route file");
					}
					int wpn = Integer.valueOf(fields[2]);
					if (wpn != wptNum + 1)
					{
						reader.close();
						throw new IllegalArgumentException("Bad route file");
					}
					wptNum++;
				    //W, 1, 2, 35,35              , -26.550290, 152.416844,35641.5077900, 0, 1, 0,   8388608,     65535,, 0, 0
			    	if ("".equals(fields[4]))
			    		fields[4] = "RWPT"+wptNum;
					route.addWaypoint(new Waypoint(fields[4].replace((char) 209, ','), fields[13].replace((char) 209, ','), Double.parseDouble(fields[5]), Double.parseDouble(fields[6])));
				}
			}
		}
		else
		{
			reader.close();
			throw new IllegalArgumentException("Bad route file");
		}

		return routes;
	}

	public static void saveRouteToFile(final File file, final String charset, final Route route) throws IOException
	{
	    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), charset));
		
		writer.write("H1,OziExplorer CE Route2 File Version 1.0\n" +
				"H2,WGS 84\n");

		// Field 1 : H3
		// Field 2 : route name (no commas allowed)
		// Field 3 : ??? (we use it for route line width)
		// Field 4 : route color (RGB)
		writer.write("H3,"+route.name.replace(',', (char) 209)+","+
				String.valueOf(route.width)+","+
				String.valueOf(rgb2bgr(route.lineColor))+"\n");
	
		//Field 1 : W
		//Field 2 : Name
		//Field 3 : Latitude - decimal degrees
		//Field 4 : Longitude - decimal degrees
		//Field 5 : Code - 0 if normal, 1 if silent
	
		// W,Tsapelka,  58.0460242,  28.9465437,0
	
        List<Waypoint> waypoints = route.getWaypoints();
        synchronized (waypoints)
        {  
	        for (Waypoint wpt : waypoints)
	        {
	        	writer.write("W,");
	        	writer.write(wpt.name.replace(',', (char) 209)+",");
	        	writer.write(coordFormat.format(wpt.latitude)+","+coordFormat.format(wpt.longitude)+",");
	        	if (wpt.silent)
	        		writer.write("1");
	        	else
	        		writer.write("0");
	        	// Format extension (probably not compatible with OziExplorer)
	        	if (wpt.proximity > 0)
	        		writer.write("," + String.valueOf(wpt.proximity));
	        	writer.write("\n");
	        }
        }
        writer.close();
	}

	public static int bgr2rgb(int bgr)
	{
		return 0xFF000000 | ((bgr & 0x00FF0000) >>> 16) | ((bgr & 0x000000FF) << 16) | (bgr & 0x0000FF00);
	}

	public static int rgb2bgr(int rgb)
	{
		return ((rgb & 0x00FF0000) >>> 16) | ((rgb & 0x000000FF) << 16) | (rgb & 0x0000FF00);
	}

	public static void loadDatums(File file) throws IOException
	{
	    BufferedReader reader = new BufferedReader(new FileReader(file));
	    String line;
	    while ((line = reader.readLine()) != null)
		{
			String[] fields = CSV.parseLine(line);
			if (fields.length == 5)
			{
				try
				{
					int e = Integer.parseInt(fields[1]);
					Ellipsoid ellipsoid = MapLoader.getEllipsoid(e);
					double dx = Double.parseDouble(fields[2]);
					double dy = Double.parseDouble(fields[3]);
					double dz = Double.parseDouble(fields[4]);
					if (ellipsoid != null)
					{
						// no need to get object reference because it is registered in constructor
						new Datum(fields[0], ellipsoid, dx, dy, dz);
					}
				}
				catch (Exception e)
				{
					e.printStackTrace();
				}
			}
		}
	    reader.close();
	}
} public class WaypointFileHelper
{
	/**
	 * Load waypoints in Ozi, KML or GPX format.
	 * 
	 * @param file File to load waypoints from
	 * @return Number of loaded waypoints
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 */
	public static int loadFile(File file) throws IOException, SAXException, ParserConfigurationException
	{
		Androzic application = Androzic.getApplication();
		List<Waypoint> waypoints = null;

		WaypointSet wptset = new WaypointSet(file);
		String lc = file.getName().toLowerCase(Locale.getDefault());
		if (lc.endsWith(".wpt"))
		{
			waypoints = OziExplorerFiles.loadWaypointsFromFile(file, application.charset);
		}
		else if (lc.endsWith(".kml"))
		{
			wptset.path = null;
			waypoints = KmlFiles.loadWaypointsFromFile(file);
		}
		else if (lc.endsWith(".gpx"))
		{
			wptset.path = null;
			waypoints = GpxFiles.loadWaypointsFromFile(file);
		}
		if (waypoints != null)
		{
			for (Waypoint waypoint : waypoints)
			{
				waypoint.set = wptset;
			}
			int count = application.addWaypoints(waypoints, wptset);
			return count;
		}
		return 0;
	}

} public class BufferedAssetsRenderTheme implements XmlRenderTheme
{
	private static final long serialVersionUID = 1L;

	private final String assetName;
	private final XmlRenderThemeMenuCallback menuCallback;
	private final String relativePathPrefix;
	private final byte[] data;

	public BufferedAssetsRenderTheme(Context context, String relativePathPrefix, String fileName, XmlRenderThemeMenuCallback menuCallback) throws IOException
	{
		this.assetName = fileName;
		this.relativePathPrefix = relativePathPrefix;

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		InputStream in = context.getAssets().open(this.assetName);
		byte[] buffer = new byte[1024];
		int read;
		while ((read = in.read(buffer, 0, buffer.length)) != -1)
			baos.write(buffer, 0, read);
		in.close();
		data = baos.toByteArray();
		this.menuCallback = menuCallback;
	}

	@Override
	public XmlRenderThemeMenuCallback getMenuCallback()
	{
		return this.menuCallback;
	}

	@Override
	public String getRelativePathPrefix()
	{
		return this.relativePathPrefix;
	}

	@Override
	public InputStream getRenderThemeAsStream()
	{
		return new ByteArrayInputStream(data);
	}
}
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;
		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);