using System; using System.Device.Location; using System.Windows.Threading; using System.Collections.Generic; namespace SharpGIS.WinPhone.Gps { /// /// Simulates the GeoCoordinateWatcher /// public sealed class GpsDriveSimulator: IGeoPositionWatcher, IDisposable { private const double MaxMovement = 0.0004; //Degrees #region Private Fields private volatile bool disposed; private DispatcherTimer timer; private GeoPositionStatus status = GeoPositionStatus.NoData; private GeoPosition position; private Random random = new Random(); private double lastlatitude = double.NaN; private double lastlongitude = double.NaN; private DateTime lastTime; private IList drivePath; private double speed; #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 GpsDriveSimulator(IList path, double speed) { drivePath = path; lastlongitude = path[0].Longitude; lastlatitude = path[0].Latitude; this.speed = speed; timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) }; timer.Tick += timer_Tick; } double totalDistance; /// /// Fired every time a simulated GPS measurement needs to occur /// /// /// private void timer_Tick(object sender, EventArgs e) { DateTime now = DateTime.Now; TimeSpan time = now - lastTime; double deltaDistance = time.TotalSeconds * speed; double course; double[] p = PointAlongLine(totalDistance + deltaDistance, out course); if (p == null) { totalDistance = 0; return; } totalDistance += deltaDistance; double lon = p[0]; double lat = p[1]; double altitude = p[2]; while (lat < -90) lat += 180; while (lat > 90) lat -= 180; while (lon < -180) lon += 360; while (lon > 180) lon -= 360; while (course < 0) course += 360; while (course > 360) course -= 360; lastTime = now; Position = new GeoPosition() { Location = new GeoCoordinate( lat, lon, altitude, //Location 0, 0, //Accuracy speed, course), //Movement Timestamp = now }; Status = GeoPositionStatus.Ready; lastlatitude = lat; lastlongitude = lon; lastTime = now; } private double[] PointAlongLine(double dist, out double course) { double accDist = 0; course = double.NaN; for (int i = 0; i < drivePath.Count - 1; i++) { var p1 = drivePath[i]; var p2 = drivePath[i + 1]; double distToWaypoint = GetDistanceGeodesic(p1.Longitude, p1.Latitude, p2.Longitude, p2.Latitude); if (dist < accDist + distToWaypoint) { var distAlongSegment = dist - accDist; double fraction = distAlongSegment / distToWaypoint; double altitude = (p2.Altitude - p1.Altitude) * fraction + p1.Latitude; course = GetTrueBearingGeodesic(p1.Longitude, p1.Latitude, p2.Longitude, p2.Latitude); return GetPointFromHeadingGeodesic(new double[] { p1.Longitude, p1.Latitude, altitude }, distAlongSegment, course); } accDist += distToWaypoint; } return null; } #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, start[2] }; } #endregion #region Properties /// /// Gets the position. /// /// The position. public GeoPosition Position { get { return position; } private set { if (value != position) { position = value; if(value != null) 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)); } } } #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(); lastTime = DateTime.Now; 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"); } } ~GpsDriveSimulator() { this.Dispose(false); } #endregion } }