Commits

littledot5566 committed b2a6731

Completed implementation of HuntingActivity. TODO: use Location Providers instead of Criteria, they give more flexibility.

  • Participants
  • Parent commits 15fc182

Comments (0)

Files changed (12)

project.properties

 #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
 
 # Project target.
-target=Google Inc.:Google APIs:10
+target=Google Inc.:Google APIs:16
 android.library.reference.1=../Utils

res/layout/geolock_dia_lay.xml

+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/geolock_tv_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/geolock_tv_title" />
+
+    <LinearLayout
+        android:id="@+id/geolock_ll_content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <ProgressBar
+            android:id="@+id/geolock_pb_circle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/geolock_tv_content"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+
+</LinearLayout>

res/layout/hunting_act_layout.xml

     android:orientation="vertical"
     tools:context=".MainActivity" >
 
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal" >
-
-        <Button
-            android:id="@+id/next_dest"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:onClick="onGetDestination"
-            android:text="Go!" />
-    </LinearLayout>
-
     <TextView
         android:id="@+id/hunt_tv_cur_location"
         android:layout_width="wrap_content"

res/layout/main_act_layout.xml

         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:onClick="onStartNewHunt"
-        android:text="Start a new treasure expedition!" />
+        android:text="@string/main_but_startHunt" />
 
 </LinearLayout>

res/layout/selectdistance_act_layout.xml

         android:id="@+id/distance_tv_title"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
-        android:text="Set your distance!" />
+        android:text="@string/distance_tv_title" />
 
     <Button
         android:id="@+id/distance_but_500m"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:onClick="onChooseDistance"
-        android:text="500m" />
+        android:text="@string/distance_but_500m" />
 
     <LinearLayout
         android:id="@+id/distance_ll_custom"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:hint="Enter custom distance"
+            android:hint="@string/distance_et_custom"
             android:inputType="number"
             android:visibility="gone" />
 
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:onClick="onChooseCustomDistance"
-            android:text="Custom" />
+            android:text="@string/distance_but_custom" />
     </LinearLayout>
 
 </LinearLayout>

res/layout/splash_act_layout.xml

     android:layout_width="fill_parent"
     android:layout_height="fill_parent" >
 
-    <LinearLayout
+    <TextView
+        android:id="@+id/splash_tv_appName"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerInParent="true"
-        android:orientation="vertical" >
-
-        <ImageView
-            android:id="@+id/splash_iv_logo"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/ic_launcher" />
-
-        <TextView
-            android:id="@+id/splash_tv_appName"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Treasure Hunter" />
-    </LinearLayout>
+        android:drawableTop="@drawable/ic_launcher"
+        android:text="@string/app_name" />
 
 </RelativeLayout>

res/values-v11/styles.xml

+<resources>
+
+    <style name="AppTheme" parent="@android:Theme.Holo.Light" />
+
+</resources>

res/values/strings.xml

 <resources>
 
     <string name="app_name">TreasureHunter</string>
-    <string name="hello_world">Hello world!</string>
     <string name="menu_settings">Settings</string>
-    <string name="title_activity_main">Treasure Hunter</string>
+    <string name="title_activity_main">Local Treasure</string>
+    <string name="geolock_tv_title">Discovering your current location</string>
+    <string name="main_but_startHunt">Start a new treasure expedition!</string>
+    <string name="distance_tv_title">Set your distance!</string>
+    <string name="distance_but_500m">500m</string>
+    <string name="distance_et_custom">Enter custom distance</string>
+    <string name="distance_but_custom">Custom</string>
 
 </resources>

src/nctuw/littledot/localtreasure/Geodesy.java

 		double radLat = Math.toRadians(curLoc.getLatitude());
 		double radBearing = Math.toRadians(bearing);
 
