Commits

littledot5566 committed ae968f2

First commit\!

Comments (0)

Files changed (39)

AndroidManifest.xml

+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="nctuw.littledot.localtreasure"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk
+        android:minSdkVersion="10"
+        android:targetSdkVersion="15" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <application
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <activity
+            android:name=".SplashActivity"
+            android:label="@string/title_activity_main" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <!--  -->
+        <activity android:name=".HuntingActivity" >
+        </activity>
+        <!--  -->
+        <activity android:name=".MainActivity" >
+        </activity>
+        <!--  -->
+        <activity android:name=".SelectDistanceActivity" >
+        </activity>
+    </application>
+
+</manifest>

ic_launcher-web.png

Added
New image

libs/android-support-v4.jar

Binary file added.
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+</lint>

proguard-project.txt

+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

project.properties

+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=Google Inc.:Google APIs:10
+android.library.reference.1=../Utils

res/drawable-hdpi/ic_action_search.png

Added
New image

res/drawable-hdpi/ic_launcher.png

Added
New image

res/drawable-ldpi/ic_launcher.png

Added
New image

res/drawable-mdpi/ic_action_search.png

Added
New image

res/drawable-mdpi/ic_launcher.png

Added
New image

res/drawable-xhdpi/ic_action_search.png

Added
New image

res/drawable-xhdpi/ic_launcher.png

Added
New image

res/layout/hunting_act_layout.xml

+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    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"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/hunt_tv_network"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/hunt_tv_distance"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/hunt_tv_dest_location"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>

res/layout/main_act_layout.xml

+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/main_tv_info"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+
+    <Button
+        android:id="@+id/main_but_startHunt"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="onStartNewHunt"
+        android:text="Start a new treasure expedition!" />
+
+</LinearLayout>

res/layout/selectdistance_act_layout.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/distance_tv_title"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:text="Set your distance!" />
+
+    <Button
+        android:id="@+id/distance_but_500m"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:onClick="onChooseDistance"
+        android:text="500m" />
+
+    <LinearLayout
+        android:id="@+id/distance_ll_custom"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="horizontal" >
+
+        <EditText
+            android:id="@+id/distance_et_custom"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:hint="Enter custom distance"
+            android:inputType="number"
+            android:visibility="gone" />
+
+        <Button
+            android:id="@+id/distance_but_custom"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:onClick="onChooseCustomDistance"
+            android:text="Custom" />
+    </LinearLayout>
+
+</LinearLayout>

res/layout/splash_act_layout.xml

+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" >
+
+    <LinearLayout
+        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>
+
+</RelativeLayout>

res/menu/activity_main.xml

+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/menu_settings"
+        android:title="@string/menu_settings"
+        android:orderInCategory="100" />
+</menu>

res/values-large/dimens.xml

+<resources>
+
+    <dimen name="padding_small">8dp</dimen>
+    <dimen name="padding_medium">16dp</dimen>
+    <dimen name="padding_large">16dp</dimen>
+
+</resources>

res/values/dimens.xml

+<resources>
+
+    <dimen name="padding_small">8dp</dimen>
+    <dimen name="padding_medium">8dp</dimen>
+    <dimen name="padding_large">16dp</dimen>
+
+</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>
+
+</resources>

res/values/styles.xml

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

src/nctuw/littledot/localtreasure/DataManager.java

+package nctuw.littledot.localtreasure;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class DataManager {
+	private Context						mCtx;
+	private SharedPreferences	mPrefs;
+
+	public DataManager(Context ctx) {
+		mCtx = ctx;
+
+		mPrefs = mCtx.getSharedPreferences("treasure", 0);
+
+	}
+}

src/nctuw/littledot/localtreasure/DialogManager.java

+package nctuw.littledot.localtreasure;
+
+import android.app.Dialog;
+import android.content.Context;
+
+public class DialogManager {
+	public static final int	DIALOG_SELECT_DISTANCE	= 1;
+
+	public static Dialog getDialog(Context ctx, int id) {
+		Dialog dialog = null;
+
+		if (id == DIALOG_SELECT_DISTANCE) {
+			dialog = new Dialog(ctx);
+			//dialog.setContentView(R.layout.choosedistance_dia_layout);
+		}
+
+		return dialog;
+	}
+
+}

src/nctuw/littledot/localtreasure/GPSLocationListener.java

