Windows Phone 8: Here Places API

Hola a todos!

Hoy quiero aprovechar para hablar de la plataforma Here. Como sabréis, Here es la plataforma de mapas de Nokia, usando datos de NAVTEQ. En Windows Phone 8, tenemos múltiples aplicaciones que usan esta tecnología, Here Drive, Here Transit, Here Maps… e incluso podemos integrarla en nuestros propios desarrollos gracias al nuevo control de maps y las clases GeocodeQuery y ReverseGeocodeQuery, como ya vimos hace algún tiempo aquí y aquí. Pero existe un API extra, que no está implementada en el SDK de Windows Phone 8: Here Places.

Here Places nos permite acceder a los datos de localización de lugares de Nokia, de la misma forma que podemos hacer en Here Maps. Esto nos ofrece mucha información que poder incluir en nuestras aplicaciones, para enriquecerlas mucho más. Podremos encontrar información de 200 países, más de 1,5 millones de lugares (ciudades, distritos…) en total tendremos más de 200 millones de puntos de interés que podremos buscar y mostrar a nuestros usuarios.

Toda esta información se encuentra disponible a través de un servicio REST de Nokia, muy fácil de consumir desde nuestras aplicaciones Windows Phone. Pero para empezar, tendremos que registrarnos y obtener un Id de aplicación y un Código de aplicación.

Registro en Here Places

Para registrarnos, debemos acudir a http://developer.here.com donde podremos autenticarnos con el usuario y password que tengamos de Nokia developer. Tras autenticarnos, vamos a ir al menú “Create app”, donde necesitaremos indicar un nombre para la aplicación y una descripción:

image

A continuación presionaremos el botón “Get Started”, que nos llevará a la pantalla de selección de plan. Básicamente tendremos dos planes distintos para elegir: “Base”, que incluye mapas 2D ilimitados y 2.500 llamadas a otras APIs, como la de Places en este caso. Este plan es totalmente gratuito. El segundo plan es el “Core” cuesta $1.500 al mes, incluye 10.000 llamadas a otras APIs, pero seguro que se nos va de precio. Por ahora podemos quedarnos con el plan “Base” y presionar el botón “Select” que se encuentra al lado del mismo.

La última pantalla nos mostrará dos códigos: App Id y App Code, que deberemos incluir en todas las llamadas al API REST que realicemos:

image

Una vez que hayamos guardado nuestro Id y Code, podemos ir a Visual Studio y crear un nuevo proyecto de Windows Phone 8 para empezar a usar el API de Here Places.

Como ejemplo, vamos a hacer una aplicación que obtenga nuestra posición y nos devuelva los lugares cercanos disponibles. Además, como bonus, usaremos la clase TaskCompletitionSource, para convertir una clase que no usa async/await y trabajar más sencillamente con ella.

Para comenzar, necesitamos la URL del API REST de Places, exactamente una de estas dos:

Siempre que estemos desarrollando, debemos usar la URL que empieza por demo, dejando la otra solo para aplicaciones en producción. Una vez que sabemos que URL usar, vamos a crear un nuevo servicio en nuestra aplicación (asumo que, después de toda la chapa que he dado, estáis usando MVVM ;-P) que se llamará HereService y contendrá un método llamado GetNearbyPlaces y recibirá un objeto GeoCoordinate como parámetro.

public interface IHereService
{
    Task<VMPlace> GetNearbyPlaces(GeoCoordinate currentLocation);
}

public class HereService : IHereService
{
    private const string APP_ID = "YOURAPPID";
    private const string APP_CODE = "YOURAPPCODE";

    public Task<VMPlace> GetNearbyPlaces(GeoCoordinate currentLocation)
    {
        //TODO: Implement awesome code here!!!
    }
}

Una vez definido su Interface e implementación, vamos a empezar a trabajar. El método GetNearbyPlaces devuelve una Task<VMPlace>, pero nosotros usaremos para comunicarnos con el servicio REST la clase HttpWebRequest que no está preparada para async/await ni usa Tasks. De hecho, si os fijáis en el método GetNearbyPlaces, no he incluido la keyword async. Esto es porque no nos va a hacer falta, ya que el código que va a contener, no hace uso de async/await. Sin embargo el modelo de trabajo normal de HttpWebRequest se basa en callbacks. Por norma general, iniciaríamos la petición y cuando se terminase, llamaría a un callback desde el cual podríamos lanzar un evento para notificar el resultado, lo que nos da el tan conocido patrón Async/Completed.

Comenzaremos el código de nuestro método GetNearbyPlaces, construyendo la URI de la petición, básicamente esta debe contener las coordenadas, el ID de aplicación y el CODE de aplicación:

StringBuilder petitionUri = new StringBuilder("http://places.nlp.nokia.com/places/v1/discover/here?");
petitionUri.AppendFormat("at={0},{1}", currentLocation.Latitude, currentLocation.Longitude);
petitionUri.AppendFormat("&app_id={0}&app_code={1}&accept=application/json", APP_ID, APP_CODE);

Usamos la clase StringBuilder para evitar concatenar múltiples Strings, que crearían instancias en memoria, por un lado añadimos las coordenadas y en segundo lugar el ID y CODE de aplicación.

Una vez que hemos creado la URI, vamos a crear una instancia de la clase TaskCompletitionSource y devolver la Task que contiene como resultado de nuestro método:

public Task<VMPlace> GetNearbyPlaces(GeoCoordinate currentLocation)
{
    StringBuilder petitionUri = new StringBuilder("http://places.nlp.nokia.com/places/v1/discover/here?");
    petitionUri.AppendFormat("at={0},{1}", currentLocation.Latitude, currentLocation.Longitude);
    petitionUri.AppendFormat("&app_id={0}&app_code={1}&accept=application/json", APP_ID, APP_CODE);

    var taskCompletitionSource = new TaskCompletionSource<VMPlace>();

    return taskCompletitionSource.Task;
}

¿Para qué hacemos esto? Es muy sencillo. La teoría detrás de las Task es simple: En ejecución, al hacer un await sobre una Task, la aplicación espera a que esa Task tenga un Result, antes de continuar la ejecución del método que la esperó. Al devolver la Task del TaskCompletitionSource, devolvemos una Task no finalizada, sin resultado, por lo que el await esperará hasta que algo o alguien la complete. Esto lo haremos en el callback del método BeginGetResponse de la clase HttpWebRequest, como podemos ver a continuación:

HttpWebRequest request = HttpWebRequest.CreateHttp(petitionUri.ToString());
request.BeginGetResponse(async result =>
{
    if (result.IsCompleted)
    {
        VMPlace places = new VMPlace();
        taskCompletitionSource.SetResult(places);
    }
}, null);

Como podemos ver en el código anterior, creamos una expresión Lambda que recibe el resultado de la petición, la respuesta. Si está completada, creamos una nueva instancia de la clase VMPlace y usamos el método SetResult de TaskCompletitionSource para establecer el resultado. Es en este momento en el que la ejecución del método que originalmente llamó con await a GetNearbyPlaces, continúa, pues ya tiene un resultado y la Task se ha completado.

Pero no estamos devolviendo el resultado, simplemente devolvemos una instancia vacía. Primero, vamos a examinar el tipo de resultado que nos ofrece el API REST de Here Places:

{
    "results":
    {
        "next":"http://demo.places.nlp.nokia.com/places/v1/discover/search;context=Zmxvdy1pZD1iZDE0OTE1Yy1hMDAzLTU1OTEtYWU4ZC1iYjRlZjE3MGY0YmZfMTM2NDM2NjA5MTE4NV8wXzQ4MTMmb2Zmc2V0PTIw?app_id=APPID&app_code=APPCODE&size=20&q=restaurant&at=37.7851%2C-122.4047",
        "items":[
            {
                "position":[
                    37.785057,-122.404768
                ],
                "distance":8,
                "title":"Bin 55",
                "averageRating":5,
                "category":
                {
                    "id":"bar-pub",
                    "title":"Bares y clubes",
                    "href":"http://demo.places.nlp.nokia.com/places/v1/categories/places/bar-pub?app_id=_peU-uCkp-j8ovkzFGNU&app_code=gBoUkAMoxoqIWfxWA5DuMQ",
                    "type":"urn:nlp-types:category"
                },
                "icon":"http://download.vcdn.nokia.com/p/d/places2/icons/categories/22.icon",
                "vicinity":"55 Fourth Street<br/>San Francisco 94103<br/>EE.UU.",
                "having":[
                ],
                "type":"urn:nlp-types:place",
                "href":"http://demo.places.nlp.nokia.com/places/v1/places/8409q8yy-b29e8557ae0d4a1d8f0d7b7f9c10a8f0;context=Zmxvdy1pZD1iZDE0OTE1Yy1hMDAzLTU1OTEtYWU4ZC1iYjRlZjE3MGY0YmZfMTM2NDM2NjA5MTE4NV8wXzQ4MTMmcmFuaz0w?app_id=APPID&app_code=APPCODE",
                "id":"8409q8yy-b29e8557ae0d4a1d8f0d7b7f9c10a8f0",
                "references":
                {
                    "building":
                    {
                        "id":"9000000000000921352"
                    }
                }
            }
        ]
    },
    "search":
    {
        "context":
        {
            "location":
            {
                "position":[
                    37.7851,-122.4047
                ],
                "address":
                {
                    "house":"55",
                    "street":"4th St",
                    "postalCode":"94103",
                    "district":"Soma",
                    "city":"San Francisco",
                    "stateCode":"CA",
                    "county":"San Francisco",
                    "countryCode":"USA",
                    "country":"EE.UU.",
                    "text":"55 4th St<br/>San Francisco, CA 94103<br/>EE.UU."
                }
            },
            "type":"urn:nlp-types:place",
            "href":"http://demo.places.nlp.nokia.com/places/v1/places/loc-dmVyc2lvbj0xO3RpdGxlPTU1KzR0aCtTdDtsYXQ9MzcuNzg1MTtsb249LTEyMi40MDQ3O3N0cmVldD00dGgrU3Q7aG91c2U9NTU7Y2l0eT1TYW4rRnJhbmNpc2NvO3Bvc3RhbENvZGU9OTQxMDM7Y291bnRyeT1VU0E7ZGlzdHJpY3Q9U29tYTtzdGF0ZUNvZGU9Q0E7Y291bnR5PVNhbitGcmFuY2lzY287Y2F0ZWdvcnlJZD1idWlsZGluZw;context=Zmxvdy1pZD1iZDE0OTE1Yy1hMDAzLTU1OTEtYWU4ZC1iYjRlZjE3MGY0YmZfMTM2NDM2NjA5MTE4NV8wXzQ4MTM?app_id=APPID&app_code=APPCODE"
        }
    }
}

Nos encontramos ante una respuesta de tipo JSON. Para que sea fácil procesarla, podemos usar la librería NewtonSoft JSON, muy sencilla de utilizar y muy efectiva. Podemos instalarla usando NuGet. Una vez que la tengamos, crearemos la clase VMPlaces y la decoraremos con atributos para que pueda deserializar JSON de forma correcta:

public class VMPlace
{
    [JsonProperty("results")]
    public PlaceResult Result { get; set; }
}

public class PlaceResult
{
    [JsonProperty("next")]
    public string Next { get; set; }

    [JsonProperty("items")]
    public List<Place> Places { get; set; }
}

public class Place
{
    [JsonProperty("position")]
    public double[] Location { get; set; }

    [JsonProperty("title")]
    public string PlaceTitle { get; set; }

    [JsonProperty("distance")]
    public double Distance { get; set; }    
}

Simplemente creamos una jerarquía de clases que nos permita mapear los diferentes elementos y niveles del JSON. En este ejemplo está simplificado, nos faltarían más elementos y propiedades. Ahora ya podemos leer la respuesta usando HttpWebRequest y HttpWebResponse y mapear el JSON a nuestra clase usando NewtonSoft JSON:

if (result.IsCompleted)
{
    HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

    string resultJson;
    using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
    {
        resultJson = await reader.ReadToEndAsync();
    }
    if (!string.IsNullOrEmpty(resultJson))
    {
        VMPlace places = JsonConvert.DeserializeObject<VMPlace>(resultJson);
        taskCompletitionSource.SetResult(places);
    }
}

En primer lugar, obtenemos la respuesta con el método EndGetResponse de la petición HttpWebRequest que habíamos iniciado. A continuación obtenemos todo el texto usando un StreamReader sobre la Stream que devuelve el método GetResponseStream de HttpWebResponse. Una vez que tenemos nuestra string, solo tenemos que usar el método DeserializeObject de JsonConverter, indicando el tipo de objeto, y tendremos nuestra clase rellena con la información obtenida, ¿fácil, verdad?

Por último, ahora sí, llamamos al método SetResult de nuestro TaskCompletitionSource para terminar la tarea y hacer que devuelva el resultado de nuestra llamada al servicio. En definitiva, nuestro método GetNearbyPlaces quedaría de la siguiente forma:

public Task<VMPlace> GetNearbyPlaces(GeoCoordinate currentLocation)
{
    StringBuilder petitionUri = new StringBuilder("http://places.nlp.nokia.com/places/v1/discover/here?");
    petitionUri.AppendFormat("at={0},{1}", currentLocation.Latitude, currentLocation.Longitude);
    petitionUri.AppendFormat("&app_id={0}&app_code={1}&accept=application/json", APP_ID, APP_CODE);

    var taskCompletitionSource = new TaskCompletionSource<VMPlace>();
            
    HttpWebRequest request = HttpWebRequest.CreateHttp(petitionUri.ToString());
    request.BeginGetResponse(async result =>
    {
        if (result.IsCompleted)
        {
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

            string resultJson;
            using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
            {
                resultJson = await reader.ReadToEndAsync();
            }
            if (!string.IsNullOrEmpty(resultJson))
            {
                VMPlace places = JsonConvert.DeserializeObject<VMPlace>(resultJson);
                taskCompletitionSource.SetResult(places);
            }
        }
    }, null);

    return taskCompletitionSource.Task;
}

Ahora solo nos queda por ver como llamamos al método, para asegurarnos de que, de cara al desarrollador, se comporta como un método basado en Tasks normal y corriente:

var result = await this.hereService.GetNearbyPlaces(this.myPosition);

if (result != null)
{
    Places = result;
    this.IsBusy = false;
}

Como podemos ver, nuestro método GetNearbyPlaces se usa como un método async más, y no implica ningún trabajo extra a la hora de consumirlo. De la misma forma podemos ver que usar el API REST de Here Places es realmente trivial y nos aporta muchísima información para nuestra aplicación.

En este ejemplo usamos HttpWebRequest, que sigue el antiguo patrón async/completed en vez de WebClient, que tiene métodos async/await basados en Task. ¿Porqué? No es gratuito, ni simplemente para lucirme o por que me guste escribir más código. WebClient está bien, pero no tiene el mismo rendimiento que HttpWebRequest. HttpWebRequest es más rápido y efectivo, además de consumir menos memoria, que WebClient. Esto se debe a que WebClient en realidad es solo un wrapper que utiliza HttpWebRequest / HttpWebResponse para realizar la petición y devolvernos el resultado ya procesado. Personalmente, considero que HttpWebRequest no supone un trabajo complicado y que usándolo en conjunción con TaskCompletitionSource, podemos tener una gran potencia, solo por escribir algunas líneas de código más. Así que no lo olvides:

WebClient es Evil, usa HttpWebRequest 

Conclusión

Y hasta aquí llega el artículo de hoy. Creo que Nokia está haciendo un gran trabajo con su plataforma Here y este API nos lo demuestra, si profundizáis más en el, usando el API Explorer, encontraréis mucha información interesante y muy útil. Como siempre, a continuación tenéis el proyecto de ejemplo que hemos estado viendo en el artículo, totalmente funcional y usando el patrón MVVM, espero que lo disfrutéis:

Un saludo y Happy Coding!!

Un comentario sobre “Windows Phone 8: Here Places API”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *