using System;
using System.Device.Location;
using System.Windows.Threading;
namespace SharpGIS.WinPhone.Gps
{
///
/// Simulates the GeoCoordinateWatcher
///
public sealed class GeoCoordinateSimulator : IGeoPositionWatcher, IDisposable
{
private const double MaxMovement = 0.0004; //Degrees
#region Private Fields
private double threshold = 0d;
private volatile bool disposed;
private DispatcherTimer timer;
private GeoPositionStatus status = GeoPositionStatus.NoData;
private GeoPosition position;
private Random random = new Random();
private double accumulatedLongitude = double.NaN;
private double accumulatedLatitude = double.NaN;
private double altitude = double.NaN;
private double lastlatitude = double.NaN;
private double lastlongitude = double.NaN;
private DateTime lastTime;
private double startLatitude;
private double startLongitude;
private double lastCourse = 0;
#endregion
///
/// Initializes a new instance of the class.
///
/// The start latitude where the simulation should begin.
/// The start longitude where the simulation should begin.
public GeoCoordinateSimulator(double startLatitude, double startLongitude)
{
this.startLatitude = startLatitude;
this.startLongitude = startLongitude;
timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
timer.Tick += timer_Tick;
}
///
/// Fired every time a simulated GPS measurement needs to occur
///
///
///
private void timer_Tick(object sender, EventArgs e)
{
double lat;
double lon;
double speed = 0;
double course = 0;
DateTime now = DateTime.Now;
double distance = double.PositiveInfinity;
if (double.IsNaN(accumulatedLatitude) || double.IsNaN(accumulatedLongitude) || double.IsNaN(altitude))
{
lon = startLongitude;
lat = startLatitude;
altitude = 500;
course = random.NextDouble() * 360;
}
else
{
course = lastCourse + (random.NextDouble() * 15 - 7.5);
var result = GetPointFromHeadingGeodesic(new double[] { accumulatedLongitude, accumulatedLatitude },
random.NextDouble() * 50, course);
lon = result[0];
lat = result[1];
altitude += (random.NextDouble() - .5) * 10;
while (lat < -90) lat += 180;
while (lat > 90) lat -= 180;
while (lon < -180) lon += 360;
while (lon > 180) lon -= 360;
distance = GetDistanceGeodesic(lastlongitude, lastlatitude, lon, lat);
if (Status == GeoPositionStatus.Ready)
{
TimeSpan time = now - lastTime;
speed = (distance / time.TotalSeconds);
}
}
accumulatedLongitude = lon;
accumulatedLatitude = lat;
lastTime = now;
lastCourse = course;
if (distance >= MovementThreshold) //Only fire if the amount moved since last event is greater than the threshold
{
while (course < 0) course += 360;
while (course > 360) course -= 360;
Position = new GeoPosition()
{
Location = new GeoCoordinate(
accumulatedLatitude, accumulatedLongitude, altitude, //Location
random.NextDouble() * 100, random.NextDouble() * 200, //Accuracy
speed, course), //Movement
Timestamp = now
};
Status = GeoPositionStatus.Ready;
lastlatitude = accumulatedLatitude;
lastlongitude = accumulatedLongitude;
lastTime = now;
}
}
#region Geographic calculations
///
/// Calculates the distance between two points in meters
///
///
///
///
///
///
private static double GetDistanceGeodesic(double lon1, double lat1, double lon2, double lat2)
{
lon1 = lon1 / 180 * Math.PI;
lon2 = lon2 / 180 * Math.PI;
lat1 = lat1 / 180 * Math.PI;
lat2 = lat2 / 180 * Math.PI;
return 2 * Math.Asin(Math.Sqrt(Math.Pow((Math.Sin((lat1 - lat2) / 2)), 2) +
Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin((lon1 - lon2) / 2), 2))) * 6378137;
}
///
/// Calculates the bearing between two points.
///
///
///
///
///
///
///
private static double GetTrueBearingGeodesic(double lon1, double lat1, double lon2, double lat2)
{
lat1 = lat1 / 180 * Math.PI;
lon1 = lon1 / 180 * Math.PI;
lat2 = lat2 / 180 * Math.PI;
lon2 = lon2 / 180 * Math.PI;
double tc1 = Math.Atan2(Math.Sin(lon1 - lon2) * Math.Cos(lat2),
Math.Cos(lat1) * Math.Sin(lat2) - Math.Sin(lat1) * Math.Cos(lat2) * Math.Cos(lon1 - lon2)) % (2 * Math.PI);
return 360 - (tc1 / Math.PI * 180);
}
public static double[] GetPointFromHeadingGeodesic(double[] start, double distance, double heading)
{
double brng = heading / 180 * Math.PI;
double lon1 = start[0] / 180 * Math.PI;
double lat1 = start[1] / 180 * Math.PI;
double dR = distance / 6378137; //Angular distance in radians
double lat2 = Math.Asin(Math.Sin(lat1) * Math.Cos(dR) + Math.Cos(lat1) * Math.Sin(dR) * Math.Cos(brng));
double lon2 = lon1 + Math.Atan2(Math.Sin(brng) * Math.Sin(dR) * Math.Cos(lat1), Math.Cos(dR) - Math.Sin(lat1) * Math.Sin(lat2));
double lon = lon2 / Math.PI * 180;
double lat = lat2 / Math.PI * 180;
while (lon < -180) lon += 360;
while (lat < -90) lat += 180;
while (lon > 180) lon -= 360;
while (lat > 90) lat -= 180;
return new double[] { lon, lat };
}
#endregion
#region Properties
///
/// Gets the position.
///
/// The position.
public GeoPosition Position
{
get
{
return position;
}
private set
{
if (value != position)
{
position = value;
RaisePositionChanged(new GeoPositionChangedEventArgs(position));
}
}
}
///
/// The status of the location service.
///
public GeoPositionStatus Status
{
get
{
return status;
}
private set
{
if (value != status)
{
status = value;
RaiseStatusChanged(new GeoPositionStatusChangedEventArgs(value));
}
}
}
///
/// The minimum distance that must be travelled between successive
/// events.
///
/// The distance, in meters, that must be travelled between
/// successive events.
public double MovementThreshold
{
get
{
DisposeCheck();
return threshold;
}
set
{
DisposeCheck();
if ((value < 0.0) || double.IsNaN(value))
{
throw new ArgumentOutOfRangeException("value", "Argument must be non negative");
}
threshold = value;
}
}
#endregion
#region Public Methods
///
/// Starts the acquisition of data from the location service.
///
/// This parameter is not used..
public void Start(bool suppressPermissionPrompt)
{
Start();
}
///
/// Starts the acquisition of data from the location service.
///
public void Start()
{
this.DisposeCheck();
timer.Start();
Status = GeoPositionStatus.Initializing;
}
///
/// Stops the acquisition of data from the location service.
///
public void Stop()
{
this.DisposeCheck();
timer.Stop();
this.Position = null;
Status = GeoPositionStatus.Disabled;
}
///
/// Attempts to start the acquisition of data from the location service. If the
/// provided timeout interval is exceeded before the location service responds,
/// the request for location is stopped and the method returns false.
///
/// This parameter is not used.
/// A TimeSpan object specifying the amount of time to wait for location data
/// acquisition to begin.
/// true if the location service responds within the
/// timeout window. Otherwise, false.
public bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
{
Start();
return true;
}
#endregion
#region Events
private void RaisePositionChanged(GeoPositionChangedEventArgs args)
{
if (PositionChanged != null)
{
PositionChanged(this, args);
}
}
///
/// Occurs when the location service detects a change in position.
///
public event EventHandler> PositionChanged;
private void RaiseStatusChanged(GeoPositionStatusChangedEventArgs args)
{
if (StatusChanged != null)
{
StatusChanged(this, args);
}
}
///
/// Occurs when the status of the location service changes.
///
public event EventHandler StatusChanged;
#endregion
#region IDisposable
///
/// Releases managed and unmanaged resources used by the
/// and stops the acquisition of data from the location service.
///
public void Dispose()
{
this.Dispose(true);
}
private void Dispose(bool disposing)
{
lock (this)
{
if (!this.disposed)
{
this.Stop();
this.disposed = true;
}
}
}
private void DisposeCheck()
{
if (this.disposed)
{
throw new ObjectDisposedException("GeoCoordinateSimulator");
}
}
~GeoCoordinateSimulator()
{
this.Dispose(false);
}
#endregion
}
}