content: /*
   * Androzic - android navigation client that uses OziExplorer maps (ozf2, ozfx3).
   * Copyright (C) 2010-2013 Andrey Novikov <http://andreynovikov.info/>
   * 
   * This file is part of Androzic application.
   * 
   * Androzic is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   * 
   * Androzic is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   * GNU General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with Androzic. If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.androzic.navigation;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.ComponentName;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
  import android.location.Location;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.preference.PreferenceManager;
  import android.support.v4.app.NotificationCompat;
  import android.util.Log;
  
  import com.androzic.Androzic;
  import com.androzic.HSIActivity;
  import com.androzic.MainActivity;
  import com.androzic.R;
  import com.androzic.data.MapObject;
  import com.androzic.data.Route;
  import com.androzic.location.ILocationListener;
  import com.androzic.location.ILocationService;
  import com.androzic.location.LocationService;
  import com.androzic.route.RouteDetails;
  import com.androzic.util.Geo;
  
  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)
  		{
  		}
  	};
  }
  