Sunday, April 29, 2012

LocationServices for Windows Phone, part 3: the setup

As stated in previous posts the entire framework is built to be observable using Reactive Extensions (Rx).

Consider this as a simple scenario:

“Get me all known motorcycle parking places in Stockholm within a radius of 500 meters from my current location”

First things first – add the core references:

  • Usoniandream.WindowsPhone.LocationServices
  • Usoniandream.WindowsPhone.Extensions
  • Usoniandream.WindowsPhone.GeoConverter
  • Next up, add a reference to the data library for Stockholm:

  • Usoniandream.WindowsPhone.LocationServices.Stockholm
  • Now, for the ViewModel code parts..

    from the top, a collection is needed:

     public ObservableCollection<Models.Stockholm.ParkingLocation> ParkingLocations { get; set; }

    next, a routine to wire that collection to the library and the underlying API:

                var rxlocations = sthlmservicelayer.GetParkingLocationsByRadius(new SearchCriterias.Stockholm.Parking.ParkingLocation.ParkingLocationsByRadius(500, Models.Enums.Stockholm.VehicleTypeEnum.Bike))
                    .ObserveOnDispatcher()
                    .Subscribe(
                    // result
                        x =>
                        {
                            ParkingLocations.Add(x);
                        },
                    // exception
                        ex =>
                        {
                            // handle exception..
                        });
    

    And that’s it – the ParkingLocations collection is ready to go!

    how does it work?

    First, there’s the ServiceLayer which acts as the point of entry to the framework.

    namespace Usoniandream.WindowsPhone.LocationServices.Service.Stockholm.Reactive
    {
        public class ServiceLayer : LocationServices.Service.Reactive.GenericServiceLayer
        {
        }
    }
    

    In each ServiceLayer (there’s one for observable calls, and one using standard RestSharp async pattern) the methods are listed one after the other:

            public IObservable<Models.Stockholm.ParkingLocation> GetParkingLocationsByRadius(SearchCriterias.Stockholm.Parking.ParkingLocation.ParkingLocationsByRadius criteria)
            {
                return ExecuteRequestReturnObservable<Models.Stockholm.ParkingLocation, Models.JSON.Stockholm.ParkingPlaces.RootObject>(criteria);
            }
    

    These use one of (currently) four available methods found in the base class GenericServiceLayer:

            public IObservable<Ttarget> ExecuteRequestReturnObservable<Ttarget, Tsource>(SearchCriterias.ISearchCriteria<Ttarget, Tsource> criteria) where Tsource : new()
            {
                if (String.IsNullOrWhiteSpace(criteria.Client.BaseUrl))
                {
                    throw new ArgumentException("missing 'baseurlresourcename', please check App.xaml.", "baseurlresourcename");
                }
                if (String.IsNullOrWhiteSpace(criteria.APIkey))
                {
                    throw new ArgumentException("missing api key, please check App.xaml.", "APIkey");
                }
    
                return criteria.Client.ExecuteAsync<Tsource>(criteria.Request)
                                .SelectMany<Tsource, Ttarget>(x => criteria.Mapper.JSON2Model(x));
            }
    
    

    Using Reactive Extensions and a mapper (see here for an example of a mapper) the JSON response is transformed to a local model representation. The in param for each method is of type SearchCriteria which simply acts as a strong-typed request definition while in turn holds key data such as endpoint url,parameters and more.

    namespace Usoniandream.WindowsPhone.LocationServices.Models.Stockholm
    {
        public class ParkingLocation : ILocation
        {
            public virtual GeoCoordinate Location { get; set; }
            public virtual object Content { get; set; }
    
            public List<List<double>> Coordinates { get; set; }
    
            public string CityDistrict { get; set; }
            public int ParkingTimeTotalMinutes 
            {
                get
                {
                    int total = (MaxDays.GetValueOrDefault(0) * 24 * 60) +
                        (MaxHours.GetValueOrDefault(0) * 60) +
                        MaxMinutes.GetValueOrDefault(0);
                    if (total > 0)
                    {
                        return total;
                    }
                    else
                    {
                        return int.MaxValue;
                    }
                }
            }
            public int? MaxDays { get; set; }
            public int? MaxHours { get; set; }
            public int? MaxMinutes { get; set; }
            public string OtherInfo { get; set; }
            public int? Meters { get; set; }
            public string Type { get; set; }
            public string StartWeekDay { get; set; }
        }
    }
    

    The SearchCriteria mentioned above is also structured in a fairly generic manner:

    namespace Usoniandream.WindowsPhone.LocationServices.SearchCriterias.Stockholm.Parking.ParkingLocation
    {
        public class ParkingLocationsByRadius : ParkingLocationsBase
        {
            public ParkingLocationsByRadius(int radius, Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum vehicletype)
                : base(vehicletype)
            {
                Radius = radius;
    
                Request.Resource += "within?";
                
                Request.AddParameter("radius", Radius);
                Request.AddParameter("lat", Location.Latitude.ToString().Replace(",", "."));
                Request.AddParameter("&lng",Location.Longitude.ToString().Replace(",", "."));
            }
    
            public GeoCoordinate Location { get; set; }
            public int Radius { get; private set; }
        }
    }
    

    as you might notice, the criteria above inherits a ParkingsLocationBase, here’s what that looks like:

    namespace Usoniandream.WindowsPhone.LocationServices.SearchCriterias.Stockholm.Parking.ParkingLocation
    {
        public abstract class ParkingLocationsBase : SearchCriteriaBase<Models.Stockholm.ParkingLocation, Usoniandream.WindowsPhone.LocationServices.Models.JSON.Stockholm.ParkingPlaces.RootObject>
        {
            public ParkingLocationsBase(Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum vehicletype)
                : base("STHLM_DATA_SERVICE_URI_PARKINGLOCATION")
            {
                Mapper = new Usoniandream.WindowsPhone.LocationServices.Mappers.Stockholm.Parking.ParkingLocation();
                APIKeyResourceName = "STHLM_DATA_API_KEY_PARKING";
                VehicleType = vehicletype;
    
                switch (this.VehicleType)
                {
                    case Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum.Car:
                        Request.Resource += "ptillaten/";
                        break;
                    case Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum.Bike:
                        Request.Resource += "pmotorcykel/";
                        break;
                    case Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum.Truck:
                        Request.Resource += "plastbil/";
                        break;
                    case Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum.Handicap:
                        Request.Resource += "prorelsehindrad/";
                        break;
                    default:
                        throw new ArgumentException("VehicleType must be set", "VehicleType");
                }
    
                Request.AddParameter("outputFormat", "json");
                Request.AddParameter("apiKey", APIkey);
            }
    
            public Usoniandream.WindowsPhone.LocationServices.Models.Enums.Stockholm.VehicleTypeEnum VehicleType { get; private set; }
        }
    }

    And, in turn, the ParkingLocationBase inherits from SearchCriteriaBase which adds the following to the mix:

        public abstract class SearchCriteriaBase<Ttarget, Tsource> : INotifyPropertyChanged, Usoniandream.WindowsPhone.LocationServices.SearchCriterias.ISearchCriteria<Ttarget, Tsource>
        {
            public SearchCriteriaBase()
            {
                Request = new RestRequest();
                Client = new RestClient();
            }
            public SearchCriteriaBase(string baseurlresourcename)
            {
                Request = new RestRequest();
                if (string.IsNullOrWhiteSpace(baseurlresourcename))
                {
                    throw new ArgumentException("missing 'baseurlresourcename', please check App.xaml.", "baseurlresourcename");
                }
                Client = new RestClient(((Models.ServiceURI)Application.Current.Resources[baseurlresourcename]).URL);
            }
            public SearchCriteriaBase(Method method)
            {
                Request = new RestRequest(method);
                Client = new RestClient();
            }
            public SearchCriteriaBase(Method method, string baseurlresourcename)
            {
                Request = new RestRequest(method);
                if (string.IsNullOrWhiteSpace(baseurlresourcename))
                {
                    throw new ArgumentException("missing 'baseurlresourcename', please check App.xaml.", "baseurlresourcename");
                }
                Client = new RestClient(((Models.ServiceURI)Application.Current.Resources[baseurlresourcename]).URL);
            }
    
            public RestClient Client { get; private set; }
            public RestRequest Request { get; private set; }
    
            public IMapper<Ttarget, Tsource> Mapper { get; set; }
            
            public Dictionary<string, string> Parameters { get; set; }
    
            public virtual string APIKeyResourceName { get; protected set; }
    
            private string apikey = null;
    
            public virtual string APIkey 
            { 
                get
                {
                    if (string.IsNullOrWhiteSpace(apikey))
                    {
                        if (!String.IsNullOrWhiteSpace(APIKeyResourceName))
                        {
                            return ((Models.ServiceAPIKey)Application.Current.Resources[APIKeyResourceName]).Value;
                        }
                        if (!string.IsNullOrWhiteSpace(apikey))
                        {
                            return apikey;
                        }
                        else
                        {
                            throw new ArgumentException("missing api key, please check App.xaml.", "APIkey");
                        }
                    }
                    return apikey;
                } 
                private set
                {
                    apikey = value;
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
        }
    

    So, there you have it. The engine parts that more or less make up the majority of the framework.

    The framework libraries, source code and sample app are all on GitHub: https://github.com/peterdrougge/Usoniandream.WIndowsPhone.LocationServices

    No comments: