Revista Tecnología

Crear mapas HTML5 interactivos con RaphaelJS

Publicado el 10 octubre 2014 por Gaspar Fernández Moreno @gaspar_fm
votar


Vivimos en pleno auge del HTML5 y es que con esta tecnología se pueden hacer cosas muy interesantes, de forma muy rápida, elegante y multiplataforma sin necesidad de recurrir a herramientas de terceros como Flash, y eso está muy bien. Personalmente nunca me gustó Flash, así que, tenemos muchas cosas chulas por hacer.

Hace unos días, para un pequeño proyecto surgió la idea de crear un mapa interactivo de municipios, la idea está bien y hay mucho trabajo que hacer. Pero por un lado, tenemos cierta información ya disponible, por ejemplo los mapas y los nombres de los municipios los podemos encontrar por Internet fácilmente. Los mapas los podemos encontrar por ejemplo en esta web. En realidad es una recopilación de mapas de la Wikipedia (he copiado la recopilación aquí, por si la web citada deja de funcionar.)

En este pequeño tutorial voy a hacer un mapa interactivo de los municipios de Málaga en el que se podrá pasar el ratón por encima de los municipios y hacer click en cada uno de ellos.

Para facilitarme la vida he utilizado la biblioteca Javascript RaphaëlJS, con la que puedo dibujar trayectorias SVG de forma muy fácil y además como cada trayectoria será un objeto podré asignarles eventos a través de jQuery. Se va a quedar muy chulo.

De todas formas, para este tutorial utilizaré este mapa de municipios de Málaga, sacado de la Wikipedia, hecha por Emilio Gómez Fernández:

Iniciándonos con RaphaelJS

La gracia está en que el formato en el que maneja RaphaelJS las trayectorias es el mismo que se usa para los archivos SVG. Al final los archivos SGV son archivos XML con información sobre las trayectorias y estilos dentro.

Para hacer algo sencillo con RaphaelJS vamos a crear un archivo HTML extremadamente sencillo, con lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Ejemplo Raphaeljs</title>
    <script type="text/javascript" src="raphael-min.js"></script>
  </head>
  <body>
    <h1>Ejemplo Raphaeljs</h1>
    <hr />
    <div id="lienzo">
    </div>
    <script>
      var rjs = Raphael('lienzo', 600, 400);
      rjs.path('m 95.725713,145.10504 0,-11.088 9.359997,0 0,-9.36 39.456,0 0,9.36 9.504,0 0,19.008 9.504,0 0,39.456 -9.504,0 0,9.36 -20.304,0 0,-10.8 9.216,0 0,-36.576 -9.36,0 0,-9.36 -7.92,0 0,94.896 -9.504,0 0,9.36 -11.088,0 0,-104.256 -9.359997,0');
      rjs.path('m 172.59086,293.9256 0,-94.896 -9.36,0 0,-11.088 9.36,0 0,-9.36 39.456,0 0,9.36 9.504,0 0,19.008 9.504,0 0,29.952 -9.36,0 0,8.064 9.36,0 0,39.456 -9.504,0 0,9.504 -9.504,0 0,9.36 -29.952,0 0,-9.36 -9.504,0 m 20.592,-11.088 17.28,0 0,-36.432 -9.216,0 0,-10.944 9.216,0 0,-27.072 -9.36,0 0,-9.36 -7.92,0 0,83.808');
    </script>
  </body>
</html>

Las dos trayectorias utilizadas, incluidas en rjs.path() están sacadas de un SVG y el resultado sería algo así:

Efecto hover

Vamos a añadir un efecto a cada uno de los paths o trayectorias del ejemplo anterior. Por ahora, todo sigue muy artesano, pero nos da una pista de cómo podemos aplicar el efecto a cada uno de los elementos del mapa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Ejemplo Raphaeljs</title>
    <script type="text/javascript" src="raphael-min.js"></script>
  </head>
  <body>
    <h1>Ejemplo Raphaeljs</h1>
    <hr />
    <div id="lienzo">
    </div>
    <script>
      var default_attributes = {
            fill: '#abcabc',
            stroke: '#000000',
            'stroke-width': 4,
        };
      var rjs = Raphael('lienzo', 600, 400);
      var p1 = rjs.path('m 95.725713,145.10504 0,-11.088 9.359997,0 0,-9.36 39.456,0 0,9.36 9.504,0 0,19.008 9.504,0 0,39.456 -9.504,0 0,9.36 -20.304,0 0,-10.8 9.216,0 0,-36.576 -9.36,0 0,-9.36 -7.92,0 0,94.896 -9.504,0 0,9.36 -11.088,0 0,-104.256 -9.359997,0');
      p1.attr(default_attributes);
      p1.hover(function() {
        this.animate({
      fill: '#00bbff'
    }, 100);
    p1[0].style.cursor = 'crosshair';
      },
      function() {
        this.animate({
      fill: default_attributes.fill
    }, 100);
      }).click(function() {
        alert("click en la P");
      });;
      var p2 = rjs.path('m 172.59086,293.9256 0,-94.896 -9.36,0 0,-11.088 9.36,0 0,-9.36 39.456,0 0,9.36 9.504,0 0,19.008 9.504,0 0,29.952 -9.36,0 0,8.064 9.36,0 0,39.456 -9.504,0 0,9.504 -9.504,0 0,9.36 -29.952,0 0,-9.36 -9.504,0 m 20.592,-11.088 17.28,0 0,-36.432 -9.216,0 0,-10.944 9.216,0 0,-27.072 -9.36,0 0,-9.36 -7.92,0 0,83.808');
      p2.attr(default_attributes);
      p2.hover(function() {
        this.animate({
      fill: '#ffbb00'
    }, 100);
    p2[0].style.cursor = 'wait';
      },
      function() {
        this.animate({
      fill: default_attributes.fill
    }, 100);
      }).click(function() {
        alert("click en la B");
      });;
    </script>
  </body>
</html>

El resultado es algo así:

Creando un mapa

En este ejemplo ya estamos cambiando cursores y colores. Ahora bien, hacer todo esto con tantas trayectorias puedes ser un poco más complicado, podemos hacerlo de varias formas. Algunas de ellas implican un poco de programación en el servidor (aunque no voy a entrar ahí hoy), lo que sí deberíamos hacer es establecer una correspondencia entre los identificadores de las trayectorias que encontramos en el SVG (sólo tenemos que abrirlo) con los nombres o la identificación de los elementos del mapa. Para lo cual, podríamos utilizar una base de datos.

Tenemos que tener en cuenta que si abrimos el archivo SVG con un programa y lo guardamos de nuevo, puede que algunos IDs cambien por lo que podremos perder información.

Hemos visto en la imagen de ejemplo, que el SVG tiene los nombres escritos (lamentáblemente, si abrimos este SVG, no encontramos el texto del nombre, sino las trayectorias que forman cada letra, por lo que sería muy complicado establecer las correspondencias automáticamente). De todas formas, los textos no nos hacen falta ahora mismo, por lo que los voy a eliminar (desde Javascript). En principio, vamos a mostrar el id de las trayectorias, por lo que no haremos correspondencias entre trayectorias<->nombres.

En este ejemplo, he utilizado jQuery para hacer una descarga ajax del archivo SVG y cargarlo, así como buscar dentro de la estructura XML del SVG y escribir el nombre del ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Ejemplo Raphaeljs</title>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="raphael-min.js"></script>
  </head>
  <body>
    <h1>Ejemplo Raphaeljs</h1>
    <hr />
    <div id="municipiotxt">Selecciona un municipio</div>
    <div id="lienzo">
      <img id="loadingicon" src="loading.gif" />
    </div>
    <script>
      var default_attributes = {
            fill: '#abcabc',
            stroke: '#000000',
            'stroke-width': 1,
        };
      var $munictxt = $('#municipiotxt');
      $.ajax({
        url: 'Malaga_municipios.svg',
    type: 'GET',
    dataType: 'xml',
    success: function(xml) {
      var rjs = Raphael('lienzo', 700, 400);
      $(xml).find('svg > g > g > path').each(function() {
        var path = $(this).attr('d');
        var pid = $(this).attr('id');
        var munic = rjs.path(path);
        munic.attr(default_attributes);
        munic.hover(function() {
          this.animate({ fill: '#00bbff' });
          $munictxt.html("Municipio: "+pid);
        }, function() {
          this.animate({ fill: default_attributes.fill });
          $munictxt.html("Selecciona un municipio");
        }). click(function() {
          alert("Click sobre un municipio. ID = "+pid);
        });
          });
      $('#loadingicon').hide();
    }
      });
    </script>
  </body>
</html>

