Caminante no hay camino, se hace camino al… programar

En la entrada anterior ya habéis visto uno de los resultados tangibles de mi último viaje a Venecia. No era mi primer viaje a la ciudad (llevo ya 8); espero que tampoco sea el último.

Ya os comenté que visitar los lugares que aparecen relacionados con Corto Maltés era el objetivo secundario de mi viaje. El objetivo principal era fotografiar todos los puentes de la Serenísima.

Para acometer un proyecto tan ambicioso tenía que planificar con exquisito cuidado el proyecto.

Lo primero era identificar todos los puentes (mi idea inicial era fotografiar solo los de Venecia, dejando a un lado las islas de la Laguna).

Para ello me planteé las siguientes preguntas:

  • ¿Cuántos puentes hay en Venecia?
  • ¿Dónde se ubican esos puentes?
  • ¿Qué camino elegir para recorrerlos?
  • ¿Cuántos días necesitaré para recorrerlos?
  • ¿Podré fotografiarlos todos? ¿Habrá un lugar desde donde pueda fotografiarlos?
  • ¿Cómo saber que no me dejo ninguno?
¿Podré responder a esas preguntas?

Identificando los puentes de Venecia

La primera opción para responder a la primera pregunta era la IA, Google o la Wikipedia.

La IA de ChatGPT te responde que aproximadamente hay 391 puentes, que según unas fuentes hay 435, que según otras hay 438…

Vamos, que parece gallega.

Google tampoco tiene una respuesta exacta a esa pregunta.

Wikipedia resultó ser el mejor punto de inicio para responder a esa pregunta. Existe una página que se llama Lista de Puentes de Venecia que incluso lleva una foto de cada uno de ellos. Según esa página hay 435 puentes (ya os adelanto que ese valor no es exacto).

Pero era un buen comienzo. La pena es que no venían las coordenadas de los puentes, pues aunque en algunos casos venía un enlace con coordenadas, este no era correcto.

Mi primera tarea fue generar un mapa de Google Maps con ese listado y ubicarlos con precisión. Tuve que confirmar que la foto que aparecía en la Wikipedia era del puente que se describía. Para ello empleé Google Street, que te permite ver los alrededores de una ubicación. Y ahí ya me di cuenta de que las contratas de Google que realizan la grabación de trayectos no tenían, en algunos casos, vistas de algunos puentes.

Como no me fiaba de esa información, recorrí la vista de satélite de Google buscando posibles puentes privados que no aparecieran en esa página (y sí, aparecieron algunos).

Cuando ya había avanzado en el software que os comentaré más adelante encontré una página web que me fue de gran utilidad.

Veniceprojectcenter permite saber dada unas condiciones de marea por qué puentes se puede pasar o no. Si sube la marea, el vano debajo del puente disminuye, por lo que si tu barca o góndola es alta no podrá pasar.

Esa página web funciona bastante regular (por ser amable). Pero tenía algo que me atrajo de inmediato: Sobre un plano de Venecia se dibuja en verde o rojo los puentes que quedaban inutilizados cuando sube la marea (valor que podías modificar con un control manual en la página).

Lo primero que pensé es que al cambiar la altura de la marea se iba al servidor para refrescar la imagen para indicar qué puentes quedan inutilizados al subir la marea. Eso quería decir que cada puente estaba en una base de datos con sus coordenadas.

Cuál fue mi sorpresa cuando al analizar el tráfico con el servidor (para ver qué peticiones se mandaban para refrescar el plano y así poder hacerlas a mano) vi que la base de datos no residía en el back-end.

Estaban usando una base de datos JSON en el lado cliente. Error de principiante si quieres evitar que te la clonen.

La cosa iba a ser mucho más sencilla que intentar sacarla del Server. Sin utilizar ninguna herramienta sofisticada (solo el navegador) extraje el archivo JSON con la base de datos y lo convertí en un Excel para trabajar con comodidad.

La base de datos era magnífica. Tenía toda la información que necesitaba y más…

Así que comparé con la información que había obtenido por el otro método y pude concluir que ya disponía de todos los puentes de Venecia con su geolocalización.

Quiero remarcar que no hay nada irregular o ilegal en analizar lo que un Server te envía al navegador. Si se manda información al navegador para que pinte o escriba algo, tienes que saber que puede ser vista, analizada y copiada en su caso. Las técnicas para analizar esa información no son el objeto de esta entrada.

El resultado de esa investigación puede resumirse en este mapa:

Tenía ya la respuesta a mis dos primeras preguntas: Había ubicado con sus coordenadas (latitud y longitud) todos los puentes venecianos y estaban accesibles en una base de datos propia.

Haciendo caminos, caminos sobre el canal

Continuando con los versos de Machado del título, haremos caminos, no sobre la mar, sino sobre el canal.

El problema a resolver es cómo planificar un camino que te haga pasar todos los puentes minimizando la distancia (o el tiempo, ya que supondremos velocidad constante) a recorrer.

No es el mismo problema de los puentes de Königsberg resuelto brillantemente por el matemático Leonard Euler en el siglo XVIII y que dio origen a una nueva rama de las matemáticas, la teoría de grafos.

Problema de los puentes de Königsberg

Euler demostró matemáticamente la imposibilidad de cruzar todos los puentes de la ciudad una única vez para atravesarlos todos.

Si sabes la solución de Euler podrás aplicarla para saber que también es imposible en Venecia.

Aquí el problema es otro, encontrar el camino mínimo que pase por todos los puentes. Ese es un problema que parece más sencillo, pero que en realidad no lo es.

Es lo que se conoce como el problema del viajante. Aunque en este caso no existía la condición de regresar al punto de origen.

Es un problema al que nos enfrentamos casi todos los días cuando caminamos o conducimos y que, hasta no hace mucho, era resuelto por nuestra mente. Pero desde que existen los GPS, navegadores, Google Maps, etc. dejamos que un algoritmo programado nos indique el camino a seguir (y no es lo peor que dejamos que hagan los algoritmos).

En Google Maps puedes pedir que te calcule el camino mínimo para un trayecto de hasta 10 paradas, pero aquí estamos hablando de más de ¡400!

Había que remangarse y hacer un software específico para resolver el problema. Seguro que Google ofrece ese servicio para suscriptores de pago. No dudo que habrá software comercial que lo haga, Pero, ¿a qué hemos venido aquí?

¡A jugar!

Definiendo el problema

Primero, los datos de entrada. Tenemos un mapa de Google Maps donde hemos definido los puntos (puentes) por los que queremos pasar.

Segundo, tenemos que descargar esos datos y convertirlos a un formato que podamos tratar.

Tercero, hay que calcular la distancia entre esos puntos y elegir el camino mínimo. ¡Ojo! No queremos la distancia en línea recta. Queremos calcular la distancia caminando por las calles. Eso nos obligará a usar algún servicio (o API) de un proveedor que nos dé ese dato.

Una vez calculado el camino queremos como salida del problema otro mapa de Google que nos dibuje el camino calculado.

¿Fácil, no?

Extracción de los datos de Google Maps

Esto es trivial.

Os imagino acostumbrados a usar Google Maps. Aunque no sé si habéis creado alguna vez un mapa.

Para generar un mapa solo hay que acceder aquí. Es necesario tener una cuenta de GMail, por supuesto.

Entra y genera un mapa que unos cuantos landmarks.

En el menú desplegable (los consabidos tres puntitos verticales) tenemos la opción de exportar los datos a kml/kmz.

Seleccionaremos el formato kml (el kmz no es más que un formato comprimido del kml).

El fichero descargado puede abrirse con un editor de texto y puedes ver su estructura. Es un fichero xml del que nos interesan (ahora mismo) los nodos <Placemark> y dentro de él, los nodos <name> y <coordinates>. Los muestro en rojo. El resto no nos interesa, pero que sepas que es la foto asociada al punto.

      <Placemark>
        <name>Ponte del Pistor</name>
        <description><![CDATA[<img src="https://mymaps.usercontent.google.com/hostedimage/m/*/3APRDMAXbFJarF-S7MfksGRU0icmBHiXx4ddfbjqMTPjrfxLVLV-Pvk6_KIWVkfkCNZ_uLRFApTSRGDolljlrzzcwWuUA39eIbMyxFCND0Iecieiv8w-U0K_UK-z7t6kY2dMpGeGKu9oyJkH62Tce_AnVeK3E-OklvQSRQYkd9IqxibLtChK82bovln80-b5wob5v_SZUq1JWQLRx44PCjEGA4IJzIHVk34PG9AQuegx-vvZSMOPzc8DOefEI1UY?authuser=0&fife=s16383" height="200" width="auto" /><br><br>Rio del Piombo]]></description>
        <styleUrl>#icon-1528-0288D1</styleUrl>
        <ExtendedData>
          <Data name="gx_media_links">
            <value><![CDATA[https://mymaps.usercontent.google.com/hostedimage/m/*/3APRDMAXbFJarF-S7MfksGRU0icmBHiXx4ddfbjqMTPjrfxLVLV-Pvk6_KIWVkfkCNZ_uLRFApTSRGDolljlrzzcwWuUA39eIbMyxFCND0Iecieiv8w-U0K_UK-z7t6kY2dMpGeGKu9oyJkH62Tce_AnVeK3E-OklvQSRQYkd9IqxibLtChK82bovln80-b5wob5v_SZUq1JWQLRx44PCjEGA4IJzIHVk34PG9AQuegx-vvZSMOPzc8DOefEI1UY?authuser=0&fife=s16383]]></value>
          </Data>
        </ExtendedData>
        <Point>
          <coordinates>
            12.3384377,45.4380212,0
          </coordinates>
        </Point>
      </Placemark>

Así que ahora toca elegir un lenguaje de programación. Siempre he dicho que el mejor lenguaje de programación es aquel en el que te sientas más cómodo. Hablo para la programación casera y de aficionado. En trabajos profesionales hay que valorar otras cosas.

Yo voy a usar PERL que es un lenguaje interpretado muy versátil. Y que me resulta sencillo de manejar.

En esta primera parte vamos a leer el fichero KML que acabamos de descargar y vamos a generar un fichero CSV que en cada línea tendrá el nombre y las coordenadas de cada punto.

Llegado a este punto siempre me surge la duda de hasta dónde explicar el software. Tengo un buen amigo que siempre dice que la mejor documentación de un software es el propio software. Que solo hay que leerlo.

Voy a hacer un poco más, pero tampoco voy a explicar cada línea de código. El objetivo es dar unas pinceladas para que cada persona que lea utilice la información para realizar su propio software en este u otro lenguaje.

El programa tomará como parámetro el nombre del fichero y comprobará que existe.

Usaremos la librería XML::LibXML para tratar el fichero KML. Si no la tienes descargada puedes buscar en internet cómo hacerlo.

#!/usr/bin/perl
use strict;
use warnings;
use XML::LibXML;
# Verifica que se haya pasado al menos un argumento
if (@ARGV < 1) {
    die "Uso: $0 <ruta_fichero>\n";
}
# Toma el primer parámetro de la línea de comandos
my $file = $ARGV[0];
# Comprueba si el fichero existe
if (!-e "$file.kml") {
    die "Error: El fichero '$file.kml' no existe.\n";
}

Cargamos y preparamos el fichero para ser analizado y definimos la matriz que almacenara los nombres y coordenadas de cada punto.

# Carga el archivo KML
my $parser = XML::LibXML->new();
my $doc = $parser->parse_file("$file.kml");
# Declarar el espacio de nombres explícitamente en el contexto del documento
my $xpc = XML::LibXML::XPathContext->new($doc);
$xpc->registerNs('kml', 'http://www.opengis.net/kml/2.2');
# Array para almacenar los landmarks
my @landmarks;

Vamos a recorrer el fichero kml buscando los nodos <Placemark>

# Procesar cada Placemark en el archivo KML y extraer nombre y coordenadas
foreach my $placemark ($xpc->findnodes('//kml:Placemark')) {
    my $name = $xpc->findvalue('kml:name', $placemark);
    my $coordinates = $xpc->findvalue('.//kml:coordinates', $placemark);
    # Verificar que el nombre y las coordenadas existen
    if ($name && $coordinates) {
       # Eliminar saltos de línea
       $name =~ s/[\r\n]+//g;
       $coordinates =~ s/[\r\n]+//g;        
       # Separar latitud y longitud
       my ($longitude, $latitude) = split(',', $coordinates);
       # Eliminar espacios extra
       $latitude  =~ s/\s+//g;
       $longitude =~ s/\s+//g;
       # Guardar en el array
       push @landmarks, { name => $name, latitude => $latitude, longitude => $longitude };
    }
}

En esto de la programación yo, al menos, uso prueba y error. Veréis unas líneas que borran «saltos de línea» y espacios en blanco del fichero KML. Eso es necesario porque el archivo exportado está pensado para que lo lea un humano. Esas líneas son adaptaciones (o mejor llamarlas correctivo) según iba probando el código.

Finalmente, generamos un fichero con el mismo nombre del fichero KML, pero con extensión CSV (Comma Separated Values) y escribimos cada uno de los elementos del array (o matriz) que hemos obtenido en el loop del bloque anterior.

# Ordenar los landmarks alfabéticamente por nombre (NO NECESARIO)
# @landmarks = sort { $a->{name} cmp $b->{name} } @landmarks;
# Abrir archivo de salida en formato CSV
my $file_out = "$file.csv";
open(my $out, '>', $file_out) or die "No se pudo abrir el archivo de salida: $!";
# Escribir la cabecera del archivo CSV
print $out "Nombre,Latitud,Longitud\n";
# Escribir los datos ordenados en formato CSV
foreach my $landmark (@landmarks) {
    print $out "\"$landmark->{name}\",$landmark->{latitude},$landmark->{longitude}\n";
}
# Cerrar archivo de salida
close($out);
print "Proceso completado. Los datos ordenados se guardaron en fichero $file.csv.\n";

En el directorio de trabajo tendremos un nuevo fichero con este aspecto

Nombre,Latitud,Longitud
"Puente de los Descalzos",45.4411516,12.3227414
"Ponte de l'Olio",45.4384688,12.3370792
"Ponte Marco Polo",45.4385988,12.3381614
"Ponte del Cristo",45.438734,12.3397698

Una primera fila que describe los campos y tantas líneas como landmarks tuviera el fichero KML que incluye nombre, latitud y longitud.

Ese fichero CSV ya lo puedes cargar en un excel, en una base de datos o donde quieras.

Pero nosotros le vamos a dar otro uso y es el de calcular el camino mínimo que pase por todos los puntos. Bueno, no el camino mínimo, sino un camino mínimo.

Cálculo del camino mínimo

Ya tenemos un fichero con los datos necesarios para calcular el camino mínimo.

Como os dije, no podemos usar para el cálculo de la distancia la línea recta (o geodésica, que no vivimos en una Tierra plana).

Tenemos que calcular la distancia mediante algo que use un callejero, pues caminar por ciudades, incluso en Venecia, se hace por calles.

Mi primera intención fue usar Google Maps. Pero choqué contra un muro. Sin entrar en excesivos detalles, el motivo de abandonar el API que ofrece Google Maps era su limitación al número de puntos que se podían pasar en cada una de las llamadas.

Mi método para calcular el camino mínimo es un poco basto. Consistía en calcular la distancia entre cada par de puntos, cargarlo en una matriz y después aplicar un algoritmo de minimización del trayecto.

Eso implicaba hacer unas 400×400 llamadas al API que permitía hasta un número limitado de puntos por llamada. La respuesta de Google se eternizaba y fallaba en cuanto le enviaba más de 40 puntos. Tuve que buscar una alternativa.

La solución vino del software libre. Muy probablemente no habéis usado nunca OPENSTREET que es una alternativa libre y gratuita a Google Maps que permitía hacer lo que quería: obtener la distancia entre dos puntos de una ciudad caminando por sus calles y estimando el tiempo de recorrido con una velocidad de paseo.

Así que con esto pasamos a comentar el software que va a generar la solución al problema.

use strict;
use warnings;
use Text::CSV;
use LWP::UserAgent;
use JSON;
use Storable;
# Verifica que se haya pasado al menos un argumento
if (@ARGV < 1) {
    die "Uso: $0 <ruta_fichero>\n";
}
# Toma el primer parámetro de la línea de comandos
our $file = $ARGV[0];
# Comprueba si el fichero existe
if (!-e "$file.csv") {
    die "Error: El fichero '$file.csv' no existe.\n";
}
# Ruta al archivo CSV
my $file_csv = "$file.csv";
# CARGA LANDMARKS DESDE ARCHIVO
my @landmarks = cargar_landmarks($file_csv);

Cargamos varias librerías que utilizaremos en el código posterior:

  • TEXT::CSV – Para tratar el fichero CSV que hemos creado cn el módulo anterior.
  • LWP::UserAgent – Para realizar llamadas al API de OPENSTREETMAP vía http.
  • JSON – Para tratar el objeto JSON respuesta que proporciona OPENSTREET.
  • Storable – Para almacenar en disco para posterior uso las llamadas al API (una especie de caché).

Al igual que en el módulo que transformaba el KML a CSV, pasamos como parámetro el nombre del fichero CSV (sin extensión) y cargaremos los landmarks con la función cargar_landmarks.

sub cargar_landmarks {
    my ($archivo) = @_;
    my @landmarks;
    open my $fh, '<', $archivo or die "¡No se pudo abrir el archivo '$archivo': $!";
    my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
    $csv->getline($fh);  # Saltar la primera línea con los campos del fichero
    while (my $row = $csv->getline($fh)) {
        push @landmarks, { name => $row->[0], lat => $row->[1], lng => $row->[2] };
    }
    close $fh;
    print "Archivo de Landmarks cargado.\n";
    return @landmarks;
}

Una vez que tenemos en el array los datos de todos los puntos (nombre, latitud y longitud) llamamos a la función que calculará el camino mínimo.

# CALCULA CAMINO MÍNIMO
my @camino_minimo = calcular_camino_minimo(@landmarks);

En esta rutina se hacen varias cosas:

Se generará un fichero caché con las distancias obtenidas entre cada par de puntos para ser usada en secusivas invocaciones del programa. Esto tampoco es muy necesario pero para depurar el código me resultó útil no tener que llamar al API cada vez que hacía una prueba. Se puede eliminar una vez depurado el código.

En el DOBLE LOOP se llama a la función obtener_ruta que será la encargada de utilizar el API de OPENSTREETMAP que devuelve para cada par de puntos:

  • Distancia entre ellos.
  • Tiempo de recorrido.
  • Un trayecto (para que quede chulo en el Mapa).

Y para el final dejamos el algoritmo de la solución del problema del viajante en la función pdv_greedy.

sub calcular_camino_minimo {
  my (@landmarks) = @_;
  # Nombre del archivo para almacenar la matriz de distancias
  my $archivo_matriz = "$file.cache";
  # Cargar matriz de distancias si ya existe
  my @matriz_distancias;
  if (-e $archivo_matriz) {
    @matriz_distancias = @{retrieve($archivo_matriz)};
  } else {
    # Crear matriz de distancias entre landmarks
    my %distancias_cache;
    # DOBLE LOOP
    for my $i (0 .. $#landmarks) {
      for my $j ($i .. $#landmarks) {
        if ($i == $j) {
          $matriz_distancias[$i][$j] = $matriz_distancias[$j][$i] = 0;
        } else {
          my $key = join("_", sort($landmarks[$i], $landmarks[$j]));
          if (!exists $distancias_cache{$key}) {
            my ($distancia, $tiempo, $ruta) = obtener_ruta($landmarks[$i], $landmarks[$j]);
            $distancias_cache{$key} = $distancia // 1e9;
          }
          $matriz_distancias[$i][$j] = $matriz_distancias[$j][$i] = $distancias_cache{$key};
        }
      }
    }
    # Guardar la matriz de distancias
    store(\@matriz_distancias, $archivo_matriz);
  }
  # Resolver el PROBLEMA DEL VENDEDOR: MÉTODO GREEDY
  return pdv_greedy(\@matriz_distancias);
}

Veamos la función obtener_ruta que invocará a OPENSTREETMAP

sub obtener_ruta {
    my ($origen, $destino) = @_;
    my $ua = LWP::UserAgent->new;
    # Construir URL con perfil 'foot' y overview=full
    my $url = sprintf(
        "https://routing.openstreetmap.de/routed-foot/route/v1/foot/%s,%s;%s,%s?overview=full&geometries=geojson",
        $origen->{lng}, $origen->{lat},
        $destino->{lng}, $destino->{lat}
    );
    # DEBUG
    #print "Solicitando ruta: $origen->{name} -> $destino->{name}\nURL: $url\n";
    #print "Solicitando ruta: $origen->{name} -> $destino->{name}\n";
    my $response = $ua->get($url);
    unless ($response->is_success) {
        warn "Error al obtener datos entre $origen->{name} y $destino->{name}: " . $response->status_line;
        return (undef, undef, []);
    }

    my $data = decode_json($response->decoded_content);
    if ($data->{code} ne 'Ok') {
        warn "Error en la respuesta: $data->{code}";
        return (undef, undef, []);
    }
    # Distancia en metros
    my $distance = $data->{routes}[0]{distance};
    # Duración en segundos
    my $duration = $data->{routes}[0]{duration};
    # Objeto que permitirá dibujar el camino a recorrer en el mapa
    my @coordinates = @{$data->{routes}[0]{geometry}{coordinates}}; 
    return ($distance, $duration, \@coordinates);
}

De esta función nos quedamos con dos cosas:

La URL que vamos a invocar (con coordenadas de origen y destino) para obtener los datos (distancia, duración y trayecto) que nos interesan.

Lo vemos con un ejemplo de una llamada individual. Esta es una llamada al API con las coordenadas de dos puentes (puedes ver en el código anterior que se forman en lo que he resaltado en rojo).

https://routing.openstreetmap.de/routed-foot/route/v1/foot/12.3370792,45.4384688;12.3381614,45.4385988?overview=full&geometries=geojson

El API devuelve esta respuesta en la que coloreo lo que nos interesa extraer

{"code":"Ok","routes":[{"geometry":{"coordinates":[[12.337055,45.438452],[12.337016,45.438479],[12.337025,45.43851],[12.337083,45.438782],[12.337099,45.438882],[12.337114,45.438971],[12.337128,45.439048],[12.337312,45.43904],[12.337389,45.439039],[12.337408,45.439029],[12.33745,45.439015],[12.337518,45.438927],[12.337696,45.438869],[12.337777,45.438839],[12.337791,45.438833],[12.337825,45.438816],[12.337855,45.438801],[12.338003,45.438698],[12.33814,45.438638],[12.33816,45.438632],[12.338166,45.438599]],"type":"LineString"},"legs":[{"steps":[],"summary":"","weight":14.16,"duration":135.4,"distance":169.5}],"weight_name":"routability","weight":14.16,"duration":135.4,"distance":169.5}],"waypoints":[{"hint":"qKcZgOUF544jAAAAGgAAACAAAAApAAAA7_eIQLfDQkAKEGFApSKWQCIAAAAZAAAAHAAAACUAAABvAAAAnz-8APRVtQK3P7wABVa1AgEAbwgKMPKA","distance":2.664152124,"name":"Ponte de l'Ogio","location":[12.337055,45.438452]},{"hint":"wrZQgHXpCY0hAAAABQAAAAAAAABGAAAA1KRsQHEd5z4AAAAA50r-QB0AAAAEAAAAAAAAAEAAAABvAAAA9kO8AIdWtQLxQ7wAh1a1AgAAzwoKMPKA","distance":0.391308813,"name":"Ponte Marco Polo","location":[12.338166,45.438599]}]}

En azul, la duración de camino entre los dos puntos por las calles de Venecia, en rojo, la distancia recorrida; en verde las coordenadas de los puntos del trayecto por las calles de Venecia.

La respuesta es un fichero JSON que es tratado en la parte de código en azul de la función obtener_ruta.

Bueno… Ya no nos queda nada.

Ya tenemos la matriz con las distancias entre cada par de puntos, Ahora hay que aplicar el algoritmo del viajante que está en la función pdv_greedy.

sub pdv_greedy {
    my ($matriz_ref) = @_;
    my @visitados = (0);  # Comenzar desde el primer punto pasado en el fchero csv
    my %visitado = (0 => 1);
    my $n = @$matriz_ref;
    while (@visitados < $n) {
        my $ultimo = $visitados[-1];
        my $min_dist = 1e9;
        my $siguiente;
        for my $i (0 .. $n-1) {
            next if $visitado{$i};
            if ($matriz_ref->[$ultimo][$i] < $min_dist) {
                $min_dist = $matriz_ref->[$ultimo][$i];
                $siguiente = $i;
            }
        }
        push @visitados, $siguiente;
        $visitado{$siguiente} = 1;
    }
    return @visitados;
}

El Problema del Viajante es un problema NP. Eso quiere decir que encontrar la solución sin probar todas es imposible (eso creo que dicen las matemáticas que estudian Matemáticas). Yo ni soy matemático, ni informático, así que leo.

Busqué un método sencillo y encontré el método «greedy» (en español, «codicioso»). Este método consiste en decidir un punto (puente) de salida y elegir el lugar no visitado más cercano como siguiente destino. Me pareció que para mi propósito era más que suficiente.

Por lo que he podido leer este método da un camino corto, aunque no el más corto. Dado que quería estar una semana en Venecia no era cuestión de que terminara demasiado pronto, ¿no?

Y ya acabamos el programa principal con la función que va a generar un fichero KML que mole.

# GENERA ARCHIVO KML
system("cls");
generar_kml_rutas(\@camino_minimo, @landmarks);

Lo de limpiar la pantalla es cosa de maños, que somos muy escoscados (ahora vas y buscas el significado de esa palabra de mi tierra).

La función generar_kml_rutas te va a molar (lo que no sé es cómo explicarla).

sub generar_kml_rutas {
    my ($camino, @landmarks) = @_;
    my $kml_file = $file . "_ruta.kml";
    open my $fh, '>', $kml_file or die "No se puede crear '$kml_file': $!";
    print $fh <<"HEADER";
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
    <!-- Estilo para las líneas -->
    <Style id="linea_roja">
        <LineStyle>
            <color>ff0000ff</color>  <!-- Rojo -->
            <width>4</width>
        </LineStyle>
    </Style>
    <!-- Estilo para el icono de puente -->
    <Style id="icon-1528-558B2F-nodesc-normal">
      <IconStyle>
        <color>ff2f8b55</color>
        <scale>1</scale>
        <Icon>
          <href>https://www.gstatic.com/mapspro/images/stock/503-wht-blank_maps.png</href>
        </Icon>
      </IconStyle>
      <LabelStyle>
        <scale>0</scale>
      </LabelStyle>
      <BalloonStyle>
        <text><![CDATA[<h3>$[name]</h3>]]></text>
      </BalloonStyle>
    </Style>
    <Style id="icon-1528-558B2F-nodesc-highlight">
      <IconStyle>
        <color>ff2f8b55</color>
        <scale>1</scale>
        <Icon>
          <href>https://www.gstatic.com/mapspro/images/stock/503-wht-blank_maps.png</href>
        </Icon>
      </IconStyle>
      <LabelStyle>
        <scale>1</scale>
      </LabelStyle>
      <BalloonStyle>
        <text><![CDATA[<h3>$[name]</h3>]]></text>
      </BalloonStyle>
    </Style>
    <StyleMap id="icon-1528-558B2F-nodesc">
      <Pair>
        <key>normal</key>
        <styleUrl>#icon-1528-558B2F-nodesc-normal</styleUrl>
      </Pair>
      <Pair>
        <key>highlight</key>
        <styleUrl>#icon-1528-558B2F-nodesc-highlight</styleUrl>
      </Pair>
    </StyleMap>
    <Folder>
      <name>Puentes</name>

HEADER

    for my $i (0 .. $#$camino) {
        my $idx_actual = $camino->[$i];
        my $landmark = $landmarks[$idx_actual];

        # Añadir punto al KML
        print $fh <<"PLACEMARK_PUENTES";
    <Placemark>
        <name>$landmark->{name}</name>
        <styleUrl>#icon-1528-558B2F-nodesc</styleUrl>
        <Point>
            <coordinates>$landmark->{lng},$landmark->{lat},0</coordinates>
        </Point>
    </Placemark>
PLACEMARK_PUENTES
}
        print $fh <<"PLACEMARK_CAPA";
    </Folder>
    <Folder>
      <name>Ruta</name>
PLACEMARK_CAPA
    my $tiempo_total = 0;
    my $distancia_total = 0;
    my $tramo = 0;
    for my $i (0 .. $#$camino) {
        my $idx_actual = $camino->[$i];
        my $landmark = $landmarks[$idx_actual];
        # Añadir ruta si no es el último punto
        if ($i < $#$camino) {
            $tramo = $tramo + 1;
            my $idx_siguiente = $camino->[$i + 1];
            my $next_landmark = $landmarks[$idx_siguiente];

            # Obtener geometría de la ruta
            my ($distancia, $duracion, $geometria) = obtener_ruta($landmark, $next_landmark);

            # Imprimir la información del tramo en pantalla
            printf "Tramo %d : %s -> %s | Distancia: %.2f m | Tiempo: %.2f min\n", $tramo,
                $landmark->{name}, $next_landmark->{name}, $distancia, $duracion / 60;
            $distancia_total = $distancia_total + $distancia;
            $tiempo_total = $tiempo_total + $duracion;
            # Construir las coordenadas de la ruta
            my $coords = join "\n", map { "$_->[0],$_->[1],0" } @$geometria;

            print $fh <<"LINESTRING";
    <Placemark>
        <name>Tramo $tramo: $landmark->{name} -> $next_landmark->{name}</name>
        <styleUrl>#linea_roja</styleUrl>
        <LineString>
            <coordinates>
            $coords
            </coordinates>
        </LineString>
    </Placemark>
LINESTRING
        }
    }

    print $fh <<"FOOTER";
</Folder>
</Document>
</kml>
FOOTER
    printf "\n\nDistancia Total: %.2f m\n", $distancia_total; 
    printf "Tiempo Total: %.2f min\n\n", $tiempo_total / 60; 
    close $fh;
    print "Archivo KML generado: $kml_file\n";
    return $kml_file;
}
¿Cómo te quedas?

A ver…

Ya he comentado arriba que los ficheros KML son ficheros XML que contienen los datos necesarios para visualizar objetos en un mapa.

Lo que hace esta función es generar la estructura XML insertando los datos que hemos calculado hasta este momento y los va colocando en su sitio teniendo en cuenta el formato definido en el estándar usado por GMaps.

Va creando landmarks con las coordenadas de los puentes, les asocia un bonito icono de un puente, y dibuja una línea siguiendo el camino calculado que nos ha devuelyo OPENSTREET.

Y todo eso se hace intercalando código con datos y leyendo desde el propio programa las líneas de datos.

Si no me crees coge todo lo que te he contado y lo ejecutas en tu ordenador. Si lo haces obtendras un fichero KML que podrás importar en Google Maps.

En mi caso como le pasé el fichero KML con los puentes de Venecia obtuve esto:

Por comodidad visual creé dos capas (que pueden seleccionarse para visualizar o no): una para los landmarks o puentes y otra con los caminos de uno a otro.

Dividí Venecia en dos partes con la frontera natural del Gran Canal. De ese modo, las llamadas al API se reducían y parecía obvio que con solo 4 puentes en el Gran Canal no iba a influir en los trayectos. Y la reducción de CPU era muy considerable.

Ya tenía una estimación de la distancia a recorrer y del tiempo que tardaría (dedicaría 2 minutos para fotografiar el puente y vi que la luz del día en esas fechas eran de unas 8 horas).

PuentesRuta (m)Ruta
(min)
Foto
(min)
Tiempo
Total
Venecia
Norte
26338672,1516,435261042,43
Venecia
Sur
15620704,7276,23312588,23
Giudecca215308,670,8242112,82
TOTAL44064685,4863,488801743,48
14:23:2914:40:0029:03:29
3 d:5 h:3 min

4 días + 1 para contingencias: 5 noches en Venecia.

Control de lo fotografiado

Una vez que comenzase a caminar tenía que llevar el control de haber fotografiado cada uno de los puentes.

Lo primero era habilitar la geolocalización en la cámara Reflex, mi querida CANON EOS 6D.

Lo segundo llevaría un dispositivo grabando mis paseos sincronizado con la cámara (no me fío mucho del GPS de CANON).

Pero ¿cómo marcaba que había visto un puente? Una cosa es llevar un camino previsto, pero luego puede haber calles cerradas, desvíos imprevistos, mil cosas.

La solución de una libreta con una lista e ir marcando X me parecía que no estaba a la altura de los objetivos del proyecto.

Tenía una solución al alcance de la mano.

¿Recuerdas que había creado una Base de datos con todos los puentes?

Solo tenía que generar un front-end o una app para el móvil que me mostrara los puentes al estilo de Google Maps y pudiera ir activando un check cuando lo fotografiaba.

Dicho y hecho.

Cargue la Base de Datos de los puentes en Google Drive y utilizando Google App Sheets me preparé un front-end que podía visualizar en móvil o PC y para facilitar la gestión dibujaría en un mapa mi posición y un acceso directo a un botón para marcar como visitado el puente.

Os muestro el aspecto de la APP en el móvil con sus dos funciones principales:

  • Una vista con para visualizar listados, editar y filtrar los registros .
  • Una vista en forma de mapa con los puentes señalados con dos colores: verde=Visitado, rojo=No Visitado.

He marcado uno en rojo para que veáis cómo iba (ya están todos en verde).

Además la App me mostraba con un cursor mi posición por lo que era muy sencillo la identificación de dónde estaba

¡A viajar!

La fase de preparación del proyecto había concluido. Solo faltaba elegir la fecha del viaje.

El requisito era muy sencillo: Que fuera gasto mínimo.

Eso hizo que seleccionara la semana que el precio del vuelo era menor. Por 56 € fui y volví de Venecia con una compañía de bajo coste. Únicamente con equipaje bajo el asiento del avión. Iba solo. Nadie se iba a dar cuenta de mi aspecto.

Los requisitos para el hotel también eran sencillos: en el centro geométrico de Venecia para que la distancia a los extremos de la ciudad fueran mínimas. Y barato.

Requisito cumplido y a 90 segundos de la plaza de San Marco.

Ya solo faltaba esperar el día del viaje.

Resultados del Proyecto

Si has leído la entrada anterior del blog habrás visto uno de los resultados: Venecia vista a través de los ojos de Corto Maltés.

Para ver el resultado principal, que son fotografías de todos los puentes venecianos con un aire melancólico e invernal, tendrás que esperar. Tengo más de 3000 fotogafrías que trataré con mucho mimo. Está por ver cómo las presente.

Mientras esperas ese resultado te puedo adelantar el trayecto real seguido por mis pies en la única, hermosa y sin par Venecia.

Disclaimer

El software que se presenta está completamente operativo, pero no exento de bugs. Se pueden presentar condiciones de error dependiendo, por ejemplo, de los nombres que elijas para los puntos de un mapa de Google.

Mi objetivo era que funcionara para mis requisitos.

No obstante, sirve para calcular cualquier camino mínimo que pase por los puntos definidos en un mapa de Google. Solo necesitas el intérprete de PERL, cargar unas librerías y … a funcionar.

No sé cuántos puntos soporta, pero con 450 funciona. No veo necesidad de fortalecer el software para más puntos.

Siéntete libre de mejorarlo.


Epílogo

En el vídeo de abajo puedes ver mi ritmo por Venecia esa semana.

Ya avisaré dónde y cuándo será la exposición fotográfica…



Deja un comentario

About Me

Físico por formación, astrónomo por devoción, ingeniero por alimentación, poeta por necesidad.

Diletante eterno, efímero polímata

Entradas recientes

Agendas Astronomía Astronáutica Calendarios Eclipses Einstein Fotografía Física Historia LHC Moonrises Programación Viajes