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 } }