using System; using System.Collections.Generic; using System.Device.Location; using Fibre.WP7.Threading; using MahTweets.Mobile.Core.Interfaces; using Microsoft.Phone.Reactive; namespace MahTweets.Mobile.Core.Services { public class LocationService : ILocationService, IDisposable { public static ILocationService Instance { get; set; } private readonly GeoCoordinateWatcher _watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default) { MovementThreshold = 20 }; private readonly List> _locationRequests = new List>(); private readonly object _lock = new object(); private readonly IObservable>> _positionObserver; private readonly IDisposable _locationRequestsObserver; private int _numberSubscriptions = -1; //We don't want to count internal observer for location requests public double PhoneLatitude { get; set; } public double PhoneLongitude { get; set; } private const double RKilometres = 6371; private const double RMiles = 3960; private bool IsEnabled; private readonly IApplicationSettings _applicationSettings; public LocationService(IApplicationSettings applicationSettings) { _applicationSettings = applicationSettings; _positionObserver = Observable.FromEvent>( ev => { _watcher.PositionChanged += ev; //So we can track number of people subscribed lock (_lock) NumberSubscriptions++; }, ev => { _watcher.PositionChanged -= ev; //So we can track number of people subscribed lock (_lock) NumberSubscriptions--; }); _watcher.StatusChanged += WatcherStatusChanged; //Observer for BeginGetLocation method call _locationRequestsObserver = _positionObserver .Subscribe(r=>NotifyWaiting(r.EventArgs.Position)); } void WatcherStatusChanged(object sender, GeoPositionStatusChangedEventArgs e) { lock (_lock) { if (e.Status == GeoPositionStatus.NoData) { foreach (var locationRequest in _locationRequests) { locationRequest.HandleException(new NoLocationDataException(), false); } } } } private int NumberSubscriptions { get { return _numberSubscriptions; } set { _numberSubscriptions = value; if (_numberSubscriptions == 0 && _watcher.Status != GeoPositionStatus.Disabled) _watcher.Stop(); else if (_numberSubscriptions > 0 && (_watcher.Status == GeoPositionStatus.Disabled || _watcher.Status == GeoPositionStatus.NoData)) _watcher.Start(); } } private void NotifyWaiting(GeoPosition position) { lock (_lock) { foreach (var locationRequest in _locationRequests) { locationRequest.Complete(position.Location, false); } } } public IAsyncResult BeginGetLocation(AsyncCallback callback, object state) { if (!IsEnabled) { _watcher.TryStart(false, TimeSpan.FromSeconds(30)); IsEnabled = true; } if (_applicationSettings.AllowGPS == false || _watcher.Status == GeoPositionStatus.Disabled) { var result = new AsyncResult(callback, state); result.Complete(null, true); return result; } lock (_lock) { var asyncResult = new AsyncResult(callback, state); _locationRequests.Add(asyncResult); NumberSubscriptions++; if (_watcher.Status == GeoPositionStatus.Ready && _watcher.Position != null) asyncResult.Complete(_watcher.Position.Location, true); return asyncResult; } } public GeoCoordinate EndGetLocation(IAsyncResult result) { lock (_lock) { var asyncResult = result as AsyncResult; if (asyncResult == null) throw new ArgumentException("Async result is not valid", "result"); if (asyncResult.Result == null) return null; if (!asyncResult.IsCompleted) throw new InvalidOperationException("Async operation not finished"); _locationRequests.Remove(asyncResult); NumberSubscriptions--; if (asyncResult.Exception != null) throw asyncResult.Exception; PhoneLatitude = asyncResult.Result.Latitude; PhoneLongitude = asyncResult.Result.Longitude; return asyncResult.Result; } } public IDisposable SubscribeToPositionChanges(Action> positionUpdated) { return _positionObserver.Subscribe(r => positionUpdated(r.EventArgs.Position)); } public void Dispose() { _locationRequestsObserver.Dispose(); } public string FriendlyDistance(double lat, double lon) { if (PhoneLatitude == 0 && PhoneLongitude == 0) return string.Empty; return CalculateDistance(PhoneLatitude, PhoneLongitude, lat, lon).ToString(); } public Distance CalculateDistance(double lat, double lon, double fromlat, double fromlon) // returns KMs between the current geolocation & another, returns Distance struct { var distanceUnit = _applicationSettings.DistanceUnit; return CalculateDistance(lat, lon, fromlat, fromlon, distanceUnit); } public Distance CalculateDistance(double latit, double longit, double fromlat, double fromlon, DistanceUnit unit) // returns KMs between the current geolocation & another, returns Distance struct { double r = unit == DistanceUnit.Kilometres ? RKilometres : RMiles; var lat = (Math.PI / 180) * (latit - fromlat); var lng = (Math.PI / 180) * (longit - fromlon); var h1 = Math.Sin(lat / 2) * Math.Sin(lat / 2) + Math.Cos((Math.PI / 180) * fromlat) * Math.Cos((Math.PI / 180) * latit) * Math.Sin(lng / 2) * Math.Sin(lng / 2); var h2 = 2 * Math.Asin(Math.Min(1, Math.Sqrt(h1))); return new Distance(r * h2, unit); } } public class NoLocationDataException : Exception { } }