Reimplementando Google Maps con OpenStreetMap

Publicado el Viernes, 13 de febrero de 2015

OSM en móvilEl título de este post es pretencioso, pero una buena forma de llamar la atención al tema. Usar Google Maps tiene varias desventajas, siendo la primera la falta de privacidad: Google ya tiene más que suficiente información sobre nosotros, por lo que cuanto más servicios de Google podamos reemplazar por alternativas, mejor. Por otro lado, los números de puerta de Montevideo están todos (muy) mal en las calles de Google Maps.

Al rescate viene OpenStreetMap: proyecto colaborativo para crear un mapa libre y editable del mundo. Es la Wikipedia de los mapas. Siendo colaborativo, crowdsourceado y siendo sus datos compartidos libremente con la comunidad, parecería hasta tonto usar una iniciativa privada y propietaria.

Al compartir una dirección, generalmente uso OSM, sobretodo en Montevideo, Uruguay. Parte de los datos de los mapas de Montevideo son provistos por la Intendencia de Montevideo. Los números de puerta existen, ¡y están donde deben estar!

Ahora, ¿cómo lograr algo parecido a Google Maps para compartir una dirección? Ya existe el sitio de OSM, pero quería aprender qué tan fácil (o difícil) era implementar algo con estos mapas en la web.

Decidí empezar con Leaflet.js, una biblioteca JavaScript de código abierto para mapas interactivos. Tiene un montón de características, funciona en la mayoría de los navegadores aprovechando HTML5 y CSS 3 (cuando están disponibles), la API está bien documentada y cuenta con bastantes plugins. Sumamente sencilla, liviana y viene siendo suficiente para todo lo que he necesitado hacer hasta ahora.

Lo primero fue lograr mostrar el mapa en un HTML simple. Creé el archivo index.html, enlacé a los CSS y JavaScripts indicados y arranqué entonces con el código de ejemplo de su sitio web:

// crear un mapa en el div con id "map", setear la vista con coordenadas y un zoom
var map = L.map('map').setView([51.505, -0.09], 13);
 
// agregar una capa de OpenStreetMap
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

El problema con este código es que nos muestra un mapa de Londres. Mi idea era que mostrara el lugar aproximado donde se encuentra el usuario por medio de geolocalización. Así que empecé a buscar entre los plugins de Leaflet y me encontré con Leaflet.GeoIP, un plugin para encontrar la posición aproximada de la máquina del cliente basados en la IP.

Centrar el mapa en la ubicación del cliente con este plugin fue cuestión de un par de líneas. Ya con esto fui familiarizándome un poco con la API y la forma en que interactúan sus elementos.

Todo lo que comento en este post se puede ver en esta página con un mapa funcional. El código fuente está disponible en GitHub.

Marcadores

Lo siguiente que hice fue implementar un Marker. Se trata de los conocidos íconos que vemos para marcar un lugar en un mapa web. Quería que al hacer click en un lugar en el mapa, se mostrara el marcador y al hacer clic sobre él desapareciera. Ya que estaba le agregué también un mensaje Popup que mostrara las coordenadas del lugar donde se estaba agregando el marcador. El código habla por sí solo:

marker = L.marker([0,0], {draggable: true});
map.on('click', function(e){
  marker.setLatLng(e.latlng).addTo(map);
  marker.bindPopup("" + e.latlng).openPopup();
});
 
marker.on('click', function(){
  map.removeLayer(marker);
});

El objeto Marker es bastante flexible por lo que podemos ingresar imágenes personalizadas y comportamientos variados en distintos eventos.

Otro aspecto sencillo del que se encarga bastante bien el framework es de hacer que nuestros mapas se vean bien y sean funcionales en plataformas móviles. Incluyen un tutorial en el que vemos que no hay que hacer nada del otro mundo para que así sea.

Búsqueda de direcciones

Sabiendo que OpenStreetMap cuenta con datos tan confiables de direcciones, faltaba la característica que hiciera que mis mapas sirvieran más que Google Maps: buscar direcciones y mostrarlas bien. Salí nuevamente a buscar un plugin que me ayudara con esto, y me encontré con Leaflet.GeoSearch.

Al igual que Leaflet.js, el plugin de GeoSearch es independiente del proveedor, por lo que podemos usar varios: Bing, Google, Esri, Nokia y OSM. En mi caso obviamente incluí solo el de OpenStreetMap. Acá también me metí a personalizar el ícono de un marcador, para mostrar un marcador distinto al encontrar una dirección, y ya aprendí algo más.

El resultado:
OSM - Buscar dirección

Geolocalización

Como comentaba más arriba, empecé usando un plugin para la geolocalización del cliente. Pero mirando más la documentación y ejemplos de Leaflet, encontré que podía usar el Geolocation API del navegador a través del método locate. Este método dispara dos eventos: locationfound y locationerror al obtener la localización o fallar. El API de Geolocalización de los navegadores es eso que pregunta al usuario si quiere compartir su ubicación con el sitio web actual.

Así que escribí el código de forma que se usara locate para centrar el mapa en un lugar cercano al cliente, y en caso de fallar al intentar obtener la ubicación, seguir usando el plugin (mucho menos preciso al ser solo por IP) como alternativa.

OSM compartir ubicación

Un problema interesante con el que me encontré acá fue el siguiente: Firefox no dispara ningún evento si el usuario elige la opción "No ahora" (o "Not now"). Si comparte la ubicación, se lanza el evento locationfound, si elige "no compartir nunca" se lanza locationerror, pero si elige "No ahora", ¡no pasa nada!

No es así con otros navegadores, y está reportado como un bug en Bugzilla, esperemos que eventualmente se arregle. El tema es que para darme cuenta si el usuario había elegido "Not now" tuve que escribir un hack medio feote, agregando una bandera y un timeout para usar el plugin de todas formas mientras tanto:

var loadedLocation = false;
 
if( isFirefox ){
  navigator.geolocation.getCurrentPosition(firefox_success, firefox_error);
  setTimeout(function(){
    if( !loadedLocation ){
      use_geoip_plugin();
    }
  }, 3000);

La variable loadedLocation se setea en true en la función firefox_success. Pueden ver todo el código que vengo comentando en GitHub.

Compartir URLs

Ahora un paso más para hacer que el mapa sea útil: compartir direcciones. Para esto, agregué las coordenadas de longitud y latitud como parámetros en un link dentro del Marker con Popup que ya había creado. Después me fijo si están los parámetros al entrar al sitio y si es así posiciono el marker ahí. Todo esto con lo aprendido hasta el momento de la API, y unas lindas expresiones regulares, ¿qué puede salir mal?

OSM Compartir URL

Quedé sorprendido de lo fácil que fue armar algo con estos datos gracias a la cantidad de herramientas que ya hay disponibles. No me llevó demasiado tiempo tener algo funcional y terminó quedando prolijo. Lo único que hay que hacer es leer cómo funciona la API e ir agregando funcionalidad o características según nuestras necesidades. No puedo enfatizar lo suficiente lo fácil de usar que es Leaflet.js, tómense el tiempo de probarlo.

Podemos hasta embeber un mapa en un iframe en otra web, ¡yay iframes!

Espero que este post los aliente a considerar usar OpenStreetMaps en vez de una alternativa propietaria si alguna vez tienen que desarrollar algo con mapas. Por mi parte seguiré experimentando a medida que se me vayan ocurriendo ideas nuevas a implementar.

24 comentarios en este post

Feed de comentarios
  1. Avatar

    Pablo Cardozo 13 febrero. 2015 - 13:14

    Muy buenos los comentarios sobre la implementación de OSM, y te quedó re prolijo 😉

    Yo soy un apasionado de todo lo que tenga que ver con mapas y desde hace mucho que intento hacer todo con OSM, aunque en mi trabajo no tuve más remedio que acudir a opciones de servicios que ofrecen cosas ya más armadas para poder poner más información en las burbujas de información, pero lo tengo pendiente el poder hacerlo más manual y que sea con OSM.

    • Avatar

      Fernando 13 febrero. 2015 - 17:34

      Gracias 😀

      Posiblemente hayan cosas que se pueden hacer con otros servicios y no con OSM (todavía), pero para estos casos creo que está sobrado. Pienso seguir investigando de hacer cosas nuevas igual. Por ahora no me ha tocado implementar nada de esto en el trabajo, creo que alguna vez embebí un Google Maps en algún sitio, pero nada muy complejo. Pero cuando me toque, ya tengo algo de preparación 🙂

      Gracias por comentar, ¡saludos!

  2. Avatar

    Ismael 17 abril. 2015 - 18:13

    Interesante el post. En el google maps por defecto las direcciones son erróneas, sin embargo en la siguiente web: http://www.pedidosya.com.uy/ las direcciones parecen estar todas correctas dentro de Montevideo. por lo que te consulto capaz que sabes ¿cómo cargar los datos de OSM o el SIG de la IMM en el google maps? Porqué el Googlemaps? por su agradable y sencilla interfaz.
    Resumiendo saber como hacer para que la busqueda arroje las direcciones correctas para cada destino ingresado.

    Muchas gracias.

    • Avatar

      Fernando 13 septiembre. 2015 - 21:15

      Hola,
      No sé cómo usar bibliotecas de Google Maps con Open Street Maps. El motivo de este post es precisamente mostrar una alternativa para no usar Google Maps y usar algo un poco más libre y colaborativo. De todas formas supongo que algo debe haber para la interacción entre estas dos bibliotecas.

      ¡Saludos!

  3. Avatar

    Albert 2 febrero. 2016 - 07:55

    Buenos dias Fernando,

    Primero de todo felicidades por este post. Después de investigar un poco he implementado un mapa con osmap, y todo perfecto. El problema es que al ejecutarlo en el movil, el siguiente evento no me funciona:

    marker.on(‘click’, function(){
    alert(“click()”);
    });

    He buscado mucho y no doy con la tecla para solucionarlo. Te agradeceria mucho la ayuda Fernando, gracias!

    • Avatar

      Fernando 2 febrero. 2016 - 12:10

      Hola Albert,
      ¿Qué sistema operativo y navegador estás usando en el móvil? Usando este ejemplo en Android con el navegador web del sistema, el evento ‘click’ parece registrarse bien. ¿Funciona en la computadora?

      Me fijé en los issues de GitHub y no parece haber mención de este asunto, pero capaz que encontraste un bug. Probaría con otro navegador en principio, para descartar ese problema, y después ver de repente si hay un problema con los alert o probar algún otro evento. Es lo que se me ocurre ahora,
      Saludos!

      • Avatar

        Albert 3 febrero. 2016 - 12:51

        Hola Fernando,
        Gracias por tu rapida respuesta.
        En el ejemplo que me muestras, sí me funciona el evento click.

        Creo que el problema viene porque estoy usando un plugin para poner labels:
        https://github.com/Leaflet/Leaflet.label

        Y un ejemplo demo de este:
        http://leaflet.github.io/Leaflet.label/

        En este ejemplo, me vuelve a fallar el evento click. A lo mejor es que sobreescribe algun metodo 🙁

        Si me puedes ayudar te lo agradeceria mucho. Des de ya muchas gracias.

        PD: Utilizo Android(v5.1.1) y Google Chrome(v47).

        • Avatar

          Albert 3 febrero. 2016 - 14:18

          Buenas de nuevo Fernando,

          Creo que el plugin que te dicho sobreescribe el evento click del marker, ya que el label también puede escuchar eventos, y en el caso del click funciona correctamente en el mobil.

          No se que codigo tengo que modificar para eliminarlo/deshabilitarlo, si tienes alguna sugerencia 🙂

          • Avatar

            Fernando 9 febrero. 2016 - 12:34

            Mmmh, se me ocurre que mirando el código de Leaflet.label encuentres una forma de sobreescribir su función para click o reimplementes esa función y la sobreescribas desde tu propio código. ¿Se entiende?

  4. Avatar

    Alex 14 marzo. 2016 - 15:27

    Hola, gracias por tu codigo. Lo estoy usando para trastear con él y tengo un problema.

    He editado y he añadido metodos para que coja la direccion desde una base de datos y me lo añada en el mapa. Pero va de uno en uno, es decir, he de seleccionar una direcion, y darle a cargar al mapa, cambiar, y otra. No hay forma de que me coja bien todas las direcciones y me las muestre bien.

    He ido las notas de github y no hay forma, he probado con un bucle que coja dinamicamente los valores de la tabla pero solo me muestra el primero aunque le meta a piñon el metodo(direccion), solo lee el primero y los demas nada de nada. A ver si se te ocurre a ti algo. Gracias.

    • Avatar

      Fernando 14 marzo. 2016 - 15:35

      Viendo un poco del código que escribiste se me haría más fácil. Se me ocurre que en el bucle donde vas recorriendo las direcciones de la base de datos, estés definiendo la longitud y latitud en un mismo marker, y por eso siempre queda uno. No sé si va por ahí la cosa, pero en el bucle deberías instanciar y agregar al mapa un marcador nuevo por cada dirección. Algo así:

      // encabezado del bucle 
      // for (var i = 0; i < direcciones.length; i++) {
        marker = L.marker([0,0], {draggable: false});
        marker.setLatLng(direcciones[i].latlng).addTo(map);
      }

      No sé si queda claro, pero se me ocurre que por ahí puede venir la mano. O directamente hacer marker = L.marker(direcciones[i].latlng).addTo(map);.

      Si podés compartir un poco del código de repente lo puedo ver mejor y se me ocurre qué puede estar pasando.

      Suerte!

      Saludos,

      • Avatar

        Alex 15 marzo. 2016 - 09:01

        Gracias por responder. Lo que me dices ya se me habia ocurrido, pero necesito que el mapa me busque la direccion que yo quiero.

        Lo que tu me comentas es para algo parecido a que hicieras clic en el mapa, pero yo quiero que dada una direccion desde la base de datos, la busque, y la guarde en el mapa. La idea del diferente marker, esta bien, pero el marker del buscador es la L, L.(argumentos) crea todo, pero no se como crear otro más que tenga todo exactamente igual, el color, opciones… y aparte tambien tengo otro problema.

        Me he creado una funcion onmark así:

        _onMark:function(){
         
                    var datos = [];
                    for (var i = 1;i&lt;tabla.rows.length;i++){
                    datos[i] = $('td[id="i"]').text();
                    }
                    this.geosearch(datos);
            }

        Basicamente, guarda las direcciones en un array, pero cuando paso las direcciones, o no me las coje, o se queda en un bucle infinito. He probado en la funcion geosearch pasarle las direcciones desde el propio array, pero se me queda en bucle infinito.

        Lo siento por marearte tanto, pero hay cosas que se me escapan, muchas gracias 🙂

        • Avatar

          Fernando 17 marzo. 2016 - 10:59

          Bien, creo que le encontré la vuelta. Lo que tienes que hacer en tu código es instanciar un “search provider” y agregar un marker con el provider por cada dirección. Esto está abajo del todo en el readme del plugin. Un detalle que señalan es que esta funcionalidad está disponible únicamente con el proveedor de Google por el momento.

          Ojo también que hay un par de problemas con el geocoder. A mí me daba error “undefined geocoder” en algunos casos y en otros no. Para evitar este error, hice un hack medio raro en las pruebas:

          var searcher = new L.GeoSearch.Provider.Google();
           
          try{
            searcher.GetLocations('test', function(data){
              console.log('Llamada de prueba');
            });
          } finally {
            console.log('Falló la primera llamada, esperable (a veces)');
          }

          Eso es si te falla, sino, con algo así debería funcionar:

          ['Bulevar Espana 2529', 'Julio Herrera y Reissig 565'].forEach(function(address){
            searcher.GetLocations(address, function(data){
              var marker = L.marker([data[0].Y, data[0].X]);
              marker.bindPopup(data[0].Label).openPopup();
              marker.addTo(map);
              console.log(data);
            });
          });

          El array con “Bulevar España 2529” y “Julio Herrera y Reissig 665” no es más que un array con direcciones, que en tu caso vienen de la base de datos. Como ves, llamo al searcher que instancié en el bloque de código más arriba (new L.GeoSearch.Provider.Google();) y agrego un marker en las coordenadas que recibo con un pop up con la dirección.

          Espero que con esto puedas resolver el asunto.

          ¡Saludos!

  5. Avatar

    Walter 30 octubre. 2016 - 11:33

    Hola Fernando. Muy bueno tu trabajo. Hace muchos años fui programador , y aunque ahora me dedico a otra cosa, he estado implementando un mapa con la direcciones de mis clientes. Quisiera saber si existe alguna api o alguna idea orientativa que sirva para lo siguiente. Una vez mostrados un grupo de marcadores en el mapa , poder de alguna manera seleccionar una zona con mouse y que me devuelva la info de los marcadores que quedaron seleccionados. Saludos a todos.

  6. Avatar

    Charlie 20 diciembre. 2016 - 22:27

    Estimado Amigo!

    Estoy dando mis primeros pasos en Leaflet, y queria consultarte si usted a encontrado manera de captar la geolocalizacion con mayor precision ya que la funcion locate() de leaflet me da un error de varios metros. No obstante Google Maps tampoco es exacta pero es mucho mas cercana.

    Agradecería alguna recomendación; talvez hay manera de lograr una geolocalizacion mas precisa o en todo caso semejante a la Google Maps, pero con uso de tecnología open source.

    Gracias

  7. Avatar

    ESME 18 noviembre. 2020 - 14:02

    Hola Fernando, estoy innovandome en el mundo de la programacion y desde hace unos meses trabajo en Apex 20.1, la cuestion es que estoy tratando de agregar unos marcadores al Mapa con Open Street Maps, pero que los datos sean desde una tabla de mi BD. Podrias orientarme de como hacerlo?.
    muchas gracias de antemano.

Dejar un comentario

Toasty!