Para finalizar, vamos a hacer corresponder algunos ID de trayectoria con el municipio que representan (muy parecido al anterior):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Ejemplo Raphaeljs</title>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="raphael-min.js"></script>
  </head>
  <body>
    <h1>Ejemplo Raphaeljs</h1>
    <hr />
    <div id="municipiotxt">Selecciona un municipio</div>
    <div id="lienzo">
      <img id="loadingicon" src="loading.gif" />
    </div>
    <script>
      var municipios_data = {'path2643': 'Cuevas de San Marcos',
        'path2647': 'Alameda',
    'path2651': 'Cuevas Hajas',
    'path2655': 'Villanueva de Algaidas',
    'path2659': 'Antequera',
    'path2667': 'Villanueva de Tapia',
    'path2671': 'Sierra de Yeguas',
    'path2675': 'Mollina',
    'path2679': 'Humilladero',
    'path2683': 'Fuente de Piedra',
    'path2687': 'Archidona',
    'path2691': 'Campillos',
    'path2695': 'Villanueva del Trabuco',
    'path2699': 'Almargen',
    'path2703': 'Teba',
    'path2707': 'Villanueva del Rosario',
    'path2711': 'Cañete la Real',
    'path2715': 'Alfarnate',
    'path2719': 'Alfarnatejo',
    'path2723': 'Colmenar',
    'path2727': 'Periana',
    'path2731': 'Riogordo',
    'path2735': 'Valle de Abdalajis',
    'path2739': 'Alcaucín',
    'path2743': 'Ardales',
    'path2747': 'Álora',
    'path2751': 'Ronda',
    'path2763': 'Cuevas del Becerro',
    'path2775': 'Canillas de Aceituno',
    'path2779': 'Sedella',
    'path2787': 'Viñuela',
    'path2795': 'Málaga',
    'path2799': 'El Burgo',
    'path2803': 'Canillas de Albaida',
    'path2807': 'Salares',
    'path2811': 'Comares',
    'path2819': 'Carratraca',
    'path2823': 'Cómpeta',
    'path2827': 'Casarabonela',
    'path2831': 'Vélez-Málaga',
    'path2839': 'Frigiliana',
    'path2843': 'Nerja',
    'path2859': 'Arriate',
    'path2867': 'Cártama',
    'path2871': 'Torrox',
    'path2875': 'Pizarra',
    'path2899': 'Algarrobo',
    'path2903': 'Yunquera',
    'path2907': 'Montejaque',
    'path2911': 'Totalán',
    'path2915': 'Alozaina',
    'path2927': 'Coín',
    'path2931': 'Benaoján',
    'path2935': 'Rincón de la Victoria',
    'path2939': 'Tolox',
    'path2943': 'Alhaurín de la Torre',
    'path2947': 'Guaro',
    'path2951': 'Alpandeite',
    'path2955': 'Alhaurín el Grande',
    'path2959': 'Parauta',
    'path2967': 'Jimera de Líbar',
    'path2971': 'Cortes de la Frontera',
    'path2975': 'Júzcar',
    'path2979': 'Atajate',
    'path2983': 'Igualeja',
    'path2987': 'Istán',
    'path2991': 'Monda',
    'path2995': 'Faraján',
    'path2999': 'Torremolinos',
    'path3003': 'Benahavís',
    'path3007': 'Benadalid',
    'path3011': 'Pujarra',
    'path3015': 'Mijas',
    'path3019': 'Benalmádena',
    'path3023': 'Benalauría',
    'path3027': 'Ojén',
    'path3031': 'Jubrique',
    'path3035': 'Algatocín',
    'path3039': 'Fuengirola',
    'path3043': 'Benarrabá',
    'path3047': 'Genalguacíl',
    'path3051': 'Gaucín',
    'path3055': 'Marbella',
    'path3059': 'Estepona',
    'path3063': 'Casares',
    'path3067': 'Manilva'};
      var default_attributes = {
            fill: '#abcabc',
            stroke: '#000000',
            'stroke-width': 1,
        };
      var $munictxt = $('#municipiotxt');
      $.ajax({
        url: 'Malaga_municipios.svg',
    type: 'GET',
    dataType: 'xml',
    success: function(xml) {
      var rjs = Raphael('lienzo', 700, 400);
      var corr="";
      $(xml).find('svg > g > g > path').each(function() {
        var path = $(this).attr('d');
    var pid = $(this).attr('id');
        var munic = rjs.path(path);
        munic.attr(default_attributes);
        munic.hover(function() {
          this.animate({ fill: '#00bbff' });
          var text = "Municipio: ";
          if (typeof(municipios_data[pid])!='undefined')
            text+=municipios_data[pid];
          else
            text+="Sin nombre";
          text+="("+$(this).attr('id')+")";
          $munictxt.html(text);
        }, function() {
          this.animate({ fill: default_attributes.fill });
          $munictxt.html("Selecciona un municipio");
        }). click(function() {
          alert("Click sobre un municipio. ID = "+$(this).attr('id'));
        });
          });
      $('#loadingicon').hide();
    }
      });
    </script>
  </body>
</html>

El resultado de todo esto será:

Selecciona municipio
Crear mapas HTML5 interactivos con RaphaelJS

ñíÁñáóéáááíáóíííúááíáíéíáíí

Otra opción, sería almacenar las trayectorias individualmente en base de datos con los datos asociados que queramos almacenar (población, usuarios de esas zonas, enlaces y más), descargarlo por AJAX cuando se genera la página o devolver las trayectorias y los datos adicionales junto con el HTML de la página.


Volver a la Portada de Logo Paperblog