-		double earthRadius = 6371.009; //km
+		double earthRadius = 6371009; //meter
 		double angle = distance / earthRadius;
 
 		double destLat = Math.asin(Math.sin(radLat) * Math.cos(angle)

src/nctuw/littledot/localtreasure/HuntingActivity.java

 import nctuw.littledot.util.Echo;
 import nctuw.littledot.util.Leg;
 import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.location.Criteria;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.os.Bundle;
 import android.text.format.Time;
+import android.view.LayoutInflater;
 import android.view.Menu;
-import android.view.View;
 import android.widget.TextView;
 
 public class HuntingActivity extends Activity implements LocationListener {
-	private LocationManager	mLM;
+	public static final long	FIVE_MIN							= 5 * 60 * 1000;
+	public static final int		LOCATION_LOCK_DIALOG	= 1;
 
-	private Geodesy					mGeo;
-	private Location				mCurLocation;
-	private Location				mDestLocation;
+	private LayoutInflater		mLI;
 
-	private TextView				tvCurLoc;
-	private TextView				tvDestLoc;
-	private TextView				tvDistance;
+	private LocationManager		mLM;
+	private Criteria					mCriteria;
 
-	private int							mDistance;
+	private double						mBearing;
+	private Location					mCurLocation;
+	private Location					mDestLocation;
+	private boolean						mRecalibrate					= true;
+
+	private TextView					tvCurLoc;
+	private TextView					tvDestLoc;
+	private TextView					tvDistance;
+	private ProgressDialog		pdLock;
+
+	private int								mDistance;
+
+	/*
+	 * Base class Activity
+	 */
 
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		setContentView(R.layout.hunting_act_layout);
 
-		tvDestLoc = (TextView) findViewById(R.id.hunt_tv_cur_location);
-		tvCurLoc = (TextView) findViewById(R.id.hunt_tv_dest_location);
+		mLI = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+		tvCurLoc = (TextView) findViewById(R.id.hunt_tv_cur_location);
+		tvDestLoc = (TextView) findViewById(R.id.hunt_tv_dest_location);
 		tvDistance = (TextView) findViewById(R.id.hunt_tv_distance);
 
 		mLM = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+		mCriteria = new Criteria();
+		mCriteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
 
 		Bundle extras = getIntent().getExtras();
 		mDistance = extras.getInt(SelectDistanceActivity.BUNDLE_DISTANCE, 0);
 
-		//test passive providers
+		//get a fresh start, only concerns are time & accuracy
 		Time time = new Time();
 		time.setToNow();
 		long now = time.toMillis(false);
 
 		Location location = mLM.getLastKnownLocation(LocationManager.GPS_PROVIDER);
-		Echo.d(this, "delta=" + (now - location.getTime()) + " passiveGPS="
-				+ location.toString());
+
+		//time freshness must be within 5 minutes & accuracy must < 100m
+		if (location != null && now - location.getTime() < FIVE_MIN
+				&& location.getAccuracy() < 100) {
+			mCurLocation = location;
+			Echo.d(this, "gps=\n" + location);
+		}
 		location = mLM.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
-		Echo.d(this, "delta=" + (now - location.getTime()) + " passiveNetwork="
-				+ location.toString());
+		if (location != null
+				&& ((mCurLocation != null
+						&& location.getTime() > mCurLocation.getTime() && location
+						.getAccuracy() < 100) || (mCurLocation == null
+						&& now - location.getTime() < FIVE_MIN && location.getAccuracy() < 100))) {
+			mCurLocation = location;
+			Echo.d(this, "network=\n" + location);
+		}
+
+		location = mLM.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
+		if (location != null
+				&& ((mCurLocation != null
+						&& location.getTime() > mCurLocation.getTime() && location
+						.getAccuracy() < 100) || (mCurLocation == null
+						&& now - location.getTime() < FIVE_MIN && location.getAccuracy() < 100))) {
+			mCurLocation = location;
+			Echo.d(this, "passive=\n" + location);
+		}
 
+		//randomize direction, calculate destination
+		mBearing = Math.random() * 360;
+		if (mCurLocation != null) {
+			mDestLocation = Geodesy.calculateVincentyDestination(mCurLocation,
+					mBearing, mDistance);
+			tvCurLoc.setText("===curLoc===\n" + mCurLocation.toString());
+			tvDestLoc.setText("===destLoc===\n" + mDestLocation.toString());
+			tvDistance.setText("===dist===" + mCurLocation.distanceTo(mDestLocation));
+		} else {
+			//display wait dialog
+			showDialog(LOCATION_LOCK_DIALOG);
+		}
 	}
 
 	@Override
 	protected void onResume() {
 		super.onResume();
 		Leg.d("");
-		mLM.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
-		mLM.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
-		mLM.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
+		mLM.requestLocationUpdates(0, 0, mCriteria, this, null);
 	}
 
 	@Override
 	protected void onPause() {
 		super.onPause();
+		Leg.d("");
 		mLM.removeUpdates(this);
 	}
 
 		return true;
 	}
 
-	public void onGetDestination(View v) {
-		if (mCurLocation != null) {
-			mDestLocation = Geodesy.calculateVincentyDestination(mCurLocation, 90,
-					1000);
-			tvDestLoc.setText("===DestLoc===" + mDestLocation.toString());
-			tvDistance
-					.setText(Double.toString(mDestLocation.distanceTo(mCurLocation)));
+	@Override
+	protected Dialog onCreateDialog(int id, Bundle args) {
+
+		if (id == LOCATION_LOCK_DIALOG) {
+			pdLock = new ProgressDialog(this);
+			pdLock.setTitle("Calibrating device");
+			pdLock.setMessage("Retrieving current location...");
+			pdLock.setCanceledOnTouchOutside(false);
+			pdLock.setOnCancelListener(new OnCancelListener() {
+				@Override
+				public void onCancel(DialogInterface dialog) {
+					finish();
+				}
+			});
+			return pdLock;
+
 		}
+
+		return super.onCreateDialog(id, args);
 	}
 
 	/*
 
 	@Override
 	public void onLocationChanged(Location location) {
-		tvCurLoc.setText("===curLoc===" + location.toString());
+		Leg.d("location=" + location.toString());
+		if (location.getAccuracy() > 100) {
+			Leg.d("Discarded inaccurate location=" + location.getAccuracy());
+			return;
+		}
 
 		mCurLocation = location;
 
-		if (mDestLocation != null) {
-			double d = mDestLocation.distanceTo(location);
-			tvDistance.setText(Double.toString(d));
-		}
+		tvCurLoc.setText("===curLoc===\n" + location.toString());
 
-		if (location.getProvider() == LocationManager.NETWORK_PROVIDER) {
-			TextView tvNetwork = (TextView) findViewById(R.id.hunt_tv_network);
-			String str = "===network===" + location.toString();
-			tvNetwork.setText(str);
-			Echo.d(this, str);
-		} else if (location.getProvider() == LocationManager.PASSIVE_PROVIDER) {
+		//always recalibrate on first gps lock
+		if (mDestLocation == null || mRecalibrate) {
+			mDestLocation = Geodesy.calculateVincentyDestination(location, mBearing,
+					mDistance);
+			tvDestLoc.setText("===destLoc===\n" + mDestLocation.toString());
+			mRecalibrate = false;
 
+			if (pdLock.isShowing()) {
+				pdLock.dismiss();
+			}
 		}
+
+		double d = mDestLocation.distanceTo(location);
+		tvDistance.setText("===dist===" + mCurLocation.distanceTo(mDestLocation));
 	}
 
 	@Override

src/nctuw/littledot/localtreasure/SplashActivity.java

 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
-import android.widget.ImageView;
+import android.widget.TextView;
 
 public class SplashActivity extends Activity {
-	public static int	SPLASH_PAUSE	= 0;
+	public static int	SPLASH_PAUSE	= 2000;
 
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		setContentView(R.layout.splash_act_layout);
 
-		ImageView logo = (ImageView) findViewById(R.id.splash_iv_logo);
+		TextView logo = (TextView) findViewById(R.id.splash_tv_appName);
 		logo.postDelayed(new Runnable() {
 
 			@Override

src/org/gavaghan/geodesy/GeodeticCalculator.java

  * 
  * @author Mike Gavaghan
  */
-public class GeodeticCalculator
-{
-   private final double TwoPi = 2.0 * Math.PI;
-
-   /**
-    * Calculate the destination and final bearing after traveling a specified
-    * distance, and a specified starting bearing, for an initial location. This
-    * is the solution to the direct geodetic problem.
-    * 
-    * @param ellipsoid reference ellipsoid to use
-    * @param start starting location
-    * @param startBearing starting bearing (degrees)
-    * @param distance distance to travel (meters)
-    * @param endBearing bearing at destination (degrees) element at index 0 will
-    *            be populated with the result
-    * @return
-    */
-   public GlobalCoordinates calculateEndingGlobalCoordinates(Ellipsoid ellipsoid, GlobalCoordinates start, double startBearing, double distance,
-         double[] endBearing)
-   {
-      double a = ellipsoid.getSemiMajorAxis();
-      double b = ellipsoid.getSemiMinorAxis();
-      double aSquared = a * a;
-      double bSquared = b * b;
-      double f = ellipsoid.getFlattening();
-      double phi1 = Angle.toRadians(start.getLatitude());
-      double alpha1 = Angle.toRadians(startBearing);
-      double cosAlpha1 = Math.cos(alpha1);
-      double sinAlpha1 = Math.sin(alpha1);
-      double s = distance;
-      double tanU1 = (1.0 - f) * Math.tan(phi1);
-      double cosU1 = 1.0 / Math.sqrt(1.0 + tanU1 * tanU1);
-      double sinU1 = tanU1 * cosU1;
-
-      // eq. 1
-      double sigma1 = Math.atan2(tanU1, cosAlpha1);
-
-      // eq. 2
-      double sinAlpha = cosU1 * sinAlpha1;
-
-      double sin2Alpha = sinAlpha * sinAlpha;
-      double cos2Alpha = 1 - sin2Alpha;
-      double uSquared = cos2Alpha * (aSquared - bSquared) / bSquared;
-
-      // eq. 3
-      double A = 1 + (uSquared / 16384) * (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared)));
-
-      // eq. 4
-      double B = (uSquared / 1024) * (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared)));
-
-      // iterate until there is a negligible change in sigma
-      double deltaSigma;
-      double sOverbA = s / (b * A);
-      double sigma = sOverbA;
-      double sinSigma;
-      double prevSigma = sOverbA;
-      double sigmaM2;
-      double cosSigmaM2;
-      double cos2SigmaM2;
-
-      for (;;)
-      {
-         // eq. 5
-         sigmaM2 = 2.0 * sigma1 + sigma;
-         cosSigmaM2 = Math.cos(sigmaM2);
-         cos2SigmaM2 = cosSigmaM2 * cosSigmaM2;
-         sinSigma = Math.sin(sigma);
-         double cosSignma = Math.cos(sigma);
-
-         // eq. 6
-         deltaSigma = B
-               * sinSigma
-               * (cosSigmaM2 + (B / 4.0)
-                     * (cosSignma * (-1 + 2 * cos2SigmaM2) - (B / 6.0) * cosSigmaM2 * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM2)));
-
-         // eq. 7
-         sigma = sOverbA + deltaSigma;
-
-         // break after converging to tolerance
-         if (Math.abs(sigma - prevSigma) < 0.0000000000001) break;
-
-         prevSigma = sigma;
-      }
-
-      sigmaM2 = 2.0 * sigma1 + sigma;
-      cosSigmaM2 = Math.cos(sigmaM2);
-      cos2SigmaM2 = cosSigmaM2 * cosSigmaM2;
-
-      double cosSigma = Math.cos(sigma);
-      sinSigma = Math.sin(sigma);
-
-      // eq. 8
-      double phi2 = Math.atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1.0 - f)
-            * Math.sqrt(sin2Alpha + Math.pow(sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1, 2.0)));
-
-      // eq. 9
-      // This fixes the pole crossing defect spotted by Matt Feemster. When a
-      // path passes a pole and essentially crosses a line of latitude twice -
-      // once in each direction - the longitude calculation got messed up. Using
-      // atan2 instead of atan fixes the defect. The change is in the next 3
-      // lines.
-      // double tanLambda = sinSigma * sinAlpha1 / (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1);
-      // double lambda = Math.atan(tanLambda);
-      double lambda = Math.atan2(sinSigma * sinAlpha1, (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1));
-
-      // eq. 10
-      double C = (f / 16) * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha));
-
-      // eq. 11
-      double L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)));
-
-      // eq. 12
-      double alpha2 = Math.atan2(sinAlpha, -sinU1 * sinSigma + cosU1 * cosSigma * cosAlpha1);
-
-      // build result
-      double latitude = Angle.toDegrees(phi2);
-      double longitude = start.getLongitude() + Angle.toDegrees(L);
-
-      if ((endBearing != null) && (endBearing.length > 0))
-      {
-         endBearing[0] = Angle.toDegrees(alpha2);
-      }
-
-      return new GlobalCoordinates(latitude, longitude);
-   }
-
-   /**
-    * Calculate the destination after traveling a specified distance, and a
-    * specified starting bearing, for an initial location. This is the solution
-    * to the direct geodetic problem.
-    * 
-    * @param ellipsoid reference ellipsoid to use
-    * @param start starting location
-    * @param startBearing starting bearing (degrees)
-    * @param distance distance to travel (meters)
-    * @return
-    */
-   public GlobalCoordinates calculateEndingGlobalCoordinates(Ellipsoid ellipsoid, GlobalCoordinates start, double startBearing, double distance)
-   {
-      return calculateEndingGlobalCoordinates(ellipsoid, start, startBearing, distance, null);
-   }
-
-   /**
-    * Calculate the geodetic curve between two points on a specified reference
-    * ellipsoid. This is the solution to the inverse geodetic problem.
-    * 
-    * @param ellipsoid reference ellipsoid to use
-    * @param start starting coordinates
-    * @param end ending coordinates
-    * @return
-    */
-   public GeodeticCurve calculateGeodeticCurve(Ellipsoid ellipsoid, GlobalCoordinates start, GlobalCoordinates end)
-   {
-      //
-      // All equation numbers refer back to Vincenty's publication:
-      // See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
-      //
-
-      // get constants
-      double a = ellipsoid.getSemiMajorAxis();
-      double b = ellipsoid.getSemiMinorAxis();
-      double f = ellipsoid.getFlattening();
-
-      // get parameters as radians
-      double phi1 = Angle.toRadians(start.getLatitude());
-      double lambda1 = Angle.toRadians(start.getLongitude());
-      double phi2 = Angle.toRadians(end.getLatitude());
-      double lambda2 = Angle.toRadians(end.getLongitude());
-
-      // calculations
-      double a2 = a * a;
-      double b2 = b * b;
-      double a2b2b2 = (a2 - b2) / b2;
-
-      double omega = lambda2 - lambda1;
-
-      double tanphi1 = Math.tan(phi1);
-      double tanU1 = (1.0 - f) * tanphi1;
-      double U1 = Math.atan(tanU1);
-      double sinU1 = Math.sin(U1);
-      double cosU1 = Math.cos(U1);
-
-      double tanphi2 = Math.tan(phi2);
-      double tanU2 = (1.0 - f) * tanphi2;
-      double U2 = Math.atan(tanU2);
-      double sinU2 = Math.sin(U2);
-      double cosU2 = Math.cos(U2);
-
-      double sinU1sinU2 = sinU1 * sinU2;
-      double cosU1sinU2 = cosU1 * sinU2;
-      double sinU1cosU2 = sinU1 * cosU2;
-      double cosU1cosU2 = cosU1 * cosU2;
-
-      // eq. 13
-      double lambda = omega;
-
-      // intermediates we'll need to compute 's'
-      double A = 0.0;
-      double B = 0.0;
-      double sigma = 0.0;
-      double deltasigma = 0.0;
-      double lambda0;
-      boolean converged = false;
-
-      for (int i = 0; i < 20; i++)
-      {
-         lambda0 = lambda;
-
-         double sinlambda = Math.sin(lambda);
-         double coslambda = Math.cos(lambda);
-
-         // eq. 14
-         double sin2sigma = (cosU2 * sinlambda * cosU2 * sinlambda) + (cosU1sinU2 - sinU1cosU2 * coslambda) * (cosU1sinU2 - sinU1cosU2 * coslambda);
-         double sinsigma = Math.sqrt(sin2sigma);
-
-         // eq. 15
-         double cossigma = sinU1sinU2 + (cosU1cosU2 * coslambda);
-
-         // eq. 16
-         sigma = Math.atan2(sinsigma, cossigma);
-
-         // eq. 17 Careful! sin2sigma might be almost 0!
-         double sinalpha = (sin2sigma == 0) ? 0.0 : cosU1cosU2 * sinlambda / sinsigma;
-         double alpha = Math.asin(sinalpha);
-         double cosalpha = Math.cos(alpha);
-         double cos2alpha = cosalpha * cosalpha;
-
-         // eq. 18 Careful! cos2alpha might be almost 0!
-         double cos2sigmam = cos2alpha == 0.0 ? 0.0 : cossigma - 2 * sinU1sinU2 / cos2alpha;
-         double u2 = cos2alpha * a2b2b2;
-
-         double cos2sigmam2 = cos2sigmam * cos2sigmam;
-
-         // eq. 3
-         A = 1.0 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)));
-
-         // eq. 4
-         B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)));
-
-         // eq. 6
-         deltasigma = B * sinsigma
-               * (cos2sigmam + B / 4 * (cossigma * (-1 + 2 * cos2sigmam2) - B / 6 * cos2sigmam * (-3 + 4 * sin2sigma) * (-3 + 4 * cos2sigmam2)));
-
-         // eq. 10
-         double C = f / 16 * cos2alpha * (4 + f * (4 - 3 * cos2alpha));
-
-         // eq. 11 (modified)
-         lambda = omega + (1 - C) * f * sinalpha * (sigma + C * sinsigma * (cos2sigmam + C * cossigma * (-1 + 2 * cos2sigmam2)));
-
-         // see how much improvement we got
-         double change = Math.abs((lambda - lambda0) / lambda);
-
-         if ((i > 1) && (change < 0.0000000000001))
-         {
-            converged = true;
-            break;
-         }
-      }
-
-      // eq. 19
-      double s = b * A * (sigma - deltasigma);
-      double alpha1;
-      double alpha2;
-
-      // didn't converge? must be N/S
-      if (!converged)
-      {
-         if (phi1 > phi2)
-         {
-            alpha1 = 180.0;
-            alpha2 = 0.0;
-         }
-         else if (phi1 < phi2)
-         {
-            alpha1 = 0.0;
-            alpha2 = 180.0;
-         }
-         else
-         {
-            alpha1 = Double.NaN;
-            alpha2 = Double.NaN;
-         }
-      }
-
-      // else, it converged, so do the math
-      else
-      {
-         double radians;
-
-         // eq. 20
-         radians = Math.atan2(cosU2 * Math.sin(lambda), (cosU1sinU2 - sinU1cosU2 * Math.cos(lambda)));
-         if (radians < 0.0) radians += TwoPi;
-         alpha1 = Angle.toDegrees(radians);
-
-         // eq. 21
-         radians = Math.atan2(cosU1 * Math.sin(lambda), (-sinU1cosU2 + cosU1sinU2 * Math.cos(lambda))) + Math.PI;
-         if (radians < 0.0) radians += TwoPi;
-         alpha2 = Angle.toDegrees(radians);
-      }
-
-      if (alpha1 >= 360.0) alpha1 -= 360.0;
-      if (alpha2 >= 360.0) alpha2 -= 360.0;
-
-      return new GeodeticCurve(s, alpha1, alpha2);
-   }
-
-   /**
-    * <p>
-    * Calculate the three dimensional geodetic measurement between two positions
-    * measured in reference to a specified ellipsoid.
-    * </p>
-    * <p>
-    * This calculation is performed by first computing a new ellipsoid by
-    * expanding or contracting the reference ellipsoid such that the new
-    * ellipsoid passes through the average elevation of the two positions. A
-    * geodetic curve across the new ellisoid is calculated. The point-to-point
-    * distance is calculated as the hypotenuse of a right triangle where the
-    * length of one side is the ellipsoidal distance and the other is the
-    * difference in elevation.
-    * </p>
-    * 
-    * @param refEllipsoid reference ellipsoid to use
-    * @param start starting position
-    * @param end ending position
-    * @return
-    */
-   public GeodeticMeasurement calculateGeodeticMeasurement(Ellipsoid refEllipsoid, GlobalPosition start, GlobalPosition end)
-   {
-      // calculate elevation differences
-      double elev1 = start.getElevation();
-      double elev2 = end.getElevation();
-      double elev12 = (elev1 + elev2) / 2.0;
-
-      // calculate latitude differences
-      double phi1 = Angle.toRadians(start.getLatitude());
-      double phi2 = Angle.toRadians(end.getLatitude());
-      double phi12 = (phi1 + phi2) / 2.0;
-
-      // calculate a new ellipsoid to accommodate average elevation
-      double refA = refEllipsoid.getSemiMajorAxis();
-      double f = refEllipsoid.getFlattening();
-      double a = refA + elev12 * (1.0 + f * Math.sin(phi12));
-      Ellipsoid ellipsoid = Ellipsoid.fromAAndF(a, f);
-
-      // calculate the curve at the average elevation
-      GeodeticCurve averageCurve = calculateGeodeticCurve(ellipsoid, start, end);
-
-      // return the measurement
-      return new GeodeticMeasurement(averageCurve, elev2 - elev1);
-   }
+public class GeodeticCalculator {
+	private final double	TwoPi	= 2.0 * Math.PI;
+
+	/**
+	 * Calculate the destination and final bearing after traveling a specified
+	 * distance, and a specified starting bearing, for an initial location. This
+	 * is the solution to the direct geodetic problem.
+	 * 
+	 * @param ellipsoid
+	 *          reference ellipsoid to use
+	 * @param start
+	 *          starting location
+	 * @param startBearing
+	 *          starting bearing (degrees)
+	 * @param distance
+	 *          distance to travel (meters)
+	 * @param endBearing
+	 *          bearing at destination (degrees) element at index 0 will be
+	 *          populated with the result
+	 * @return
+	 */
+	public GlobalCoordinates calculateEndingGlobalCoordinates(
+			Ellipsoid ellipsoid, GlobalCoordinates start, double startBearing,
+			double distance, double[] endBearing) {
+		double a = ellipsoid.getSemiMajorAxis();
+		double b = ellipsoid.getSemiMinorAxis();
+		double aSquared = a * a;
+		double bSquared = b * b;
+		double f = ellipsoid.getFlattening();
+		double phi1 = Angle.toRadians(start.getLatitude());
+		double alpha1 = Angle.toRadians(startBearing);
+		double cosAlpha1 = Math.cos(alpha1);
+		double sinAlpha1 = Math.sin(alpha1);
+		double s = distance;
+		double tanU1 = (1.0 - f) * Math.tan(phi1);
+		double cosU1 = 1.0 / Math.sqrt(1.0 + tanU1 * tanU1);
+		double sinU1 = tanU1 * cosU1;
+
+		// eq. 1
+		double sigma1 = Math.atan2(tanU1, cosAlpha1);
+
+		// eq. 2
+		double sinAlpha = cosU1 * sinAlpha1;
+
+		double sin2Alpha = sinAlpha * sinAlpha;
+		double cos2Alpha = 1 - sin2Alpha;
+		double uSquared = cos2Alpha * (aSquared - bSquared) / bSquared;
+
+		// eq. 3
+		double A = 1 + (uSquared / 16384)
+				* (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared)));
+
+		// eq. 4
+		double B = (uSquared / 1024)
+				* (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared)));
+
+		// iterate until there is a negligible change in sigma
+		double deltaSigma;
+		double sOverbA = s / (b * A);
+		double sigma = sOverbA;
+		double sinSigma;
+		double prevSigma = sOverbA;
+		double sigmaM2;
+		double cosSigmaM2;
+		double cos2SigmaM2;
+
+		for (;;) {
+			// eq. 5
+			sigmaM2 = 2.0 * sigma1 + sigma;
+			cosSigmaM2 = Math.cos(sigmaM2);
+			cos2SigmaM2 = cosSigmaM2 * cosSigmaM2;
+			sinSigma = Math.sin(sigma);
+			double cosSignma = Math.cos(sigma);
+
+			// eq. 6
+			deltaSigma = B
+					* sinSigma
+					* (cosSigmaM2 + (B / 4.0)
+							* (cosSignma * (-1 + 2 * cos2SigmaM2) - (B / 6.0) * cosSigmaM2
+									* (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM2)));
+
+			// eq. 7
+			sigma = sOverbA + deltaSigma;
+
+			// break after converging to tolerance
+			if (Math.abs(sigma - prevSigma) < 0.0000000000001)
+				break;
+
+			prevSigma = sigma;
+		}
+
+		sigmaM2 = 2.0 * sigma1 + sigma;
+		cosSigmaM2 = Math.cos(sigmaM2);
+		cos2SigmaM2 = cosSigmaM2 * cosSigmaM2;
+
+		double cosSigma = Math.cos(sigma);
+		sinSigma = Math.sin(sigma);
+
+		// eq. 8
+		double phi2 = Math
+				.atan2(
+						sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1,
+						(1.0 - f)
+								* Math.sqrt(sin2Alpha
+										+ Math.pow(sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1,
+												2.0)));
+
+		// eq. 9
+		// This fixes the pole crossing defect spotted by Matt Feemster. When a
+		// path passes a pole and essentially crosses a line of latitude twice -
+		// once in each direction - the longitude calculation got messed up. Using
+		// atan2 instead of atan fixes the defect. The change is in the next 3
+		// lines.
+		// double tanLambda = sinSigma * sinAlpha1 / (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1);
+		// double lambda = Math.atan(tanLambda);
+		double lambda = Math.atan2(sinSigma * sinAlpha1, (cosU1 * cosSigma - sinU1
+				* sinSigma * cosAlpha1));
+
+		// eq. 10
+		double C = (f / 16) * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha));
+
+		// eq. 11
+		double L = lambda
+				- (1 - C)
+				* f
+				* sinAlpha
+				* (sigma + C * sinSigma
+						* (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)));
+
+		// eq. 12
+		double alpha2 = Math.atan2(sinAlpha, -sinU1 * sinSigma + cosU1 * cosSigma
+				* cosAlpha1);
+
+		// build result
+		double latitude = Angle.toDegrees(phi2);
+		double longitude = start.getLongitude() + Angle.toDegrees(L);
+
+		if ((endBearing != null) && (endBearing.length > 0)) {
+			endBearing[0] = Angle.toDegrees(alpha2);
+		}
+
+		return new GlobalCoordinates(latitude, longitude);
+	}
+
+	/**
+	 * Calculate the destination after traveling a specified distance, and a
+	 * specified starting bearing, for an initial location. This is the solution
+	 * to the direct geodetic problem.
+	 * 
+	 * @param ellipsoid
+	 *          reference ellipsoid to use
+	 * @param start
+	 *          starting location
+	 * @param startBearing
+	 *          starting bearing (degrees)
+	 * @param distance
+	 *          distance to travel (meters)
+	 * @return
+	 */
+	public GlobalCoordinates calculateEndingGlobalCoordinates(
+			Ellipsoid ellipsoid, GlobalCoordinates start, double startBearing,
+			double distance) {
+		return calculateEndingGlobalCoordinates(ellipsoid, start, startBearing,
+				distance, null);
+	}
+
+	/**
+	 * Calculate the geodetic curve between two points on a specified reference
+	 * ellipsoid. This is the solution to the inverse geodetic problem.
+	 * 
+	 * @param ellipsoid
+	 *          reference ellipsoid to use
+	 * @param start
+	 *          starting coordinates
+	 * @param end
+	 *          ending coordinates
+	 * @return
+	 */
+	public GeodeticCurve calculateGeodeticCurve(Ellipsoid ellipsoid,
+			GlobalCoordinates start, GlobalCoordinates end) {
+		//
+		// All equation numbers refer back to Vincenty's publication:
+		// See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
+		//
+
+		// get constants
+		double a = ellipsoid.getSemiMajorAxis();
+		double b = ellipsoid.getSemiMinorAxis();
+		double f = ellipsoid.getFlattening();
+
+		// get parameters as radians
+		double phi1 = Angle.toRadians(start.getLatitude());
+		double lambda1 = Angle.toRadians(start.getLongitude());
+		double phi2 = Angle.toRadians(end.getLatitude());
+		double lambda2 = Angle.toRadians(end.getLongitude());
+
+		// calculations
+		double a2 = a * a;
+		double b2 = b * b;
+		double a2b2b2 = (a2 - b2) / b2;
+
+		double omega = lambda2 - lambda1;
+
+		double tanphi1 = Math.tan(phi1);
+		double tanU1 = (1.0 - f) * tanphi1;
+		double U1 = Math.atan(tanU1);
+		double sinU1 = Math.sin(U1);
+		double cosU1 = Math.cos(U1);
+
+		double tanphi2 = Math.tan(phi2);
+		double tanU2 = (1.0 - f) * tanphi2;
+		double U2 = Math.atan(tanU2);
+		double sinU2 = Math.sin(U2);
+		double cosU2 = Math.cos(U2);
+
+		double sinU1sinU2 = sinU1 * sinU2;
+		double cosU1sinU2 = cosU1 * sinU2;
+		double sinU1cosU2 = sinU1 * cosU2;
+		double cosU1cosU2 = cosU1 * cosU2;
+
+		// eq. 13
+		double lambda = omega;
+
+		// intermediates we'll need to compute 's'
+		double A = 0.0;
+		double B = 0.0;
+		double sigma = 0.0;
+		double deltasigma = 0.0;
+		double lambda0;
+		boolean converged = false;
+
+		for (int i = 0; i < 20; i++) {
+			lambda0 = lambda;
+
+			double sinlambda = Math.sin(lambda);
+			double coslambda = Math.cos(lambda);
+
+			// eq. 14
+			double sin2sigma = (cosU2 * sinlambda * cosU2 * sinlambda)
+					+ (cosU1sinU2 - sinU1cosU2 * coslambda)
+					* (cosU1sinU2 - sinU1cosU2 * coslambda);
+			double sinsigma = Math.sqrt(sin2sigma);
+
+			// eq. 15
+			double cossigma = sinU1sinU2 + (cosU1cosU2 * coslambda);
+
+			// eq. 16
+			sigma = Math.atan2(sinsigma, cossigma);
+
+			// eq. 17 Careful! sin2sigma might be almost 0!
+			double sinalpha = (sin2sigma == 0) ? 0.0 : cosU1cosU2 * sinlambda
+					/ sinsigma;
+			double alpha = Math.asin(sinalpha);
+			double cosalpha = Math.cos(alpha);
+			double cos2alpha = cosalpha * cosalpha;
+
+			// eq. 18 Careful! cos2alpha might be almost 0!
+			double cos2sigmam = cos2alpha == 0.0 ? 0.0 : cossigma - 2 * sinU1sinU2
+					/ cos2alpha;
+			double u2 = cos2alpha * a2b2b2;
+
+			double cos2sigmam2 = cos2sigmam * cos2sigmam;
+
+			// eq. 3
+			A = 1.0 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)));
+
+			// eq. 4
+			B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)));
+
+			// eq. 6
+			deltasigma = B
+					* sinsigma
+					* (cos2sigmam + B
+							/ 4
+							* (cossigma * (-1 + 2 * cos2sigmam2) - B / 6 * cos2sigmam
+									* (-3 + 4 * sin2sigma) * (-3 + 4 * cos2sigmam2)));
+
+			// eq. 10
+			double C = f / 16 * cos2alpha * (4 + f * (4 - 3 * cos2alpha));
+
+			// eq. 11 (modified)
+			lambda = omega
+					+ (1 - C)
+					* f
+					* sinalpha
+					* (sigma + C * sinsigma
+							* (cos2sigmam + C * cossigma * (-1 + 2 * cos2sigmam2)));
+
+			// see how much improvement we got
+			double change = Math.abs((lambda - lambda0) / lambda);
+
+			if ((i > 1) && (change < 0.0000000000001)) {
+				converged = true;
+				break;
+			}
+		}
+
+		// eq. 19
+		double s = b * A * (sigma - deltasigma);
+		double alpha1;
+		double alpha2;
+
+		// didn't converge? must be N/S
+		if (!converged) {
+			if (phi1 > phi2) {
+				alpha1 = 180.0;
+				alpha2 = 0.0;
+			} else if (phi1 < phi2) {
+				alpha1 = 0.0;
+				alpha2 = 180.0;
+			} else {
+				alpha1 = Double.NaN;
+				alpha2 = Double.NaN;
+			}
+		}
+
+		// else, it converged, so do the math
+		else {
+			double radians;
+
+			// eq. 20
+			radians = Math.atan2(cosU2 * Math.sin(lambda), (cosU1sinU2 - sinU1cosU2
+					* Math.cos(lambda)));
+			if (radians < 0.0)
+				radians += TwoPi;
+			alpha1 = Angle.toDegrees(radians);
+
+			// eq. 21
+			radians = Math.atan2(cosU1 * Math.sin(lambda), (-sinU1cosU2 + cosU1sinU2
+					* Math.cos(lambda)))
+					+ Math.PI;
+			if (radians < 0.0)
+				radians += TwoPi;
+			alpha2 = Angle.toDegrees(radians);
+		}
+
+		if (alpha1 >= 360.0)
+			alpha1 -= 360.0;
+		if (alpha2 >= 360.0)
+			alpha2 -= 360.0;
+
+		return new GeodeticCurve(s, alpha1, alpha2);
+	}
+
+	/**
+	 * <p>
+	 * Calculate the three dimensional geodetic measurement between two positions
+	 * measured in reference to a specified ellipsoid.
+	 * </p>
+	 * <p>
+	 * This calculation is performed by first computing a new ellipsoid by
+	 * expanding or contracting the reference ellipsoid such that the new
+	 * ellipsoid passes through the average elevation of the two positions. A
+	 * geodetic curve across the new ellisoid is calculated. The point-to-point
+	 * distance is calculated as the hypotenuse of a right triangle where the
+	 * length of one side is the ellipsoidal distance and the other is the
+	 * difference in elevation.
+	 * </p>
+	 * 
+	 * @param refEllipsoid
+	 *          reference ellipsoid to use
+	 * @param start
+	 *          starting position
+	 * @param end
+	 *          ending position
+	 * @return
+	 */
+	public GeodeticMeasurement calculateGeodeticMeasurement(
+			Ellipsoid refEllipsoid, GlobalPosition start, GlobalPosition end) {
+		// calculate elevation differences
+		double elev1 = start.getElevation();
+		double elev2 = end.getElevation();
+		double elev12 = (elev1 + elev2) / 2.0;
+
+		// calculate latitude differences
+		double phi1 = Angle.toRadians(start.getLatitude());
+		double phi2 = Angle.toRadians(end.getLatitude());
+		double phi12 = (phi1 + phi2) / 2.0;
+
+		// calculate a new ellipsoid to accommodate average elevation
+		double refA = refEllipsoid.getSemiMajorAxis();
+		double f = refEllipsoid.getFlattening();
+		double a = refA + elev12 * (1.0 + f * Math.sin(phi12));
+		Ellipsoid ellipsoid = Ellipsoid.fromAAndF(a, f);
+
+		// calculate the curve at the average elevation
+		GeodeticCurve averageCurve = calculateGeodeticCurve(ellipsoid, start, end);
+
+		// return the measurement
+		return new GeodeticMeasurement(averageCurve, elev2 - elev1);
+	}
 }