+package nctuw.littledot.localtreasure;
+
+import android.location.Location;
+import android.location.LocationListener;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+
+public class GPSLocationListener implements LocationListener {
+	public static int	MSG_LOCATION_CHANGED	= 1;
+	private Handler		mHandle;
+
+	public GPSLocationListener(Handler handle) {
+		mHandle = handle;
+	}
+
+	@Override
+	public void onLocationChanged(Location location) {
+		// TODO Auto-generated method stub
+		Message msg = mHandle.obtainMessage(MSG_LOCATION_CHANGED, location);
+		mHandle.sendMessage(msg);
+	}
+
+	@Override
+	public void onProviderDisabled(String provider) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void onProviderEnabled(String provider) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void onStatusChanged(String provider, int status, Bundle extras) {
+		// TODO Auto-generated method stub
+
+	}
+
+}

src/nctuw/littledot/localtreasure/Geodesy.java

+package nctuw.littledot.localtreasure;
+
+import org.gavaghan.geodesy.Ellipsoid;
+import org.gavaghan.geodesy.GeodeticCalculator;
+import org.gavaghan.geodesy.GlobalCoordinates;
+
+import android.location.Location;
+
+public class Geodesy {
+
+	public static Location calculateHaversineDestination(Location curLoc,
+			double bearing, double distance) {
+
+		double radLong = Math.toRadians(curLoc.getLongitude());
+		double radLat = Math.toRadians(curLoc.getLatitude());
+		double radBearing = Math.toRadians(bearing);
+
+		double earthRadius = 6371.009; //km
+		double angle = distance / earthRadius;
+
+		double destLat = Math.asin(Math.sin(radLat) * Math.cos(angle)
+				+ Math.cos(radLat) * Math.sin(angle) * Math.cos(radBearing));
+		double destLong = radLong
+				+ Math.atan2(Math.sin(radBearing) * Math.sin(angle) * Math.cos(radLat),
+						Math.cos(angle) - Math.sin(radLat) * Math.sin(radLat));
+
+		Location loc = new Location("TreasureHunter");
+		loc.setLatitude(destLat);
+		loc.setLongitude(destLong);
+		return loc;
+	}
+
+	public static Location calculateVincentyDestination(Location curLoc,
+			double bearing, double distance) {//meter
+		// instantiate the calculator
+		GeodeticCalculator geoCalc = new GeodeticCalculator();
+
+		// select a reference elllipsoid
+		Ellipsoid reference = Ellipsoid.WGS84;
+
+		// set Lincoln Memorial coordinates
+		GlobalCoordinates lincolnMemorial;
+		lincolnMemorial = new GlobalCoordinates(curLoc.getLatitude(),
+				curLoc.getLongitude());
+
+		// find the destination
+		double[] endBearing = new double[1];
+		GlobalCoordinates dest = geoCalc.calculateEndingGlobalCoordinates(
+				reference, lincolnMemorial, bearing, distance, endBearing);
+
+		Location destLoc = new Location("TreasureHunter");
+		destLoc.setLatitude(dest.getLatitude());
+		destLoc.setLongitude(dest.getLongitude());
+		return destLoc;
+	}
+
+}

src/nctuw/littledot/localtreasure/HuntingActivity.java

+package nctuw.littledot.localtreasure;
+
+import nctuw.littledot.util.Echo;
+import nctuw.littledot.util.Leg;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.text.format.Time;
+import android.view.Menu;
+import android.view.View;
+import android.widget.TextView;
+
+public class HuntingActivity extends Activity implements LocationListener {
+	private LocationManager	mLM;
+
+	private Geodesy					mGeo;
+	private Location				mCurLocation;
+	private Location				mDestLocation;
+
+	private TextView				tvCurLoc;
+	private TextView				tvDestLoc;
+	private TextView				tvDistance;
+
+	private int							mDistance;
+
+	@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);
+		tvDistance = (TextView) findViewById(R.id.hunt_tv_distance);
+
+		mLM = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+
+		Bundle extras = getIntent().getExtras();
+		mDistance = extras.getInt(SelectDistanceActivity.BUNDLE_DISTANCE, 0);
+
+		//test passive providers
+		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());
+		location = mLM.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+		Echo.d(this, "delta=" + (now - location.getTime()) + " passiveNetwork="
+				+ location.toString());
+
+	}
+
+	@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);
+	}
+
+	@Override
+	protected void onPause() {
+		super.onPause();
+		mLM.removeUpdates(this);
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.activity_main, menu);
+		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)));
+		}
+	}
+
+	/*
+	 * Interface LocationListener
+	 */
+
+	@Override
+	public void onLocationChanged(Location location) {
+		tvCurLoc.setText("===curLoc===" + location.toString());
+
+		mCurLocation = location;
+
+		if (mDestLocation != null) {
+			double d = mDestLocation.distanceTo(location);
+			tvDistance.setText(Double.toString(d));
+		}
+
+		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) {
+
+		}
+	}
+
+	@Override
+	public void onProviderDisabled(String provider) {
+	}
+
+	@Override
+	public void onProviderEnabled(String provider) {
+
+	}
+
+	@Override
+	public void onStatusChanged(String provider, int status, Bundle extras) {
+		Echo.d(this, "provider=" + provider + " status=" + status + " extras="
+				+ extras.toString());
+	}
+
+}

src/nctuw/littledot/localtreasure/MainActivity.java

+package nctuw.littledot.localtreasure;
+
+import nctuw.littledot.util.Leg;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.widget.TextView;
+
+public class MainActivity extends Activity {
+	private DataManager	mDataManager;
+
+	private TextView		tvInfo;
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.main_act_layout);
+
+		mDataManager = new DataManager(this);
+
+		tvInfo = (TextView) findViewById(R.id.main_tv_info);
+
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+
+		getMenuInflater().inflate(R.menu.activity_main, menu);
+		return true;
+	}
+
+	@Override
+	protected Dialog onCreateDialog(int id) {
+		Dialog dialog = null;
+		return dialog;
+	}
+
+	@Override
+	protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+		super.onPrepareDialog(id, dialog, args);
+		Leg.d("");
+	}
+
+	public void onStartNewHunt(View v) {
+		Intent intent = new Intent(this, SelectDistanceActivity.class);
+		startActivity(intent);
+	}
+
+}

src/nctuw/littledot/localtreasure/SelectDistanceActivity.java

+package nctuw.littledot.localtreasure;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+public class SelectDistanceActivity extends Activity {
+	public static String	BUNDLE_DISTANCE	= "bundle_distance";
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.selectdistance_act_layout);
+
+	}
+
+	public void onChooseDistance(View v) {
+		Intent intent = new Intent(this, HuntingActivity.class);
+
+		switch (v.getId()) {
+			case R.id.distance_but_500m:
+				intent.putExtra(BUNDLE_DISTANCE, 500);
+				break;
+		}
+
+		startActivity(intent);
+	}
+
+	public void onChooseCustomDistance(View v) {
+		EditText et = (EditText) findViewById(R.id.distance_et_custom);
+		Button but = (Button) findViewById(R.id.distance_but_custom);
+
+		if (et.getVisibility() == View.GONE) {
+			et.setVisibility(View.VISIBLE);
+			but.setText("OK!");
+		} else {
+			Intent intent = new Intent(this, HuntingActivity.class);
+			intent.putExtra(BUNDLE_DISTANCE,
+					Integer.parseInt(et.getText().toString()));
+			startActivity(intent);
+		}
+	}
+}

src/nctuw/littledot/localtreasure/SplashActivity.java

+package nctuw.littledot.localtreasure;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ImageView;
+
+public class SplashActivity extends Activity {
+	public static int	SPLASH_PAUSE	= 0;
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.splash_act_layout);
+
+		ImageView logo = (ImageView) findViewById(R.id.splash_iv_logo);
+		logo.postDelayed(new Runnable() {
+
+			@Override
+			public void run() {
+
+				Intent mainActivity = new Intent(SplashActivity.this,
+						MainActivity.class);
+				startActivity(mainActivity);
+				finish();
+			}
+		}, SPLASH_PAUSE);
+
+	}
+}

src/org/gavaghan/geodesy/Angle.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+/**
+ * Utility methods for dealing with angles.
+ *  
+ * @author Mike Gavaghan
+ */
+public class Angle
+{
+   /** Degrees/Radians conversion constant. */
+   static private final double PiOver180 = Math.PI / 180.0;
+   
+   /**
+    * Disallow instantiation.
+    */
+   private Angle()
+   {
+   }
+
+   /**
+    * Convert degrees to radians.
+    * @param degrees
+    * @return
+    */
+   static public double toRadians( double degrees )
+   {
+      return degrees * PiOver180;
+   }
+   
+   /**
+    * Convert radians to degrees.
+    * @param radians
+    * @return
+    */
+   static public double toDegrees( double radians )
+   {
+      return radians / PiOver180;
+   }
+}

