Android SDK: Trabajando con Google Maps: Mostrar lugares de interés
Spanish (Español) translation by CYC (you can also view the original English article)
Con Google Maps en tus aplicaciones Android, puedes proporcionar a los usuarios funciones de localización, como información geográfica. A lo largo de esta serie, hemos estado desarrollando una aplicación para Android en la que la API v3 de Android de Google Maps se combina con la API de Google Places. Hasta ahora hemos mostrado un mapa, en el que el usuario puede ver su ubicación actual, y hemos enviado una consulta de Google Places para devolver datos sobre lugares de interés cercanos. Esto requirió configurar el acceso a la API para ambos servicios. En la parte final de la serie, analizaremos los datos de JSON de Google Places y los utilizaremos para mostrar al usuario los lugares de interés cercanos. También haremos que la aplicación actualice los marcadores cuando cambie la ubicación del usuario.
Esta es la última de cuatro partes en una serie de tutoriales sobre Usando Google Maps y Google Places en aplicaciones de Android:
- Trabajando con Google Maps - Configuración de la aplicación
- Trabajando con Google Maps - Configuración del mapa
- Trabajando con Google Maps - Integración de lugares
- Trabajando con Google Maps: Mostrando los lugares cercanos



1. Procesar los datos de lugar
Paso 1
Deberás agregar las siguientes instrucciones de importación a tu clase Activity para este tutorial:
1 |
|
2 |
import org.json.JSONArray; |
3 |
import org.json.JSONException; |
4 |
import org.json.JSONObject; |
5 |
import android.util.Log; |
En el último tutorial creamos una clase AsyncTask interna para manejar la obtención de los datos de Google Places en segundo plano. Agregamos el método doInBackground para solicitar y recuperar los datos. Ahora podemos implementar el método onPostExecute para analizar la cadena JSON devuelta desde doInBackground, dentro de su clase AsyncTask, después del método doInBackground:
1 |
|
2 |
protected void onPostExecute(String result) { |
3 |
//parse place data returned from Google Places
|
4 |
}
|
Paso 2
De vuelta a la segunda parte de esta serie, creamos un objeto Marker para indicar la última ubicación registrada del usuario en el mapa. También vamos a usar Marcadores para mostrar los lugares de interés cercanos. Utilizaremos una matriz para almacenar estos marcadores. En la parte superior de tu declaración de clase Activity, agrega la siguiente variable de instancia:
1 |
|
2 |
private Marker[] placeMarkers; |
Por defecto, la API de Google Places devuelve un máximo de 20 lugares, así que vamos a definir esto también como una constante:
1 |
|
2 |
private final int MAX_PLACES = 20; |
Cuando creamos los marcadores para cada lugar, usaremos los objetos MarkerOptions para configurar los detalles del marcador. Crea otra variable de instancia de matriz para estos:
1 |
|
2 |
private MarkerOptions[] places; |
Ahora instalemos la matriz. En el método onCreate de Activity, después de la línea en la que establecemos el tipo de mapa, crea una matriz del tamaño máximo requerido:
1 |
|
2 |
placeMarkers = new Marker[MAX_PLACES]; |
Ahora veamos el método onPostExecute que creamos. Primero, recorre la matriz Marker, eliminando cualquier marcador existente. Este método se ejecutará varias veces a medida que el usuario cambie de ubicación:
1 |
|
2 |
if(placeMarkers!=null){ |
3 |
for(int pm=0; pm<placeMarkers.length; pm++){ |
4 |
if(placeMarkers[pm]!=null) |
5 |
placeMarkers[pm].remove(); |
6 |
}
|
7 |
}
|
Cuando el código de la aplicación se ejecuta primero, se crearán nuevos marcadores. Sin embargo, cuando el usuario cambia de ubicación, estos métodos se ejecutarán nuevamente para actualizar los lugares mostrados. Por esta razón, lo primero que debemos hacer es eliminar cualquier marcador existente del mapa para prepararlo para crear un nuevo lote.
Paso 3
Utilizaremos los recursos Java JSON para procesar los datos de lugar recuperados. Dado que estas clases arrojan ciertas excepciones, debemos construir un nivel de manejo de errores a lo largo de esta sección. Comienza agregando los bloques para try y catch:
1 |
|
2 |
try { |
3 |
//parse JSON
|
4 |
}
|
5 |
catch (Exception e) { |
6 |
e.printStackTrace(); |
7 |
}
|
Dentro del bloque try, crea un nuevo JSONObject y pásalo al resultado cadena JSON devuelta por doInBackground:
1 |
|
2 |
JSONObject resultObject = new JSONObject(result); |
Si observas la página Búsqueda de lugar en la documentación de la API de Google Places, puedes ver una muestra de lo que realmente devuelve la consulta en JSON. Verás que los lugares están contenidos dentro de una matriz llamada "resultados". Primero recuperemos esa matriz del objeto JSON devuelto:
1 |
|
2 |
JSONArray placesArray = resultObject.getJSONArray("results"); |
Debes consultar el resultado JSON de muestra a medida que completamos cada sección de este proceso: mantén la página abierta en un navegador mientras completas el resto del tutorial. A continuación, instalemos la matriz MarkerOptions que creamos con la longitud de la matriz "resultados" devuelta:
1 |
|
2 |
places = new MarkerOptions[placesArray.length()]; |
Esto debería darnos un objeto MarkerOptions para cada lugar devuelto. Agrega un bucle para iterar a través de la matriz de lugares:
1 |
|
2 |
//loop through places
|
3 |
for (int p=0; p<placesArray.length(); p++) { |
4 |
//parse each place
|
5 |
}
|
Paso 4
Ahora podemos analizar los datos de cada lugar devuelto. Dentro del ciclo for, construiremos detalles para pasar al objeto MarkerOptions para el lugar actual. Esto incluirá latitud y longitud, nombre de lugar, tipo y cercanía, que es un extracto de los datos de dirección del lugar. Recuperaremos todos estos datos del JSON de Google Places, pasándoselo al marcador por el lugar a través de su objeto MarkerOptions. Si falta alguno de los valores en la fuente JSON devuelta, simplemente no mostraremos un Marcador para ese lugar, en caso de Excepciones. Para realizar un seguimiento de esto, agrega un indicador booleano:
1 |
|
2 |
boolean missingValue=false; |
Ahora agrega variables locales para cada aspecto del lugar que necesitamos recuperar y pasar al marcador:
1 |
|
2 |
LatLng placeLL=null; |
3 |
String placeName=""; |
4 |
String vicinity=""; |
5 |
int currIcon = otherIcon; |
Creamos e inicializamos un objeto LatLng para latitud y longitud, cadenas para el nombre del lugar y alrededores e inicialmente configuramos el ícono para usar el icono predeterminado dibujable que creamos. Ahora necesitamos otro bloque try, para que podamos detectar si realmente falta algún valor:
1 |
|
2 |
try{ |
3 |
//attempt to retrieve place data values
|
4 |
}
|
5 |
catch(JSONException jse){ |
6 |
missingValue=true; |
7 |
jse.printStackTrace(); |
8 |
}
|
Configuramos el indicador de valor flag en verdadero para verificar más tarde. Dentro de este bloque try, ahora podemos intentar recuperar los valores requeridos de los datos de lugar. Comienza por inicializar el indicador booleano en falso, suponiendo que no faltan valores hasta que descubramos lo contrario:
1 |
|
2 |
missingValue=false; |
Ahora obtén el objeto actual de la matriz de lugar:
1 |
|
2 |
JSONObject placeObject = placesArray.getJSONObject(p); |
Si miras hacia atrás en los datos de búsqueda de lugar de muestra, verás que cada sección de lugar incluye una sección de "geometría" que a su vez contiene una sección de "ubicación". Aquí es donde se encuentran los datos de latitud y longitud del lugar, así que recupéralo ahora:
1 |
|
2 |
JSONObject loc = placeObject.getJSONObject("geometry").getJSONObject("location"); |
Intenta leer los datos de latitud y longitud de esto, refiriéndose a los valores "lat" y "lng" en el JSON:
1 |
|
2 |
placeLL = new LatLng( |
3 |
Double.valueOf(loc.getString("lat")), |
4 |
Double.valueOf(loc.getString("lng"))); |
A continuación, obtén la matriz "types" que puedes ver en la muestra JSON:
1 |
|
2 |
JSONArray types = placeObject.getJSONArray("types"); |
Consejo: Sabemos que se trata de una matriz tal como aparece en la fuente JSON rodeada por los caracteres "["and"]". Tratamos cualquier otra sección anidada como objetos JSON en lugar de matrices.
Pasa por la matriz de tipos:
1 |
|
2 |
for(int t=0; t<types.length(); t++){ |
3 |
//what type is it
|
4 |
}
|
Obtener el tipo de string:
1 |
|
2 |
String thisType=types.get(t).toString(); |
Vamos a usar íconos particulares para ciertos tipos de lugares (comida, bar y tienda) así que agrega un condicional:
1 |
|
2 |
if(thisType.contains("food")){ |
3 |
currIcon = foodIcon; |
4 |
break; |
5 |
}
|
6 |
else if(thisType.contains("bar")){ |
7 |
currIcon = drinkIcon; |
8 |
break; |
9 |
}
|
10 |
else if(thisType.contains("store")){ |
11 |
currIcon = shopIcon; |
12 |
break; |
13 |
}
|
La lista de tipos para un lugar puede contener más de uno de estos lugares, pero para tu comodidad simplemente utilizaremos el primero encontrado. Si la lista de tipos para un lugar no contiene ninguno de estos, lo dejaremos mostrando el icono predeterminado. Recuerda que especificamos estos tipos en la cadena de consulta URL de búsqueda de lugar la última vez:
1 |
|
2 |
food|bar|store|museum|art_gallery |
Esto significa que los únicos tipos de lugares que utilizarán el icono predeterminado serán los museos o las galerías de arte, ya que estos son los únicos tipos que solicitamos.
Después del ciclo a través de la matriz de tipo, recupera los datos de proximidad:
1 |
|
2 |
vicinity = placeObject.getString("vicinity"); |
Finalmente, recupera el nombre del lugar:
1 |
|
2 |
placeName = placeObject.getString("name"); |
Paso 5
Después del bloque catch en el que estableces el indicador missingValue en true, verifica ese valor y establece el lugar MarkerOptions como nulo, de modo que no intentemos instanciar ningún objeto Marker con datos faltantes:
1 |
|
2 |
if(missingValue) places[p]=null; |
De lo contrario, podemos crear un objeto MarkerOptions en esta posición en la matriz:
1 |
|
2 |
else
|
3 |
places[p]=new MarkerOptions() |
4 |
.position(placeLL) |
5 |
.title(placeName) |
6 |
.icon(BitmapDescriptorFactory.fromResource(currIcon)) |
7 |
.snippet(vicinity); |
Paso 6
Ahora, al final de onPostExecute después de los bloques try y catch externos, recorre el conjunto de MarkerOptions, instanciando un marcador para cada uno, agregándolo al mapa y almacenando una referencia en el arreglo que creamos:
1 |
|
2 |
if(places!=null && placeMarkers!=null){ |
3 |
for(int p=0; p<places.length && p<placeMarkers.length; p++){ |
4 |
//will be null if a value was missing
|
5 |
if(places[p]!=null) |
6 |
placeMarkers[p]=theMap.addMarker(places[p]); |
7 |
}
|
8 |
}
|
El almacenamiento de una referencia al Marker nos permite eliminarlo fácilmente cuando se actualizan los lugares, como lo implementamos al principio del método onPostExecute. Ten en cuenta que incluimos dos pruebas condicionales cada vez que este ciclo se repite, en caso de que la búsqueda de lugares no devuelva los 20 lugares completos. También verificamos si MarkerOptions es nulo, lo que indica que falta un valor.
Paso 7
Finalmente, podemos instanciar y ejecutar nuestra clase AsyncTask. En tu método updatePlaces, después del código existente en el que construimos la cadena de consulta de búsqueda, inicia este procesamiento en segundo plano para obtener los datos de lugar usando esa cadena:
1 |
|
2 |
new GetPlaces().execute(placesSearchStr); |
Puedes ejecutar tu aplicación ahora para verla en acción. Debería mostrar tu última ubicación registrada junto con los lugares de interés cercanos. Los colores que ven en los marcadores dependerán de los lugares que se devuelvan. Aquí está la aplicación que muestra una ubicación de usuario en el centro de Glasgow, Reino Unido:



Tal vez, como era de esperar, muchos de los lugares que figuran en Glasgow son bares.
Cuando el usuario toca un marcador, verá el nombre del lugar y la información del fragmento:



2. Actualización con cambios de ubicación del usuario
Paso 1
La aplicación tal como está se ejecutará una vez cuando se lance. Construyamos la funcionalidad necesaria para que se actualice y refleje los cambios en la ubicación del usuario, refrescando los marcadores de lugar cercanos al mismo tiempo.
Altera la línea de apertura de la declaración de clase de actividad para que implementes la interfaz LocationListener y así poder detectar cambios en la ubicación del usuario:
1 |
|
2 |
public class MyMapActivity extends Activity implements LocationListener { |
Un receptor de ubicación puede responder a varios cambios, cada uno de los cuales usa un método dedicado. Dentro de la clase Activity, implementa estos métodos:
1 |
|
2 |
@Override
|
3 |
public void onLocationChanged(Location location) { |
4 |
Log.v("MyMapActivity", "location changed"); |
5 |
updatePlaces(); |
6 |
}
|
7 |
@Override
|
8 |
public void onProviderDisabled(String provider){ |
9 |
Log.v("MyMapActivity", "provider disabled"); |
10 |
}
|
11 |
@Override
|
12 |
public void onProviderEnabled(String provider) { |
13 |
Log.v("MyMapActivity", "provider enabled"); |
14 |
}
|
15 |
@Override
|
16 |
public void onStatusChanged(String provider, int status, Bundle extras) { |
17 |
Log.v("MyMapActivity", "status changed"); |
18 |
}
|
El único en el que estamos realmente interesados es el primero, el cual indica que la ubicación ha cambiado. En este caso llamamos al método updatePlaces nuevamente. De lo contrario, simplemente escribimos un mensaje de registro.
Al final del método updatePlaces, agrega una solicitud para que la aplicación reciba actualizaciones de ubicación:
1 |
|
2 |
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this); |
Usamos el Administrador de ubicaciones que creamos anteriormente en la serie, solicitando actualizaciones usando el proveedor de la red, con demoras de 30 segundos (indicadas en milisegundos), con un cambio mínimo de ubicación de 100 metros y la clase de actividad misma para recibir las actualizaciones. Por supuesto, puedes modificar algunos de los parámetros para satisfacer tus propias necesidades.
Sugerencia: Aunque el método requestLocationUpdates especifica un tiempo y una distancia mínimos para las actualizaciones, en realidad puedes causar que el método onLocationChanged se ejecute con mucha más frecuencia, lo que tiene serias implicaciones de rendimiento. En todas las aplicaciones que planeas liberar a los usuarios, debes limitar la frecuencia con la que el código responde a estas actualizaciones de ubicación. El método alternative requestSingleUpdate usado en un tiempo determinado puede valer la pena considerarlo.
Paso 2
Por último, debemos ocuparnos de lo que sucede cuando la aplicación hace una pausa y se reanuda. Anule los dos métodos de la siguiente manera:
1 |
|
2 |
@Override
|
3 |
protected void onResume() { |
4 |
super.onResume(); |
5 |
if(theMap!=null){ |
6 |
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this); |
7 |
}
|
8 |
}
|
9 |
|
10 |
@Override
|
11 |
protected void onPause() { |
12 |
super.onPause(); |
13 |
if(theMap!=null){ |
14 |
locMan.removeUpdates(this); |
15 |
}
|
16 |
}
|
Comprobamos el objeto GoogleMap antes de intentar cualquier procesamiento, como en onCreate. Si la aplicación hace una pausa, impediremos que solicite actualizaciones de ubicación. Si la aplicación se reanuda, comenzamos a solicitar las actualizaciones nuevamente.
Sugerencia: Hemos usado el LocationManager.NETWORK_PROVIDER varias veces en esta serie. Si estás explorando la funcionalidad de localización en tus aplicaciones, consulta el método getBestProvider alternativo con el que puedes especificar criterios para que Android elija un proveedor en función de factores como la precisión y la velocidad.
Antes de que terminemos
¡Eso prácticamente completa la aplicación! Sin embargo, hay muchos aspectos de la API v3 de Android de Google Maps que ni siquiera hemos tocado. Una vez que hayas ejecutado tu aplicación, puedes experimentar con características tales como rotación e inclinación. El servicio de mapas actualizado muestra mapas interiores y 3D en ciertos lugares. La siguiente imagen muestra la instalación 3D con la aplicación si la ubicación del usuario fue en Venecia, Italia:



Esto tiene el tipo de mapa establecido en normal: aquí hay otra vista de Venecia con el conjunto de tipos de mapas híbridos:



Conclusión
En esta serie de tutoriales, hemos trabajado en el proceso de integración de API de Google Maps y Google Places en una sola aplicación de Android. Manejamos el acceso a la clave API, configuramos el entorno de desarrollo, el área de trabajo y la aplicación para usar los servicios de Google Play. Utilizamos datos de ubicación, mostrando la ubicación del usuario junto con lugares cercanos de interés, y mostrando los datos con elementos personalizados de la interfaz de usuario. Aunque lo que hemos tratado en esta serie es bastante extenso, realmente es solo el comienzo cuando se trata de crear funciones de localización en las aplicaciones de Android. Con el lanzamiento de la Versión 2 de la API de Maps, las aplicaciones de Android están configuradas para llevar tales funciones al siguiente nivel.



