Mapas y capas de información

Tal y como explico en ¿Cómo monto mis páginas web de viajes? (II), antes empleaba una funcionalidad de Google Maps que permitía mostrar un fichero KML hospedado en mi propio servidor web. Nunca me ha gustado mucho depender de Google, pero en aquel momento no había mucho donde elegir.

Google eliminó esta funcionalidad en una revisión posterior de Google Maps. Por un lado, trabajo extra; por otro, explorar alternativas.

Ahora el criterio fundamental es desplegar una solución que pueda hospedar yo mismo y que, en caso necesario, pueda reemplazar con facilidad y sin tener que modificar mi web de forma extensiva.

Tras mucho tiempo investigando el asunto y preguntando a expertos en el tema, acabé curioseando la librería Follium. La idea es buena: seleccionar un mapa, añadir puntos de información, polígonos, líneas, etc., todo ello desde Python y al final exportar un mapa interactivo que se pueda incrustar una página web personal.

Tras evaluar Follium cuidadosamente, me pareció que no se ajustaba a mis necesidades. No obstance, conocí gracias a ella librería Javascript subyacente: Leaflet.js

Leaflet.js es una pequeña librería Javascript para visualizar mapas y múltiples capas de información de forma simple e interactiva. Además, interacciona muy bien con dispositivos móviles, una de sus grandes ventajas. En este artículo no voy presentar la librería, para eso tenéis su página web. Simplemente voy a explicar cómo integro esta tecnología en mi servidor.

Es muy simple:

  • En la raíz de mi servidor Zope, creo un script Python con el nombre mapa y el siguiente contenido:

    from Products.PythonScripts.standard import html_quote
    request = container.REQUEST
    response =  request.response
    
    titulo = ''
    contenedor = context.aq_parent
    if 'index.htm' in contenedor :
        titulo = contenedor['index.htm'].title
    
    if not titulo :
        titulo = contenedor.menu2(entry='')[0]
    
    print """<!DOCTYPE html>
    <head>
      <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
       <link rel="stylesheet" href="https://rawgit.com/andrewgiessel/leafletstuff/master/leaflet.css" />
       <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"></script>
       <script src='https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-omnivore/v0.2.0/leaflet-omnivore.min.js'></script>
       <style>
          html, body {
            width: 100%%;
            height: 100%%;
            margin: 0;
            padding: 0;
          }
    
          #map {
            position:absolute;
            top:0;
            bottom:0;
            right:0;
            left:0;
          }
       </style>
       <title>%s</title>
    </head>
    <body>
       <div id="mapa" style="width: 100%%; height: 100%%"></div>
       <script>
          var base_tile = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 18,
              minZoom: 1,
              attribution: 'Map data (c) <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
          });
    
          var OpenCycleMap_base_tile = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', {
              maxZoom: 18,
              minZoom: 1,
              attribution: '&copy; <a href="http://www.opencyclemap.org">OpenCycleMap</a>, &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
          });
    
          var baseLayer = {
            "Mapa Open Street Map": base_tile,
            "Mapa Open Cycle Map": OpenCycleMap_base_tile
          };
    
    
          /*
          list of layers to be added
          */
          var layer_list = {
    
          };
    
          /*
          Bounding box.
          */
          var southWest = L.latLng(-90, -180),
              northEast = L.latLng(90, 180),
              bounds = L.latLngBounds(southWest, northEast);
    
          /*
          Creates the map and adds the selected layers
          */
          var map = L.map('mapa', {
                                           maxBounds: bounds,
                                           layers: [base_tile]
                                         });
    
          L.control.layers(baseLayer, layer_list, {collapsed: false}).addTo(map);
    
          var kml = omnivore.kml('%s')
              .on('ready', function() {
                      kml.setStyle({color: "#0F0", opacity: 0.7});
                  map.fitBounds(kml.getBounds());
                  })
                      .addTo(map);
    
          L.control.scale().addTo(map);
       </script>
    </body>
    """ %(titulo, context.absolute_url())
    
    return printed
    

    Este código utiliza la magia de la adquisición de Zope. Es una técnica muy peligrosa. De hecho, los frameworks modernos no la emplean. Pero, en este caso, nos simplifica mucho la vida.

    Las líneas 5-11 intentan determinar qué título debe aparecer en la página del mapa. En las líneas 16-18 incluímos el código Javascript de Leaflet.js y la extensión Omnivore. Observa que indico versiones concretas de las librerías para evitar problemas. Puedo actualizar las versiones fácilmente tras probarlas en otra página. También podría, llegado el caso, hospedar yo mismo esas librerías en mi servidor web.

    Las líneas 40-44 y 46-49 especifican los mapas que vamos a utilizar. En mi caso, OpenStreetMap y OpenCycleMap. Los dos son libres y el segundo incluye curvas de nivel, lo que me viene muy bien para mis rutas de senderismo.

    En las líneas 82-87 cargo la ruta KML y la añado como capa adicional al mapa. Para eso uso la extensión Omnivore de Leaflet.js. No tengo problemas con la protección Same Origin de los navegadores porque, si bien cargo las librerías desde servidores externos, el origen de la página que estoy cargando es el mismo que el fichero KML.

    Ajusto simultaneamente el nivel de ampliación y la posición a visualizar en la línea 85. La línea 89 añade una escala al mapa, algo que necesito para poder estimar distancias de senderismo, etc.

  • Como Omnivore no soporta KMZ de forma nativa, recorro mi servidor web convirtiendo mis ficheros KMZ a KML. El código es muy simple. Es un script Python en el directorio raíz de mi servidor Zope con el siguiente contenido:

    def arbol(obj,path) :
      v = []
      for i in obj.objectValues() :
        id=i.getId()
        m=i.meta_type
        if id.endswith('.kml') or id.endswith('.kmz') :
            v.append(path+"/"+id)
        if ((m=="Folder") or (m=='Folder (Ordered)')) and (id!="temp_folder") :
            v.extend(arbol(i,path+"/"+id))
      return v
    
    print "Buscar ficheros KML/KMZ..."
    print
    
    for i in arbol(context,"") :
        print i
    
    print
    print "Operación completada."
    
    return printed
    

    Es una búsqueda recursiva básica.

Misión cumplida. Ahora, cuando quiero añadir el enlace a un mapa, pongo un enlace de esta forma: PUNTOS.MKL/mapa. Por ejemplo: https://www.jcea.es/pics/italia2015/italia2015.kml/mapa. Esto carga el mapa, añade una capa de información encima con la ruta KML y nos permite interactuar con él.

Si en algún momento quiero cambiar mi tecnología de mapas, basta con que modifique el fichero mapa en el raíz de mi servidor Zope. Cualquier alteración en ese fichero afectará automáticamente a todos los mapas de mi servidor web. Justo lo que quiero.