src/org/gavaghan/geodesy/Ellipsoid.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+import java.io.Serializable;
+
+/**
+ * Encapsulation of an ellipsoid, and declaration of common reference ellipsoids.
+ * @author Mike Gavaghan
+ */
+public class Ellipsoid implements Serializable
+{
+   /** Semi major axis (meters). */
+   private final double mSemiMajorAxis;
+
+   /** Semi minor axis (meters). */
+   private final double mSemiMinorAxis;
+
+   /** Flattening. */
+   private final double mFlattening;
+
+   /** Inverse flattening. */
+   private final double mInverseFlattening;
+
+   /**
+    * Construct a new Ellipsoid.  This is private to ensure the values are
+    * consistent (flattening = 1.0 / inverseFlattening).  Use the methods 
+    * fromAAndInverseF() and fromAAndF() to create new instances.
+    * @param semiMajor
+    * @param semiMinor
+    * @param flattening
+    * @param inverseFlattening
+    */
+   private Ellipsoid(double semiMajor, double semiMinor, double flattening, double inverseFlattening)
+   {
+     mSemiMajorAxis = semiMajor;
+     mSemiMinorAxis = semiMinor;
+     mFlattening = flattening;
+     mInverseFlattening = inverseFlattening;
+   }
+
+   /** The WGS84 ellipsoid. */
+   static public final Ellipsoid WGS84 = fromAAndInverseF(6378137.0, 298.257223563);
+
+   /** The GRS80 ellipsoid. */
+   static public final Ellipsoid GRS80 = fromAAndInverseF(6378137.0, 298.257222101);
+
+   /** The GRS67 ellipsoid. */
+   static public final Ellipsoid GRS67 = fromAAndInverseF(6378160.0, 298.25);
+
+   /** The ANS ellipsoid. */
+   static public final Ellipsoid ANS = fromAAndInverseF(6378160.0, 298.25);
+
+   /** The WGS72 ellipsoid. */
+   static public final Ellipsoid WGS72 = fromAAndInverseF(6378135.0, 298.26);
+
+   /** The Clarke1858 ellipsoid. */
+   static public final Ellipsoid Clarke1858 = fromAAndInverseF(6378293.645, 294.26);
+
+   /** The Clarke1880 ellipsoid. */
+   static public final Ellipsoid Clarke1880 = fromAAndInverseF(6378249.145, 293.465);
+
+   /** A spherical "ellipsoid". */
+   static public final Ellipsoid Sphere = fromAAndF(6371000, 0.0);
+
+   /**
+    * Build an Ellipsoid from the semi major axis measurement and the inverse flattening.
+    * @param semiMajor semi major axis (meters)
+    * @param inverseFlattening
+    * @return
+    */
+   static public Ellipsoid fromAAndInverseF(double semiMajor, double inverseFlattening)
+   {
+     double f = 1.0 / inverseFlattening;
+     double b = (1.0 - f) * semiMajor;
+
+     return new Ellipsoid(semiMajor, b, f, inverseFlattening);
+   }
+
+   /**
+    * Build an Ellipsoid from the semi major axis measurement and the flattening.
+    * @param semiMajor semi major axis (meters)
+    * @param flattening
+    * @return
+    */
+   static public Ellipsoid fromAAndF(double semiMajor, double flattening)
+   {
+     double inverseF = 1.0 / flattening;
+     double b = (1.0 - flattening) * semiMajor;
+
+     return new Ellipsoid(semiMajor, b, flattening, inverseF);
+   }
+   
+   /**
+    * Get semi-major axis.
+    * @return semi-major axis (in meters).
+    */
+   public double getSemiMajorAxis()
+   {
+     return mSemiMajorAxis;
+   }
+
+   /**
+    * Get semi-minor axis.
+    * @return semi-minor axis (in meters).
+    */
+   public double getSemiMinorAxis()
+   {
+     return mSemiMinorAxis;
+   }
+
+   /**
+    * Get flattening
+    * @return
+    */
+   public double getFlattening()
+   {
+     return mFlattening;
+   }
+
+   /**
+    * Get inverse flattening.
+    * @return
+    */
+   public double getInverseFlattening()
+   {
+     return mInverseFlattening;
+   }
+}

src/org/gavaghan/geodesy/Example.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula-java/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+import org.gavaghan.geodesy.Ellipsoid;
+import org.gavaghan.geodesy.GeodeticCalculator;
+import org.gavaghan.geodesy.GeodeticCurve;
+import org.gavaghan.geodesy.GeodeticMeasurement;
+import org.gavaghan.geodesy.GlobalCoordinates;
+import org.gavaghan.geodesy.GlobalPosition;
+
+public class Example {
+	/**
+	 * Calculate the destination if we start at: Lincoln Memorial in Washington,
+	 * D.C --> 38.8892N, 77.04978W and travel at 51.7679 degrees for 6179.016136
+	 * kilometers
+	 * 
+	 * WGS84 reference ellipsoid
+	 */
+	static void TwoDimensionalDirectCalculation() {
+		// instantiate the calculator
+		GeodeticCalculator geoCalc = new GeodeticCalculator();
+
+		// select a reference elllipsoid
+		Ellipsoid reference = Ellipsoid.WGS84;
+
+		// set Lincoln Memorial coordinates
+		GlobalCoordinates lincolnMemorial;
+		lincolnMemorial = new GlobalCoordinates(38.88922, -77.04978);
+
+		// set the direction and distance
+		double startBearing = 51.7679;
+		double distance = 6179016.13586;
+
+		// find the destination
+		double[] endBearing = new double[1];
+		GlobalCoordinates dest = geoCalc.calculateEndingGlobalCoordinates(
+				reference, lincolnMemorial, startBearing, distance, endBearing);
+
+		System.out
+				.println("Travel from Lincoln Memorial at 51.767921 deg for 6179.016 km");
+		System.out.printf("   Destination: %1.4f%s", dest.getLatitude(),
+				(dest.getLatitude() > 0) ? "N" : "S");
+		System.out.printf(", %1.4f%s\n", dest.getLongitude(),
+				(dest.getLongitude() > 0) ? "E" : "W");
+		System.out.printf("   End Bearing: %1.2f degrees\n", endBearing[0]);
+	}
+
+	/**
+	 * Calculate the two-dimensional path from
+	 * 
+	 * Lincoln Memorial in Washington, D.C --> 38.8892N, 77.04978W
+	 * 
+	 * to
+	 * 
+	 * Eiffel Tower in Paris --> 48.85889N, 2.29583E
+	 * 
+	 * using WGS84 reference ellipsoid
+	 */
+	static void TwoDimensionalCalculation() {
+		// instantiate the calculator
+		GeodeticCalculator geoCalc = new GeodeticCalculator();
+
+		// select a reference elllipsoid
+		Ellipsoid reference = Ellipsoid.WGS84;
+
+		// set Lincoln Memorial coordinates
+		GlobalCoordinates lincolnMemorial;
+		lincolnMemorial = new GlobalCoordinates(38.88922, -77.04978);
+
+		// set Eiffel Tower coordinates
+		GlobalCoordinates eiffelTower;
+		eiffelTower = new GlobalCoordinates(48.85889, 2.29583);
+
+		// calculate the geodetic curve
+		GeodeticCurve geoCurve = geoCalc.calculateGeodeticCurve(reference,
+				lincolnMemorial, eiffelTower);
+		double ellipseKilometers = geoCurve.getEllipsoidalDistance() / 1000.0;
+		double ellipseMiles = ellipseKilometers * 0.621371192;
+
+		System.out
+				.println("2-D path from Lincoln Memorial to Eiffel Tower using WGS84");
+		System.out.printf(
+				"   Ellipsoidal Distance: %1.2f kilometers (%1.2f miles)\n",
+				ellipseKilometers, ellipseMiles);
+		System.out.printf("   Azimuth:              %1.2f degrees\n",
+				geoCurve.getAzimuth());
+		System.out.printf("   Reverse Azimuth:      %1.2f degrees\n",
+				geoCurve.getReverseAzimuth());
+	}
+
+	/**
+	 * Calculate the three-dimensional path from
+	 * 
+	 * Pike's Peak in Colorado --> 38.840511N, 105.0445896W, 4301 meters
+	 * 
+	 * to
+	 * 
+	 * Alcatraz Island --> 37.826389N, 122.4225W, sea level
+	 * 
+	 * using WGS84 reference ellipsoid
+	 */
+	static void ThreeDimensionalCalculation() {
+		// instantiate the calculator
+		GeodeticCalculator geoCalc = new GeodeticCalculator();
+
+		// select a reference elllipsoid
+		Ellipsoid reference = Ellipsoid.WGS84;
+
+		// set Pike's Peak position
+		GlobalPosition pikesPeak;
+		pikesPeak = new GlobalPosition(38.840511, -105.0445896, 4301.0);
+
+		// set Alcatraz Island coordinates
+		GlobalPosition alcatrazIsland;
+		alcatrazIsland = new GlobalPosition(37.826389, -122.4225, 0.0);
+
+		// calculate the geodetic measurement
+		GeodeticMeasurement geoMeasurement;
+		double p2pKilometers;
+		double p2pMiles;
+		double elevChangeMeters;
+		double elevChangeFeet;
+
+		geoMeasurement = geoCalc.calculateGeodeticMeasurement(reference, pikesPeak,
+				alcatrazIsland);
+		p2pKilometers = geoMeasurement.getPointToPointDistance() / 1000.0;
+		p2pMiles = p2pKilometers * 0.621371192;
+		elevChangeMeters = geoMeasurement.getElevationChange();
+		elevChangeFeet = elevChangeMeters * 3.2808399;
+
+		System.out
+				.println("3-D path from Pike's Peak to Alcatraz Island using WGS84");
+		System.out.printf(
+				"   Point-to-Point Distance: %1.2f kilometers (%1.2f miles)\n",
+				p2pKilometers, p2pMiles);
+		System.out.printf(
+				"   Elevation change:        %1.1f meters (%1.1f} feet)\n",
+				elevChangeMeters, elevChangeFeet);
+		System.out.printf("   Azimuth:                 %1.2f degrees\n",
+				geoMeasurement.getAzimuth());
+		System.out.printf("   Reverse Azimuth:         %1.2f degrees\n",
+				geoMeasurement.getReverseAzimuth());
+	}
+
+	static public void main(String[] args) {
+		TwoDimensionalDirectCalculation();
+
+		System.out.println();
+
+		TwoDimensionalCalculation();
+
+		System.out.println();
+
+		ThreeDimensionalCalculation();
+
+		System.out.println();
+	}
+}

src/org/gavaghan/geodesy/GeodeticCalculator.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+/**
+ * <p>
+ * Implementation of Thaddeus Vincenty's algorithms to solve the direct and
+ * inverse geodetic problems. For more information, see Vincent's original
+ * publication on the NOAA website:
+ * </p>
+ * See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
+ * 
+ * @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);
+   }
+}

src/org/gavaghan/geodesy/GeodeticCurve.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+import java.io.Serializable;
+
+/**
+ * This is the outcome of a geodetic calculation. It represents the path and
+ * ellipsoidal distance between two GlobalCoordinates for a specified reference
+ * ellipsoid.
+ * 
+ * @author Mike Gavaghan
+ */
+public class GeodeticCurve implements Serializable
+{
+   /** Ellipsoidal distance (in meters). */
+   private final double mEllipsoidalDistance;
+
+   /** Azimuth (degrees from north). */
+   private final double mAzimuth;
+
+   /** Reverse azimuth (degrees from north). */
+   private final double mReverseAzimuth;
+
+   /**
+    * Create a new GeodeticCurve.
+    * @param ellipsoidalDistance ellipsoidal distance in meters
+    * @param azimuth azimuth in degrees
+    * @param reverseAzimuth reverse azimuth in degrees
+    */
+   public GeodeticCurve(double ellipsoidalDistance, double azimuth, double reverseAzimuth)
+   {
+      mEllipsoidalDistance = ellipsoidalDistance;
+      mAzimuth = azimuth;
+      mReverseAzimuth = reverseAzimuth;
+   }
+
+   /**
+    * Get the ellipsoidal distance.
+    * @return ellipsoidal distance in meters
+    */
+   public double getEllipsoidalDistance()
+   {
+      return mEllipsoidalDistance;
+   }
+
+   /**
+    * Get the azimuth.
+    * @return azimuth in degrees
+    */
+   public double getAzimuth()
+   {
+      return mAzimuth;
+   }
+
+   /**
+    * Get the reverse azimuth.
+    * @return reverse azimuth in degrees
+    */
+   public double getReverseAzimuth()
+   {
+      return mReverseAzimuth;
+   }
+
+   /**
+    * Get curve as a string.
+    * @return
+    */
+   @Override
+   public String toString()
+   {
+      StringBuffer buffer = new StringBuffer();
+
+      buffer.append("s=");
+      buffer.append(mEllipsoidalDistance);
+      buffer.append(";a12=");
+      buffer.append(mAzimuth);
+      buffer.append(";a21=");
+      buffer.append(mReverseAzimuth);
+      buffer.append(";");
+
+      return buffer.toString();
+   }
+}

src/org/gavaghan/geodesy/GeodeticMeasurement.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+/**
+ * This is the outcome of a three dimensional geodetic calculation. It
+ * represents the path a between two GlobalPositions for a specified reference
+ * ellipsoid.
+ * 
+ * @author Mike Gavaghan
+ */
+public class GeodeticMeasurement extends GeodeticCurve
+{
+   /**
+    * The elevation change, in meters, going from the starting to the ending
+    * point.
+    */
+   private final double mElevationChange;
+
+   /** The distance travelled, in meters, going from one point to the next. */
+   private final double mP2P;
+
+   /**
+    * Creates a new instance of GeodeticMeasurement.
+    * 
+    * @param ellipsoidalDistance ellipsoidal distance in meters
+    * @param azimuth azimuth in degrees
+    * @param reverseAzimuth reverse azimuth in degrees
+    * @param elevationChange the change in elevation, in meters, going from the
+    *           starting point to the ending point
+    */
+   public GeodeticMeasurement(double ellipsoidalDistance, double azimuth, double reverseAzimuth, double elevationChange)
+   {
+      super(ellipsoidalDistance, azimuth, reverseAzimuth);
+      mElevationChange = elevationChange;
+      mP2P = Math.sqrt(ellipsoidalDistance * ellipsoidalDistance + mElevationChange * mElevationChange);
+   }
+
+   /**
+    * Creates a new instance of GeodeticMeasurement.
+    * 
+    * @param averageCurve average geodetic curve
+    * @param elevationChange the change in elevation, in meters, going from the
+    *           starting point to the ending point
+    */
+   public GeodeticMeasurement(GeodeticCurve averageCurve, double elevationChange)
+   {
+      this(averageCurve.getEllipsoidalDistance(), averageCurve.getAzimuth(), averageCurve.getReverseAzimuth(), elevationChange);
+   }
+
+   /**
+    * Get the elevation change.
+    * 
+    * @return elevation change, in meters, going from the starting to the ending
+    *         point
+    */
+   public double getElevationChange()
+   {
+      return mElevationChange;
+   }
+
+   /**
+    * Get the point-to-point distance.
+    * 
+    * @return the distance travelled, in meters, going from one point to the
+    *         next
+    */
+   public double getPointToPointDistance()
+   {
+      return mP2P;
+   }
+
+   /**
+    * Get the GeodeticMeasurement as a string.
+    */
+   @Override
+   public String toString()
+   {
+      StringBuffer buffer = new StringBuffer();
+
+      buffer.append(super.toString());
+      buffer.append("elev12=");
+      buffer.append(mElevationChange);
+      buffer.append(";p2p=");
+      buffer.append(mP2P);
+
+      return buffer.toString();
+   }
+
+}

src/org/gavaghan/geodesy/GlobalCoordinates.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * Encapsulation of latitude and longitude coordinates on a globe. Negative
+ * latitude is southern hemisphere. Negative longitude is western hemisphere.
+ * </p>
+ * <p>
+ * Any angle may be specified for longtiude and latitude, but all angles will be
+ * canonicalized such that:
+ * </p>
+ * 
+ * <pre>
+ * -90 &lt;= latitude &lt;= +90 - 180 &lt; longitude &lt;= +180
+ * </pre>
+ * 
+ * @author Mike Gavaghan
+ */
+public class GlobalCoordinates implements Comparable<GlobalCoordinates>, Serializable
+{
+   /** Latitude in degrees. Negative latitude is southern hemisphere. */
+   private double mLatitude;
+
+   /** Longitude in degrees. Negative longitude is western hemisphere. */
+   private double mLongitude;
+
+   /**
+    * Canonicalize the current latitude and longitude values such that:
+    * 
+    * <pre>
+    * -90 &lt;= latitude &lt;= +90 - 180 &lt; longitude &lt;= +180
+    * </pre>
+    */
+   private void canonicalize()
+   {
+      mLatitude = (mLatitude + 180) % 360;
+      if (mLatitude < 0) mLatitude += 360;
+      mLatitude -= 180;
+
+      if (mLatitude > 90)
+      {
+         mLatitude = 180 - mLatitude;
+         mLongitude += 180;
+      }
+      else if (mLatitude < -90)
+      {
+         mLatitude = -180 - mLatitude;
+         mLongitude += 180;
+      }
+
+      mLongitude = ((mLongitude + 180) % 360);
+      if (mLongitude <= 0) mLongitude += 360;
+      mLongitude -= 180;
+   }
+
+   /**
+    * Construct a new GlobalCoordinates. Angles will be canonicalized.
+    * 
+    * @param latitude latitude in degrees
+    * @param longitude longitude in degrees
+    */
+   public GlobalCoordinates(double latitude, double longitude)
+   {
+      mLatitude = latitude;
+      mLongitude = longitude;
+      canonicalize();
+   }
+
+   /**
+    * Get latitude.
+    * 
+    * @return latitude in degrees
+    */
+   public double getLatitude()
+   {
+      return mLatitude;
+   }
+
+   /**
+    * Set latitude. The latitude value will be canonicalized (which might result
+    * in a change to the longitude). Negative latitude is southern hemisphere.
+    * 
+    * @param latitude in degrees
+    */
+   public void setLatitude(double latitude)
+   {
+      mLatitude = latitude;
+      canonicalize();
+   }
+
+   /**
+    * Get longitude.
+    * 
+    * @return longitude in degrees
+    */
+   public double getLongitude()
+   {
+      return mLongitude;
+   }
+
+   /**
+    * Set longitude. The longitude value will be canonicalized. Negative
+    * longitude is western hemisphere.
+    * 
+    * @param longitude in degrees
+    */
+   public void setLongitude(double longitude)
+   {
+      mLongitude = longitude;
+      canonicalize();
+   }
+
+   /**
+    * Compare these coordinates to another set of coordiates. Western longitudes
+    * are less than eastern logitudes. If longitudes are equal, then southern
+    * latitudes are less than northern latitudes.
+    * 
+    * @param other instance to compare to
+    * @return -1, 0, or +1 as per Comparable contract
+    */
+   public int compareTo(GlobalCoordinates other)
+   {
+      int retval;
+
+      if (mLongitude < other.mLongitude) retval = -1;
+      else if (mLongitude > other.mLongitude) retval = +1;
+      else if (mLatitude < other.mLatitude) retval = -1;
+      else if (mLatitude > other.mLatitude) retval = +1;
+      else retval = 0;
+
+      return retval;
+   }
+
+   /**
+    * Get a hash code for these coordinates.
+    * 
+    * @return
+    */
+   @Override
+   public int hashCode()
+   {
+      return ((int) (mLongitude * mLatitude * 1000000 + 1021)) * 1000033;
+   }
+
+   /**
+    * Compare these coordinates to another object for equality.
+    * 
+    * @param other
+    * @return
+    */
+   @Override
+   public boolean equals(Object obj)
+   {
+      if (!(obj instanceof GlobalCoordinates)) return false;
+
+      GlobalCoordinates other = (GlobalCoordinates) obj;
+
+      return (mLongitude == other.mLongitude) && (mLatitude == other.mLatitude);
+   }
+
+   /**
+    * Get coordinates as a string.
+    */
+   @Override
+   public String toString()
+   {
+      StringBuffer buffer = new StringBuffer();
+
+      buffer.append(Math.abs(mLatitude));
+      buffer.append((mLatitude >= 0) ? 'N' : 'S');
+      buffer.append(';');
+      buffer.append(Math.abs(mLongitude));
+      buffer.append((mLongitude >= 0) ? 'E' : 'W');
+      buffer.append(';');
+
+      return buffer.toString();
+   }
+}

src/org/gavaghan/geodesy/GlobalPosition.java

+/* Geodesy by Mike Gavaghan
+ * 
+ * http://www.gavaghan.org/blog/free-source-code/geodesy-library-vincentys-formula/
+ * 
+ * This code may be freely used and modified on any personal or professional
+ * project.  It comes with no warranty.
+ */
+package org.gavaghan.geodesy;
+
+/**
+ * <p>
+ * Encapsulates a three dimensional location on a globe (GlobalCoordinates
+ * combined with an elevation in meters above a reference ellipsoid).
+ * </p>
+ * <p>
+ * See documentation for GlobalCoordinates for details on how latitude and
+ * longitude measurements are canonicalized.
+ * </p>
+ * 
+ * @author Mike Gavaghan
+ */
+public class GlobalPosition extends GlobalCoordinates
+{
+   /** Elevation, in meters, above the surface of the ellipsoid. */
+   private double mElevation;
+
+   /**
+    * Creates a new instance of GlobalPosition.
+    * 
+    * @param latitude latitude in degrees
+    * @param longitude longitude in degrees
+    * @param elevation elevation, in meters, above the reference ellipsoid
+    */
+   public GlobalPosition(double latitude, double longitude, double elevation)
+   {
+      super(latitude, longitude);
+      mElevation = elevation;
+   }
+
+   /**
+    * Creates a new instance of GlobalPosition.
+    * 
+    * @param coords coordinates of the position
+    * @param elevation elevation, in meters, above the reference ellipsoid
+    */
+   public GlobalPosition(GlobalCoordinates coords, double elevation)
+   {
+      this(coords.getLatitude(), coords.getLongitude(), elevation);
+   }
+
+   /**
+    * Get elevation.
+    * 
+    * @return elevation about the ellipsoid in meters.
+    */
+   public double getElevation()
+   {
+      return mElevation;
+   }
+
+   /**
+    * Set the elevation.
+    * 
+    * @param elevation elevation about the ellipsoid in meters.
+    */
+   public void setElevation(double elevation)
+   {
+      mElevation = elevation;
+   }
+
+   /**
+    * Compare this position to another. Western longitudes are less than eastern
+    * logitudes. If longitudes are equal, then southern latitudes are less than
+    * northern latitudes. If coordinates are equal, lower elevations are less
+    * than higher elevations
+    * 
+    * @param other instance to compare to
+    * @return -1, 0, or +1 as per Comparable contract
+    */
+   public int compareTo(GlobalPosition other)
+   {
+      int retval = super.compareTo(other);
+
+      if (retval == 0)
+      {
+         if (mElevation < other.mElevation) retval = -1;
+         else if (mElevation > other.mElevation) retval = +1;
+      }
+
+      return retval;
+   }