Rails-geocoder. Geocodificación sencilla en Ruby on rails

La geolocalización está de moda. Redes sociales como foursquare o facebook places están en boca de muchos por el potencial nuevo negocio de la publicidad local. Y geolocalización y geocodificación van casi siempre de la mano. Pero no hace falta enfrascarse en un proyecto puramente geográfico para sacar partido a la geocodificación.
La geocodificación consiste en el proceso de obtener las coordenadas geográficas a partir de la dirección detallada, usando para ello un servicio de geocodificación de los muchos que hay disponibles que hará una búsqueda y nos devolverá la latitud y la longitud. Google maps provee uno, y es el que vamos a usar.
La geocodificación inversa es investigar el nombre de un lugar sabiendo sus coordenadas.
Voy a plantear un proyecto muy sencillo.

Supongamos que un ayuntamiento, o delegación provincial del gobierno desea ofrecer a sus sufridos contribuyentes, en la página de su servicio de salud, información sobre las farmacias de guardia. Para este ejemplo voy a suponer que por algún extraño motivo divino la base de datos del servicio de salud tiene información correcta y actualizada sobre la dirección detallada de las farmacias.

La idea es ofrecer un servicio para localizar la farmacia de guardia más cercana, y lo haremos basándonos en cuáles están más cerca en linea recta. En el mundo real sería más correcto calcular los trayectos según el callejero, porque la mejor elección no siempre se corresponde con la más cercana geográficamente hablando.

Rails-geocoder es un plugin que nos permite en apenas un par de pasos añadir geocodificación a nuestros modelos, y además realizar algunos cálculos con ellos, como distancias. No requiere tampoco el uso de una base de datos específica con funciones espaciales para ello. El plugin es obra de Alex Reisner, y se encuentra alojado aquí.

El proceso de instalación es el habitual. Se inserta la siguiente linea en el archivo Gemfile.

gem "rails-geocoder", :require => "geocoder"

A continuación invocamos el comando bundle install

Bien. Ahora, si pretendemos geocodificar, debemos crear un par de campos en la BD para almacenar latitud y longitud. Los llamaré lat y lng. Creo una migración para añadir los campos.

class AddLatLng < ActiveRecord::Migration
  def self.up
    add_column :farmacias,:lat,:float
    add_column :farmacias,:lng,:float
  end
  def self.down
    remove_column :farmacias,:lat
    add_column :farmacias,:lng
  end
end

A continuación aplicamos la migración (rake db:migrate) y le comunicamos dos cosas al plugin.
La primera es que campo alberga la información para geocodificar. La segunda es que dos campos van a albergar la latitud y la longitud (si no le decimos nada, asume que se llaman latitude y longitude).

Supongamos el modelo farmacia con campos nombre,provincia, localidad, calle y numero, todos de tipo string.

No existe un campo que almacene toda la dirección, porque está dividida, pero creamos un método llamado address que concatene número, calle, localidad, provincia y “España”. El modelo queda así:

class Farmacia < ActiveRecord::Base   geocoded_by :address, :latitude  => :lat, :longitude => :lng    # RAILS-GEOCODER

  def address
    [calle, numero, localidad, provincia, "España"].compact.join(', ')
  end
end

Hecho esto, siendo farm un objeto de clase Farmacia, el método fetch_coordinates nos devuelve sus coordenadas. Como nos interesa almacenarlas para siempre, desde la consola interactiva de rails ejecutamos la versión bang (modificador) de ese mismo método sobre la colección de todas las farmacias.
Invocamos rails console , y mandamos geocodificar todas las farmacias.

ruby-1.9.2-p0 > farmacias = Farmacia.all
#=> [array de todas las farmacias de la BBDD]
ruby-1.9.2-p0 > farmacias.each { |farm| farm.fetch_coordinates! }
#=> [array con todas las farmacias, pero con los campos lat y lng completados (O deberían)]

Llevará un rato según el numero de farmacias y la velocidad de conexión, pero con esto deberíamos tener ya almacenadas todas las coordenadas de las farmacias de la base de datos, para poder empezar a pensar en calcular distancias.
En una versión reciente (o eso creo, porque no tenía constancia de ello hasta ahora que he repasado la documentación del plugin) se ha añadido una tarea rake para geocodificar todos los modelos de una clase con una sola linea, de esta manera rake geocode:all CLASS=Farmacia.
De todas formas viene bien saber usar la consola por si quisiéramos geocodificar solo las farmacias de una determinada localidad, o las que cumpliesen algún otro filtro.

 

En cualquier caso, llegados a este punto, hemos obtenido las coordenadas geográficas todas las farmacias de la provincia (o de el país) en unos sencillos pasos.

Ahora toca hacer el cálculo de las farmacias cercanas. Si el usuario nos dice su dirección, seguiríamos los mismos pasos para obtener las coordenadas del usuario. Vamos a suponer que el usuario usa la aplicación desde un smartphone con GPS, y ese dato nos lo facilita él.

No existe una forma directa y simple de decir “encuéntrame el más cercano”. El plugin nos ofrece eso si la posibilidad de obtener los objetos cercanos en un radio X (en millas, eso sí) a la redonda. En ese caso, la mejor forma de proceder es establecer radio de búsqueda arbitrario lo suficientemente amplio, por ejemplo 20km. Malo será que no se tenga una farmacia de guardia en 20km a la redonda. Esto nos ofrece las farmacias en el radio de búsqueda, pero no sabemos cuales son las más cercanas. Para ordenarlas por distancia, debemos buscarnos la vida. Por suerte el plugin nos ofrece la función distance_to, que calcula la distancia entre dos puntos geográficos según la fórmula de Haversine para calculo de distancias en un geoide, y la podemos usar en un bloque de ordenación. Así sería.

farm_cercanas = Farmacia.near([user.lat,user.lng], 20*0.62).all # 20km * 0,62 millas/km para cambiar unidades
farm_cercanas = farm_cercanas.sort_by { |f| f.distance_to( user.lat, user.lng ) }

Ya tenemos una lista de las farmacias en un radio de 20km, ordenadas por cercanía al usuario. Si a la query de las farmacias le hubiésemos añadido una clausula WHERE para filtrar las que están de guardia ya nos queda bordado.

 

Hay plugins más potentes (y complejos) que rails-geocoder, pero siento una preferencia hacia este por lo sencillo de su funcionamiento y porque mantiene un desarrollo activo, estando actualizado para rails3. Yo mismo lo estoy usando ahora mismo en un proyecto relativamente complejo, con muy buen rendimiento incluso con gran cantidad de objetos codificados.

Aunque si nos interesa el rendimiento y no forzar a nuestro servidor, hay otra opción. En el caso de las farmacias no tiene importancia, porque son las que son y se actualizan relativamente poco. Pero si fuese necesario geocodificar cada usuario, sería más pesado. ¿Porqué obligar a nuestra aplicación a geocodificar cada dirección que recibe, cuando puede hacerlo el propio usuario en su máquina, con un poco de javascript? En la próxima entrada comentaré esta opción.
Un saludo, y hasta la próxima.

Anuncios

, , ,

  1. #1 por Josue el junio 29, 2011 - 11:12 pm

    hola amigo, ando trabado en un paso. estoy justamente haciendo un proyecto parecido al ejemplo, entonces quisiera saber de qué manera agregarias la consulta WHERE al ejemplo de las farmacias, por que que le busco no me queda. Salu2.

    • #2 por miguelcamba el junio 30, 2011 - 8:33 am

      Pues simplemente, en rails 3, los scopes son encadenables.

      Farmacias.where(:id => [12..50]).near([user.lat, user.lng], 20 * 0.62)

      Un saludo

  2. #3 por Emmanuel el marzo 18, 2012 - 7:56 am

    Como sacas el user.lat y el user.Ing de algun visitante al sitio

    Lo eh intentado con gemas que te dan la latitud y logitud por medio de la ip pero son muy inexactas (GeoKit y geo_location)

    Me gustaria utilizar la API de Html5 pero no se como pasar los datos que saco con javascript al lado al Controller

    • #4 por miguelcamba el marzo 18, 2012 - 9:24 am

      En el ejemplo asumí que ya se habían pasado. Yo la posición de un usuario la saco por el API de HTML5.
      Una vez tienes esa posición en javascript, la puedes enviar al servidor con una llamada ajax para almacenarla. También puedes usar cookies, pero no me convence.
      Puedes almacenarla dentro del usuario, como en este ejemplo, pero conceptualmente es más correcto almacenarla en la sesión, porque cada vez el usuario se puede conectar desde un lugar diferente. Si lo haces dentro del usuario, deberías expirar ese campo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: