3.1. Creación de mapas

Bajo este epígrafe se hará una exposición didáctica de cómo aprovechar las ventajas de la librería leafext.js para construir mapas. De hecho, el mapa de adjudicaciones y oferta educativa es una aplicación particular de su uso basada en tres niveles:

  • leafext.js en un nivel más bajo y absolutamente genérico.
  • adjofer/maps/adjofer/map.js para definir en concreto ila naturaleza de mapa, cuáles son los iconos, cómo se dibujan a partir de los datos y cómo varían en función de las correcciones y filtros que se apliquen sobre tales datos. Su implementación es independiente de cuál sea la herramienta que se use para construir la interfaz visual.
  • interfaz/adofer.vuejs/scripts/main.js que implementa una interfaz atractiva haciendo uso de los dos niveles anteriores. Es el encargado de la barra lateral.

Trataremos aquí cómo crear un segundo nivel usando las herramientas que nos brinda el primero, aunque para los ejemplos prácticos tendremos que introducir algún aspecto propio del tercero, ya que el mapa se tendrá que ver de alguna forma.

Más adelante, describiremos cómo usar el segundo nivel implementado para el mapa de adjudaciones para construir una interfaz visual (tercer nivel).

3.1.1. Conceptos previos

Partimos de la necesidad de representar sobre un mapa una serie de entidades cada una de las cuales tiene asociado un conjunto de datos. Para ello, podemos usar Leaflet y colocar una marca por entidad, pero el uso de esta librería (u otra equivalente) se limitará a permitirnos asignar un icono cuyo aspecto será independiente de los valores concretos de los datos. Nuestra intención, sin embargo, es ligar el aspecto del icono a los datos

En principio, distinguiremos estos conceptos.

Datos
Incluyen las coordinadas de la entidad y otros datos característicos de los que se quiere dejar constancia, total o parcialmente, a través del aspecto visual de la marca.
Marca
Es la plasmación de la entidad (y sus datos) sobre el mapa.
Clase (o tipo) de marca.
Todas las marcas que representen un mismo tipo de entidad pertenecen a una misma clase de marca. Cada clase tiene, además, asociados un sistema de correcciones y un sistema de filtros.
Sistema de correcciones
Sistema que permite registrar, aplicar y revertir correcciones aplicables a los datos de la marca que constituyan una serie. Por ejemplo, en un centro educativo, su oferta de enseñanzas es la lista de enseñanzas que se imparten en él. Al usuario pueden interesarle unas enseñanzas, pero no otras, así que su intención puede ser eliminar temporalmente esas enseñanzas que considera irrelevantes a los efectos de su búsqueda.
Sistema de filtros
Sistema que permite fijar criterios para hacer desaparecer (o volver a mostrar) marcas.

¿Cuándo puede resultarme útil leafext.js?

La librería se torna útil cuando, dado un conjunto de datos a representar sobre un mapa:

  1. Se quiere que los iconos que representan datos de un mismo tipo de entidad no sean exactamente iguales, sino que partiendo de una plantilla sufran alteraciones en función de loes valores de sus datos. Por ejemplo, que el color dependa de lo grande que sea la magnitud del valor correspondiente.
  2. Los datos puede sufrir correcciones por la interacción del usuario, lo cual por supuesto podrá tener reflejo en el aspecto del icono, si éste dependía de los datos cuyo valor ha cambiado.
  3. Parte de los iconos pueden filtrarse y desaparecer, como consecuencia de las decisiones del usuario.

Y, por supuesto, cuando deseamos hacer todo esto conjuntamente. ;-)

Para que se haga una idea, esto es mapa desarrollado con la librería:

../_images/iconos.png

Todos los iconos representan centros educativos y todos tienen una plantilla común, pero sus detalles visuales particulares dependen de los datos que cada uno tiene asociados (algunos son bilingües en inglés y muestran una bandera, otros son de compensatoria y muestran un pequeño círculo azul, etc.).

3.1.2. Uso básico

Nota

Quizás pueda servirle de ayuda a la lectura, tener a la vista desde el principio el ejemplo mínimo de aplicación.

3.1.2.1. Preliminares

3.1.2.1.1. Datos

La idea es que disponemos de un conjunto de datos, que describen un conjunto de entidades localizables en un mapa. Por ejemplo, la entidades pueden ser centros educativos, de cada uno de lo cuales se conoce su situación geográfica y una serie de características de interés. Nuestra intención es convertir cada entidad en una marca dentro del mapa. Consideremos que el formato de los datos es GeoJSON:

{
   "type": "FeatureCollection",
   "features": [
      {
         "type": "Feature",
         "geometry": {
            "type": "Point",
            "coordinates": [-5.9526, 37.275475]
         },
         "properties": {
            "name": "Centro 1",
            "adj": ["Suprimido", "Concursillo", "Concursillo", "Interino"],
            "oferta": ["SMR", "DAM", "BACHILLERATO"],
            "tipo": "normal"
         }
      },
      {
         "type": "Feature",
         "geometry": {
            "type": "Point",
            "coordinates": [-4.6389, 37.58434]
         },
         "properties": {
            "name": "Centro 2",
            "adj": ["Concursillo", "Expectativa", "Interino"],
            "oferta": ["SMR", "ASIR"],
            "tipo": "dificil"
         }
      }
   ]
}

Nota

No es requisito que los datos tengan este formato, pero es un estándar y Leaflet dispone de un tipo de capa que es capaz de interpretarlos directamente generando una marca y conectándole los datos a través del atributo feature. En cualquier caso, es posible utilizar un formato cualquiera de datos, si creamos nosotros mismos la marca y le asociamos sus datos a través de un atributo cualquiera.

3.1.2.1.2. Requerimientos

Como es obvio, el uso de la librería exige la carga previa de Leaflet:

<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
      integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
       crossorigin="">
<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
        integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
        crossorigin=""></script>

A lo que podríamos añadir nuestros plugins favoritos de Leaflet, y la carga de nuestra librería y el script donde desarrollaremos la creación del mapa.

<!-- Extensión para el soporte de iconos mutables -->
<script src="../dist/leafext.js"></script>

<!-- Script particular para este mapa -->
<script src="scripts/demo.js"></script>

Las pautas para escribir este último script (scripts/demo.js) (y el propio documento HTML claro está) son el propósito de este documento.

También, por supuesto, deberíamos incluir en el HTML un elemento en el que incrustar el mapa. Típicamente:

<div id="map"></div>

3.1.2.2. Carga básica

Para cargar el mapa y los datos podemos distinguir cuatro tareas distintas:

const Icono = crearIcono();

map = L.map("map").setView([37.07, -6.27], 9);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 18
}).addTo(map);

// Y una capa GeoJSON para crear las marcas y conectarles los datos.
const layer =  L.geoJSON(null, {
   pointToLayer: (f, p) => new Centro(p, {
      icon: new Icono(),
      title: f.properties.name,
   })
}).addTo(map);

const Centro = L.MutableMarker.extend({
   options: {mutable: "feature.properties"}
});

layer.addData(datos);
  1. La creación del icono, que hemos incluido dentro de la función crearIcono(), a lo que dedicaremos el próximo apartado.

  2. La creación del mapa, que es la habitual con Leaflet.

  3. La creación de una capa para el tratamiento de los datos en formato GeoJSON. En este caso se ha supuesto que los datos se obtuvieron previamente de algún modo. Obsérvese cómo se usa la clase de marca (Centro) e icono (Icono). En caso de que el formato de entrada no sea GeoJSON, podríamos usar simplemente L.LayerGroup o L.FeatureGroup, aunque tendríamos que ligar manualmente los datos a la marca.

  4. La creación de la marca apropiada que trataremos más adelante.

3.1.2.3. Icono

La definición del icono es la parte más engorrosa de toda la programación, en la medida en que al ser un icono cuyo aspecto cambia según los datos particulares asociados a cada marca o según las correcciones que el usuario imponga a estos datos, hay que definir cuáles son las reglas de cambio. En un icono normal, además de propiedades adicionales como el tamaño o el punto de anclaje, la propiedad fundamental es aquella que define cuál es el icono: iconUrl para iconos que se definen como imágenes, y html para iconos L.DivIcon. Para nuestros iconos diversos y mutables, en cambio, hay que definir también cómo los datos se traducen en detalles visuales del icono.

Para ilustrar la explicación usemos este sencillo, parecido a un Chupa Chups:

../_images/chupachups.png

El icono tiene dos detalles que depende de los datos asociados: el número que representa el número de adjudicaciones; y el fondo del círculo que es un color que depende del tipo de centro.

3.1.2.3.1. Definición

Para la definición de iconos, en principio, se ha manipulado la clase L.DivIcon para que mediante opciones adicionales acepte una plantilla para generar iconos más que un elemento HTML que defina el aspecto invariante del icono.

class MutableIcon()

Extensión de L.DivIcon a fin de crear iconos definidos por una plantilla a la que se aplican cambios en sus detalles según sean cambien los valores de sus opciones de dibujo. Consulte Icon.options para conocer cuales son las opciones adicionales que debe proporcionar para que la clase sea capaz de manejar iconos mutables.

Advertencia

Para crear el icono, use preferente la función L.utils.createMutableIconClass().

Examples:

function updater(o) {
   const content = this.querySelector(".content");
   if(o.hasOwnProperty(tipo) content.className = "content " + o.tipo;
   if(o.hasOwnProperty(numadj) content.textContent = o.numadj;
   return this;
}

const Icon = L.MutableIcon.extend({
   options: {
      className: "icon",
      iconSize: [25, 34],
      iconAnchor: [12.5, 34],
      url: "images/boliche.svg",
      updater: updater,
      converter: new L.utils.Converter(["numadj", "tipo"])
                            .define("numadj", "adj", a => a.total)
                            .define("tipo")
   }
});

const icon = new Icon();

Las opciones que debemos proporcionar en la creación de un tipo[1] de icono son las siguientes:

html (o bien, url)

Define la plantilla que se usará para crear el icono. Sobre esa plantilla se realizarán variaciones determinadas por los valores concretos de los datos. Si se proporciona url se entiende que es un fichero donde se ha almacenado la definición. Un típico caso, sería pasar la URL a un SVG:

const url = "images/centro.svg";

html, en cambio, debe usarse cuado la definición de la plantilla se hace:

  • A través de una cadena:

    const html = '<div class="content"><span></span></div><div class="arrow"></div>'
    
  • A través de un DocumentFragment que sería el objeto que obtendríamos si hubiéramos incluido la definición a través de un template HTML:

    <template id="icono">
        <div class="content"><span></span></div>
        <div class="arrow"></div>
    </template>
    

    que permitiría hacer en el código Javascript esta definición:

    const html = document.getElementById("icono").content;
    
  • Directamente a través de un HTMLElement[2]:

    const html = document.createElement("div");
    const content = document.createElement("div");
    content.className = "content";
    html.appendChild(content);
    const arrow = document.createElement("div");
    arrow.className = "arrow";
    html.appendChild(arrow);
    content.appendChild("span");
    

Advertencia

Tenga presente que, cuando se proporciona una URL, el elemento HTML no está inmediatamente disponible y, en consecuencia no podemos utlizar inmediatamente el construtcor resultante para empezar a crear iconos. Trataremos más adelante qué hacer en estos casos.

css

Cuando el icono se define a través de elementos HTML (o sea, todos los ejemplos anteriores, excepto el icono SVG), es preciso indicar las reglas CSS que permiten generar el icono:

const css = "images/chupachups.css";

El fichero podría ser algo así[3]:

.chupachups .content {
   position: relative;
   box-sizing: border-box;
   height: 70%;
   margin: 0; padding: 3px;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
   border: solid 3px #888;
   font-weight: bold;
}

.chupachups .arrow {
   position: relative;
   margin: 0; padding: 0;
   width: 10%; height: 30%;
   left: 45%;
   background-color: #444;
}

.chupachups .normal {
   background-color: #ddd;
}

.chupachups .compensatoria {
   background-color: #7be;
}

.chupachups .dificil {
   background-color: #ebb;
}

que provoca que el icono adquiera la forma de un chupachups y en el que se pretende notar dos características: la cantidad de adjudicaciones (como contenido del elemento <span>) y el tipo de centro como color de fondo.

converter

El aspecto del icono depende de los datos asociados, pero es bastante probable que no dependa de todos, sino sólo de una parte. En nuestro ejemplo, los datos son:

"data": {
   "adj": ["Suprimido", "Concursillo", "Concursillo", "Interino"],
   "oferta": ["SMR", "DAM", "BACHILLERATO"],
   "tipo": "normal".
}

o sea, las adjudicaciones, la oferta y el tipo de centro. Sin embargo, el icono se representa tomando el número de adjudicaciones y el tipo de centro; la oferta no contribuye al aspecto en obsoluto. Por tanto, las opciones de dibujo deberían ser:

opts = {
   numadj: 4,
   tipo: "normal"
}

Para definir cómo transformar data en opts, la librería provee de una clase L.utils.Converter():

const converter = new L.utils.Converter(["numadj", "tipo"])
                     .define("numadj", "adj", a => a.length)
                     .define("tipo");

Aunque hayamos definido todo en una sola orden, hemos realizado tres tareas:

  1. Crear el objeto:

    const converter = new L.utils.Converter(["numadj", "tipo"]);
    

    que permite especificar cuáles son las opciones de dibujo de las que dependerán los detalles visuales del icono: «numadj» y «»tipo».

  2. Definir cómo obtener numadj a partir de los datos:

    converter.define("numadj", "adj", a => a.length);
    

    qye significa: para obtener numadj (primer argumento) debemos basarnos en el valor de adj (segundo argumento) y obtener la longitud de su valor (que es el significado de la función que se ha usado en tercer lugar).

  3. Definir cómo obtener tipo, para lo cual se ha hecho esta simple definición:

    converter.define("tipo");
    

    lo cual es posible, ya que si no especifica el nombre de la propiedad de los datos, éste coincide con el de la opción de dibujo; y, si no se especifica la función conversora, el valor no se transforma en absoluto. Por tanto, lo anterior es equivalente a:

    converter.define("tipo", "tipo", t => t);
    

Como el método L.utils.Converter.define() devuelve el objeto mismo, es posible hacer encadenamiento y convertir las tres instrucciones en una sola.

Hay, no obstante, dos puntualizaciones que hacer:

  1. Cuando la opción de dibujo depende de dos o más propiedades, puede usarse un array. Por ejemplo, supongamos que una opción de dibujo fuera adjofer que es la suma del número de adjudicaciones y el número de enseñanzas. En ese caso, la definición podría haber sido:

    converter.define("adjofer", ["adj", "oferta"], (a, o) => a.length + o.length);
    

    Téngase en cuenta que los argumentos de la función conversora siguen el orden definido en el array. Por tanto, a representa al array de adjudicaciones y o al de oferta.

  2. Cuando la propiedad está anidada dentro de los datos puede usarse la notaciión de punto. Por ejemplo, supongamos que la definición de los datos hubiera sido así:

    "data": {
       "adj": ["Suprimido", "Concursillo", "Concursillo", "Interino"],
       "oferta": ["SMR", "DAM", "BACHILLERATO"],
       "mod": {
          "tipo": "normal".
       }
    }
    

    En ese caso la definición de tipo podría haberse hecho del siguiente modo:

    converter.define("tipo", "mod.tipo");
    

Advertencia

Si se desean aplicar correcciones, los valores de los atributos de los datos que son arrays susceptibles de sufrir correcciones, deben consultarse teniéndolo en cuenta. Vea más adelante cómo hacerlo.

updater

Define la función que traslada los valores de las opciones de dibujo al dibujo en sí:

function updater(o) {
   const content = this.querySelector(".content");
   if(o.tipo) content.className = "content " + o.tipo;
   if(o.numadj !== undefined) content.firstElementChild.textContent = o.numadj;
   return this;
}

El contexto de la función es el elemento HTML que representa al icono en la página[4], y o es el objeto que contiene las opciones de dibujo.

Advertencia

Para la mejora del rendimiento, no se pasan todos los parámetros sino sólo aquellos que han cambiado desde la última vez que se dibujó el icono. Por ese motivo, debe definir la función teniendo en cuenta esto. En la función de ejemplo, si no se pasa el tipo, no se modifica la clase de «content», y si no se pasa numadj, no se modifica el número contenido en el elemento <span>. Esto es así, porque no pasar la opción significa que su valor no ha cambiado y, en consecuencia, ese aspecto del dibujo debe permanecer igual.

Aunque puede definirse directamente el constructor es conveniente hacerlo a través de la función:

L.utils.createMutableIconClass(name, options, updater)

Facilita la construcción de clases de iconos. Cada clase está asociada a un estilo de icono distinto.

Argumentos:
  • name (string) – Nombre identificativo para la clase de icono.
  • options (Object) – Opciones de construcción de la clase.
  • options.css (string) – Para un icono creado con CSS, el archivo .css. que define el aspecto.
  • options.html (string|DocumentFragment|Document) – HTML que define la plantilla del icono. Se puede pasar como: * Una cadena que contenga directamente el código HTML. * Un DocumentFragment, que sería lo que se obtiene como contenido de un <template>. * Un Document, que sería lo que se obtiene de haber hecho una petición AJAX y quedarse cn la respuesta XML.
  • options.url (string) – Alternativamente a la opción anterior, la URL de un archivo donde está definido el icono (p.e. un SVG).
  • options.converter (L.utils.Converter) – Objeto L.utils.Converter para la conversión de los datos en opciones de dibujo.
  • updater (function) – Función que actualiza el aspecto del icono a partir de los nuevos valores que tengan las opciones de dibujo. Toma las opciones de dibujo (o una parte de ellas) y modifica el elemento DIV (o SVG. etc.) del icono para que adquiera un aspecto adecuado. Debe escribirse teniendo presente que pueden no pasarse todas las opciones de dibujo, sino sólo las que se modificaron desde la última vez que se dibujó el icono. Por tanto, debe escribirse la función para realizar modificaciones sobre el aspecto preexistente del icono, en vez de escribirse para recrear el icono desde la plantilla

Examples:

function updater(o) {
   const content = this.querySelector(".content");
   if(o.hasOwnProperty(tipo) content.className = "content " + o.tipo;
   if(o.hasOwnProperty(numadj) content.textContent = o.numadj;
   return this;
}

const Icon = L.utils.createMutableIconClass("chupachups", {
   iconSize: [25, 34],
   iconAnchor: [12.5, 34],
   css: "styles/chupachups.css",
   html: '<div class="content"><span></span></div><div class="arrow"></div>',
   converter: new L.utils.Converter(["numadj", "tipo"])
                         .define("numadj", "adj", a => a.total)
                         .define("tipo")
   updater: updater
});

Por ello, la función crearIcono() que introdujimos antes podemos definirla así:

function crearIcono() {
   // Definiciones de html, css, converter, updater

   return L.utils.createMutableIconClass("chupachups", {
      iconSize: [25, 34],
      iconAnchor: [12.5, 34],
      css: css,
      html: html,
      converter: converter,
      updater: updater
   });
}

Nota

Por supuesto, podemos seguir añadiendo opciones definidas para la clase L.Icon como es el caso de className, iconSize o iconAnchor. En el caso de esta primera opción no se ha definido valor alguno, pero cuando eso ocurre, la función añade un nombre de clase igual al del nombre que se le da al icono («chupachups»), de ahí que en el CSS que definía la forma del icono, se hubiera usado la clase «chupachups».

Si regresa a consultar las opciones que deben suministrase para construir un iconoi, comprobará que para definir cuál es el elemento HTML que define el icono debe proporcionarse html o url. La primera lo proporciona inmediatamente, pero la segunda exige una petición AJAX que provocará que el icono no esté plenamente operativo hasta que no se complete esta. En consecuencia el código básico propuesto podría fallar si en el momento de añadir los datos a la capa, no se ha completado la descarga del archivo que contiene la definición del icono. No lo hace en nuestro ejemplo, porque hemos usado la opción html. Para controlarlo, se disponen dos métodos aplicables directamente sobre el construcor (o sea, métodos de clase y no instancia):

Icon.isready()

Devuelve true si el icono es plenamente operativo. Por ejemplo:

const Icono = crearIcono();
Icono.isready();  // Devolverá false si para definir el icono se uso la opción url.
Icon.onready(func_success, func_fail)

Ejecuta la función suministrada como primer argumento cuando el constructor esté listo o la segunda, si falla su su creación (p.e. porque no se puede descargar el archivo dónde está definido el HTML):

const Icono = crearIcono()
Icono.onready(() => {
   console.log("¿Está listo ya el icono?", Icono.isready()); // true.
   const icono = new Icono();
   // etc...
});

CAMBIAR ESTO…

class L.utils.Converter(params)

Permite definir cómo un objeto se obtiene a partir de las propiedades de otro.

Construye conversores entre objetos de distinto tipo.

Argumentos:
  • params (Array.<String>) – Enumera las nombres de las propiedades que tiene el objeto de destino.

Examples:

const converter = L.utils.Converter(["numadj", "tipo"])
                            .define("numadj", "adj", a => a.total)
                            .define("tipo");
L.utils.Converter.define(param, properties, func)

Define cómo obtener una propiedad del objeto resultante.

Argumentos:
  • param (String) – El nombre de la propiedad.
  • properties (Array.<String>|String) – Los nombres de las propiedades del objeto original que contribuyen a formar el valor de la propiedad del objeto resultante. Si la propiedad es una sola, puede evitarse el uso del array y escribir directamente el nombre. Si se omite este argumento, se sobreentiende que el nombre de la propiedad en el objeto original y el resultante es el mismo.
  • func (function) – La función conversora. Debe definirse de modo que reciba como argumentos los valores de las propiedades que se enumeran en properties, conservando el orden.
Devuelve:

L.utils.Converter – El propio objeto de conversión o null, si la propiedad que se intenta definir, no se registró al crear el objeto.

L.utils.Converter.defined

Informa de si todas las propiedades habilitadas tienen definida una conversión

L.utils.Converter.disable(param)

Deshabilita una propiedad del objeto resultante. Esto significa que cuando se obre la conversión del objeto, nunca se intentará obtener el valor de esta propiedad.

Argumentos:
  • param (string) – Nombre de la propiedad.
Devuelve:

L.utils.Converter – El propio objeto.

L.utils.Converter.enable(param)

Habilita una propiedad del objeto resultante.

Argumentos:
  • param (string) – Nombre de la propiedad.
Devuelve:

L.utils.Converter – El propio objeto.

L.utils.Converter.enabled

type: Array.<String>

Las propiedades habilitadas para el objeto resultante.

L.utils.Converter.isDefined(param)

Informa de si la propiedad tiene definida la conversión.

Argumentos:
  • param (String) – El nombre de la propiedad.
Devuelve:

Boolean

L.utils.Converter.params

type: Array.<String>

Las propiedades definidas para el objeto resultante.

L.utils.Converter.run(o)

Lleva a cabo la conversión del objeto suministrado. Sólo se obtienen las propiedades que estén habilitadas y para las que se pueda realizar la conversión, porque exista toda la información requerida en el objeto.

Argumentos:
  • o (Object) – El objeto original
Devuelve:

Object – El objeto resultante.

… HASTA AQUÍ

3.1.2.4. Marcas

leafext.js define el constructor L.MutableMarker que permite definir marcas con iconos mutables y a cuyo datos asociados se le pueden aplicar correcciones y filtros como se verá más adelante.

class MutableMarker()

Marca que soporta iconos mutables y permite realizar correcciones y filtros sobre los datos asociados a ella.

Su definición requiere obligatoriamente incluir la opción mutable, cuyo valor debe ser el atributo de la marca donde se guardarán los datos asociados. Dado que usamos como origen de los datos un objeto GeoJSON y los añadimos al mapa mediante una capa L.GeoJSON , éstos aparecerán dentro de feature.properties, de ahí que hayamos definido Centro así:

const Centro = L.MutableMarker.extend({
   options: {mutable: "feature.properties"}
});

En cualquier caso, si no usa el formato GeoJSON, y, en vez de ello, crea la marca y añade artesanalmente los datos, asegúrese de colocar los datos en la propiedad que ha señalado con mutable.

3.1.2.4.1. Acceso

En el ejemplo de carga la inserción de los datos en la capa, genera para cada uno de ellos la marca que definimos en el método .pointToLayer(). Ahora bien, ¿qué mecanismos tenemos para acceder a estas marcas?

MutableMarker.store

Es un atributo del constructor (o sea de clase) que almacena en un array todas las marcas que se han definido con él:

for(const c in Centro.store) console.log("Hola, soy una marca de centro", c);

Advertencia

Advierta que todas esas marcas pueden no encontrarse en el mapa. Por ejemplo, una que se haya filtrado.

MutableMarker.invoke(metodo, progress, arg1, arg2, ...)

Permite aplicar un método de instancia a todas las marcas incluidas en MutableMarker.store Por ejemplo:

Centro.invoke("on", null, "click", e => console.log("Soy la marca que acabas de pulsar", e.target));

El segundo parámetro es una función que permite informar del progreso de la acción, en los casos en que esta se demore mucho. La función recibe tres argumentos: la posición de la marca de la que se ejcuta el método en el array MutableMarker.store, el total de marcas y el tiempo trascurrido en milisegundos desde que se comenzó a ejecutar .invoke(). Lo que se hace internamente es durante 200ms lanzar sucesivamente el método para cada marca y al cumplirse este tiempo, ejecutar la función progress y suspender durante 50ms la ejecución. Esto permite informar al usuario de cuál es el progreso y desbloquear la interfaz. Durante el primer segundo, se producen suspensiones, pero no se ejecuta progress. Si se prevé que la acción no será muy penosa, pueden evitarse las suspensiones y ejecutar de corrido el método sobre todas las márcanas dando a progress el valor null. Si se desean realizar las interrupciones, para evitar el bloqueo, pero no se quiere mostrar progreso alguna, puede usarse el valor true.

MutableMarker.getData()

Devuelve los datos asociados a la marca tomándolos del atributo que se definió a través de la opción mutable.

Advertencia

Si en algún momento requiere consultar los datos y estos han sufrido alguna corrección, tenga presente que las propiedades que sean *arrays* susceptibles de corrección deben consultarse de un modo particular.

Ver también

Cuando los datos son numerosos y, en consecuencia, las marcas también, es imprescindible usar la extensión L.MarkerClusterGroup para agrupar las marcas cercanas en una sola y que la marca conjunta vaya disgregándose a medida que aumentamos la escala. Consulte el uso de esta capa de clusters más adelante.

MutableMarker.changeData(obj)

Método de instancia que permite modificar directamente los datos de una marca concreta:

centro.changeData({tipo: "dificil"}):
MutableMarker.refresh()

Método de instancia que actualiza el aspecto del icono en caso de que sea necesario, esto es, porque desde la última vez que se refrescó se produjeron cambios en las opciones de dibujo:

centro.refresh()

3.1.2.4.2. Eventos

Además de las eventos definidos por el propio Leaflet, como «click»:

centro.on("click", e => console.log("Soy la marca que acabas de pulsar", e.target));

MutableMarker() tiene definidos otros tipos de eventos:

dataset

Evento que se desencadena en el momento de asociar los datos a la marca:

Centro.invoke("on", null, "dataset", e => console.log("Justamente ahora me acaba de asociar unos datos"));
iconchange

Evento que se dispara cada vez que un icono cambia de aspecto como consecuencia de un cambio en las opciones de dibujo:

centro.on("iconchange", e => console.log(`Se redibuja ${e.target.getData().name}`));

El evento presenta dos atributos relevantes: opts que contiene la lista de opciones de dibujo que cambiaron entre un dibujado y el siguiente; y reason que define la razón por la que se redibuja el icono. Puede tener dos valores:

  • «redraw», el icono ya dibujado, se redibuja porque se forzó su dibujo a través del método MutableMarker.refresh() y había opciones que habían cambiado desde el últimko refresco.
  • «draw», el icono se dibuja porque antes no lo estaba por alguna razón (p.e. se encontraba filtrado) y durante el tiempo en que estuvo filtrado cambiaron las opciones de dibujo, por lo que el aspecto del icono no es el mismo que el que tenía éste cuando desapareció del mapa.

Nota

Hay otros eventos más relacionados con las correcciones y los filtros. que se ctarán a su debido tiempo.

Ejemplo de aplicación

Con lo expuesto hasta ahora, seríamos capaces de construir un mapa con marcas que ajusten su aspecto al valor de sus datos, esto es, que son capaces de realizar el primer punto con que expusimos la utilidad de la librería:

3.1.2.5. Correcciones

El sistema de correcciones permite alterar los datos iniciales de las marcas según una serie de criterios establecidos por el usuario al interaccionar con la interfaz visual. En el ejemplo anterior, podríamos desear «eliminar todas las adjudicaciones que sean de un colectivo determinado». Si el colectivo fuese el de interinos, es claro que las adjudicaciones pasarían de 4 a 3 y de 3 a 2.

Nota

Las correcciones pueden aplicarse, exclusivamente, sobre atributos cuyo valor sea un array.

Hay dos tipos diferentes de correcciones:

  1. Las correcciones que eliminan elementos del array. como es el caso de la corrección de ejemplo que se acaba de enunciar.
  2. Las correcciones que añaden elementos al array.

Es importante, además, tener presente que las correcciones se aplican a un tipo de marca (p.e. la marca Centro) y que, en consecuencia, todas las marcas de este tipo se verán afectadas por la correcciones. En un mismo mapa pueden convivir varios tipos distintos de marcas y cada uno de ellos tendrá un sistema de corrrecciones diferente.

3.1.2.5.1. Definición

Para definir los criterios de corrección es preciso registrar cada criterio sobre el constructor de la marca con el método de clase:

MutableMarker.register(nombre, attr, fn)

Método del constructor que registra en el tipo de marca una corrección sobre el atributo «attr» con nombre «nombre» según la función «fn». Por ejemplo:

Centro.register("adjcol", {
   attr: "adj",
   // opts = {colectivo: ["Interino"]}
   func: function(idx, adj, opts) {
      return !!(opts.inv ^ (opts.colectivo.indexOf(adj[idx]) !== -1));
   }
});

El código crea un corrección de nombre «adjcol» que se aplica sobre la propiedad de los datos adj. Como es una corrección que pretende eliminar elementos, se ejecutará la función suministrada por fn para cada uno de los elementos del array adj, de manera que cuando devuelva true se eliminará el elemento y cuando devuelva false, se conservará. Tal como está escrita la función, se desecharán las adjudicaciones a colectivos que se encuentren en la lista suministrada a través de las opciones. Además se incluye un atributo inv para poder invertir el sentido de la corrección.

Nota

Una corrección sólo puede aplicarse a una única propiedad.

La función usa como contexto la marca sobre la que opera la corrección y tiene tres argumentos:

idx
El índice correspondiente al valor que comprueba la función.
adj
que es el array completo. En el ejemplo, el array adj.
opts
que es un objeto que contiene las opciones que permiten determinar la corrección y cuya obtención será tarea de la interfaz de usuario. Para la definición de ejemplo, se necesitan los colectivos cuya adjudicación deseamos conservar (propiedad colectivo) y una propiedad inv que sirve para invertir el significado.

En caso de que la corrección sirva para añadir elementos, es necesario añadir la propiedad add con valor true en la definición, y durante la aplicación no se recorrerá el array elemento por elemento, sino que la función se ejecutará una vez y deberá devolver un array con los elementos que se desean incorporar. Como idx no tiene sentido en este caso, tomará el valor de null:

Centro.register("vt+", {
   attr: "adj",
   add: true
   func: function(idx, adj, opts) {
      const data = this.getData();
      // Como nuestros datos son muy simples y no hay información alguna,
      // nos inventamos que en todos ha habido dos vacantes telefónicas
      return ["Interino", "Interino"];
   }
});

Advertencia

Aunque tenga disponible el array dentro de la función, no añada los nuevos elementos; limítese a devolverlos.

3.1.2.5.2. Aplicación

El registro de una corrección no provoca ningún cambio en los datos: sólo la define. Para llevar a efecto la corrección es necesario aplicar la corrección mediante el método de clase:

MutableMarker.correct(nombre, opts, encadenamiento)

Método del constructor que aplica una corrección proporcionando las opciones para su aplicación:

Centro.ccrrect("adjcol", {colectivo: ["Prácticas", "Interino"]});

que sutirá efecto en todas las marcas de la clase Centro. En este caso, en todas estas márcas se eliminará del array de adjudicaciones (o sea, adj), los elementos que no representan adjudicaciones a personal en prácticas o interino.

..seealso:: Para la explicación del significado del argumento
encadenamiento, consulte más adelante el epígrafe dedicado al encadenamiento.`

Advertencia

La aplicación de la corrección no altera automáticamente el aspecto de las marcas. Para hacerlo, debe aplicarse el método MutableMarker.refresh() sobre las marcas:

Centro.invoke("refresh");

Nota

Si con posterioridad a la aplicación, se crea una nueva marca de tipo Centro, las correcciones aplicadas a la clase, se aplicarán sobre a la marca en cuanto se conecten a ella los datos.

Se ha afirmado alegremente que se eliminan elementos del array, pero no es cierto, puesto que si se eliminaran sin más no podría revertirse la corrección. En realidad, todos los elementos siguen ahí, e incluso pueden haber aparecido nuevos si hubo correcciones que los añadieron. Para consultar un array con correcciones debe tenerse en cuenta lo siguiente:

.length
Devuelve la cantidad total de elementos: los preexistentes y los añadidos, se hayan eliminado o no. En general, todas las funciones que se aplican al array elemento por elemento (.forEach(), .map(), etc.) actúan de esta forma. En consecuenta, recorrerán todos los elementos, pero no se podrá conocer de ellos si está eliminado o no.
.total

Devuelve el número total de elementos, descontados los eliminados. Por tanto, la conversión de adj en numadj debimos haberla hecho así:

const converter = new L.utils.Converter(["numadj", "tipo"])
                        .define("numadj", "adj", a => a.total)
                        .define("tipo");
for .. of

Devuelve también todos los elementos, eliminados o no, pero cada elemento no es el elemento original, sino un nuevo objeto que se caracteriza por lo siguiente:

  • Si el elemento original era un objeto, devuelve un objeto con las mismas propiedades al que se ha añadido otra llamada filters que es un array con los nombres de las correcciones que filtran el valor. En consecuencia, un filters vacío supone que el valor no se ha filtrado.
  • Si el elemento original no era un objeto, sino un tipo primitivo, se devuelve un objeto en cuyo atributo value se almacena el valor original del elemento. También dispone del atributo filters.

En ambos casos se dispone de un método isPrimitive para saber si el elemento original era o no un objeto en su origen.

Nota

Nótese que esta construcción actúa así, porque se ha alterado el interador del array corregible. En consecuencia, si nuestra propiedad se llama adj:

Array.from(centro.getData().adj)

devolverá un array con los elementos descritos al tratar esta construcción.

3.1.2.5.3. Reversión

Para revertir una corrección, basta con usar:

MutableMarker.uncorrect(nombre)

Método del constructor que revierte la corrección de nombre suministrado a todas las marcas del tipo:

Centro.uncorrect("adjcol");

Advertencia

Tampoco en este caso se refresca el aspecto de las marcas. Por tanto, si quiere trasladar el cambio al aspecto de los iconos:

Centro.invoke("refresh");

Además, existe

MutableMarker.reset(deep)

Método del constructor que desaplica todas las correcciones y vacía MutableMarker.store. Si se proprociona deep con valor true desaplica también los filtros.

3.1.2.5.4. Encadenamiento

En algunos casos, podría darse la circunstancia de que la aplicación de una corrección sobre una propiedad supusiera la aplicación automática de la aplicación de otra corrección. Un ejemplo real podría ser el siguiente: si sólo nos interesan enseñanzas bilingües (corrección sobre la oferta educativa), entonces sólo nos deberían interesar las adjudicaciones a puestos bilingües (corrección sobre las adjucaciones). Como, desgraciadamente, no podemos llevar a la práctica este ejemplo al estar utilizando una versión muy simplificada de los datos, implementaremos un encadenamiento absurdo, pero que sirve para ilustrar cómo se hace: si nos interesan sólo puestos interinos, entonces no nos interesa la enseñanza DAM.

Sin encadenamiento, la definición de nuestras correcciones sería así:

Centro.register("adjcol", {
   attr: "adj",
   func: function(idx, adj, opts) {
      return !!(opts.inv ^ (opts.colectivo.indexOf(adj[idx]) !== -1));
   }
})
      .register("of", {
   attr: "oferta",
   // opts = {ens: ["DAM"] }
   func: function(idx, oferta, opts) {
      return opts.ens.indexOf(oferta[i]) !== -1;
   }
});

Como vemos, hemos definido la corrección «of» para que se desechen las enseñanzas que coincidan con alguna de las que pasemos a través de las opciones. Para el encadenamiento que queremos, podemos hacer la siguiente definición:

Centro.register("adjcol", {
   attr: "adj",
   func: function(idx, adj, opts) {
      return !!(opts.inv ^ (opts.colectivo.indexOf(adj[idx]) !== -1));
   },
   autochain: false,
   chain: [
      {
         corr: "of",
         func: function(opts) {
            if(opts.colectivo.length === 1 && opts.colectivo[0] === "Interino") {
               return {ens: ["DAM"]}
            }
            return false;
         }
      }
   ]
})
     .register("of", {  // Esto no cambia en absoluto.
   attr: "oferta",
   // opts = {ens: ["DAM"] }
   func: function(idx, oferta, opts) {
      return opts.ens.indexOf(oferta[i]) !== -1;
   }
});

O sea:

  • Definimos una cadena de correcciones para adjcol a través del atributo chain. El encadenamiento sólo define que podrá lanzarse automáticamente la corrección of.
  • El atributo autochain permite definir si queremos que el encadenamiento se lleve a cabo automáticamente al aplicar la corrección (true) o si debemos especificarlo al aplicar la corrección.
  • Se debe definir una función que transforme las opciones de adjcol en opciones de of. El contexto de esta función es el tipo de marca (Centro en el ejemplo).
  • Si los valores de las opciones de adjcol, no provocan ningún efecto, entones la función de transformación debe devolver false.

Además al aplicar las correcciones, podemos incluir un argumento adicional que sobreescribe el valor de autochain:

Centro.ccrrect("adjcol", {colectivo: ["Interino"]}, true);

en este caso se llevara a cabo el encadenamiento, a pesar de haber indicado false antes.

Nota

Internamiente no se aplica una corrección of, sino un corrección «adjcol of», por lo que podemos aplicar de forma independiente y manual una corrección of:

Centro.correct("of", {ens: ["SMR"]})

Si se desea conocer para una corrección qué otras correcciones la han aplicado automáticamente y con qué opciones, puede usarse el método:

MutableMarker.getAutoCorrect(nombre)

Devuelve las correcciones manuales que han desencadenado automáticamente la corrección de nombre suministrado:

Centro.getAutoCorrect("of");  // {adjcol: {ens: ["DAM"]}}

3.1.2.5.5. Comprobación de su estado

Para conocer cuál el estado de aplicación de las correcciones sobre las marcas, la clase provee dos métodos del constructor:

MutableMarker.getCorrectStatus()

Devuelve el estado de las correcciones en forma de objeto con dos atributos: aplicadas.manual donde se desglosan las correcciones que se aplicaron manualmente; y aplicadas.auto donde se desglosan las correcciones que se aplicaron automáticamente como consecuencia de algún encadenamiento. El primer atributo es un objeto en el que las claves son los nombres de las correcciones y los valores, sus opciones de aplicación. El segundo objeto es algo más complejo, porque una misma corrección ha podido aplicarse varias veces automáticamente. En este caso, las claves son, de nuevo, los nombres de las correcciones, pero los valores son a su vez un objeto en que las claves son los nombres de las correcciones aplicadas manualmente que provocaron la aplicación automática y los valores las opciones de aplicación automática. Por ejemplo:

const aplicadas = Centro.getCorrectStatus()

que si provoca que el valor de aplicadas fuera:

manual: {
   bilingue: {bil: ["Inglés"], inv: true}}
}
auto: {
   adjpue: {
      bilingue: {puesto: [ "00590107", "DU590107"]}
   }
}

significa que se aplicó manualmente una corrección llamada bilingue con las opciones expresadas. Además, hay aplicada automáticamente una corrección llamada adjpue, consecuencia de la aplicación manual de bilingue y cuyas opciones de aplicación son las indicadas. Obsérvese que estas opciones son opciones de aplicación de adjpue, no de la aplicación automática de bilingüe.

El segundo método útil para conocer las correcciones aplicadas está más relacionado con saber si aplicar una corrección con unas determinads opciones de aplicación, tendrá efecto en el mapa o será inútil porque el estado actual ya supone directa o indirectamente la aplicación de esa corrección. Hay al menos dos escenarios en los que esto es útil:

  1. Si se han aplicado correcciones por alguna razón (p.e. porque al abrir el mapa queremos que se apliquen sin que el usuario tenga que llevarlas a cabo) y es necesario que la interfaz visual se ajuste a ese estado de correcciones que no se han llevado a cabo a través de ella.
  2. Cuando existe encadenamiento de correcciones, la aplicación manual de una corrección A, desencadena la aplicación automática de otra corrección B. De nuevo, esto puede afectar a la interfaz visual, si ésta permitía la aplicación manual de B:

Para llevar a cabo esto, existe el método del constructor:

MutableMarker.appliedCorrections()

Permite saber si la aplicación de una corrección es irrelevante, porque ya existen otras que aplicadas que provacan ese efecto:

Centro.appliedCorrections("adjcol", {colectivos: ["Interino"]}, "auto");

El método admite tres argumentos: el nombre del filtro, las opciones de aplicación y el tipo de comprobación que se desea realizar:

  • auto, sólo comprueba si la aplicación requerida (adjcol con la opción referido) ya está incluida en alguna de las aplicaciones automáticas; y, por tanto, es inútil. Tiene utilidad para resolver el segundo escenario.
  • manual, sólo comprueba si la aplicación requerida ya está incluida en la aplicación manual que se haya hecho anteriormente (si es que se ha hecho). Tine utilidad para resolver el primer escenario.
  • Cualquier otro valor comprueba tanto en la aplicación manual como en las automáticas.

Ahora bien, dado que cada corrección tiene una idiosincrasia propia, para que sea posible comparar opciones de aplicación y determinar si unas implican otras, es necesario que al registrar la corrección se indique cuál es el algoritmo. Por tanto:

// Desecha las enseñanzas que se facilitan.
Centro.register("of", {
   attr: "oferta",
   // opts = {ens: ["DAM"] }
   func: function(idx, oferta, opts) {
      return opts.ens.indexOf(oferta[i]) !== -1;
   },
   // Si las opciones contienen al menos todas las enseñanzas
   // que tienen las nuevas, entonces la aplicación actual es más
   // restrictiva.
   apply: function(opts, newopts) {
      for(const ens of newopts.ens) {
         // Hay una enseñanza en las nuevas opciones que no está en las actuales.
         if(opts.ens.indexOf(ens) === -1) return false;
      }
      return true;
   }
});

La función compara las opciones actuales (opts) con las nuevas opciones (newopts) y devuelve true si la aplicación de las nuevas opciones no provocase ningún cambio en caso de poder aplicar la corrección sin desaplicar la aplicación actual.

Nota

En caso de que no se facilite ninguna función, la comparación se limitará a ver si las opciones de aplicación son iguales.

3.1.2.5.6. Eventos

La aplicación y eliminación de correcciones sobre el tipo de marca tiene asociados eventos.

correct:nombre

Evento aplicable sobre el constructor de la marca que se desencadena cuando se aplica la corrección indicada con nombre:

Centro.on("correct:adjcol", e => {
   const modo = e.auto?"automáticamente":"manualmente";
   console.log(`Ha aplicado ${modo} una corrección ${e.name}:`, e.opts);
});

El evento tiene los siguientes atributos relevantes (además de target y type, claro está):

name
Contiene el nombre de la corrección.
auto
Booleano que informa de si la corrección se aplicó manualmente (false) o fue consecuencia de un encadenamiento (true).
opts
Opciones con las que se aplicó la corrección.
uncorrect:nombre
Evento análogo al anterior que se desencadena al eliminar la corrección especificada con nombre.

Nota

Es posible usar «correct:*» y «uncorrect:*» para que el evento esté asociado a cualquier tipo de corrección.

3.1.2.6. Filtros

El sistema de filtros posibilita eliminar entidades que cumplan con los criterios que establezcamos. Para habilitar el sistema de filtros es necesario añadir la opción filter al crear la clase de marca:

const Centro = L.MutableMarker.extend({
   options: {
      mutable: "feature.properties",
      filter: layer
   }
});

En principio, le daremos como valor a filter la capa en la que se insertarán las marcas (que habíamos llamado layer), aunque pueden facilitarse otros valores (véase estilos de filtros). Obrando así, el efecto del filtrado es que desaparecerán totalmente del mapa las marcas filtradas.

3.1.2.6.1. Definición

De manera análoga a como se obra con las correcciones, antes de poder aplicar filtros es necesario registrarlos:

MutableMarker.registerF(nombre, attrs, func)

Método del constructor que define un filtro para las marcas definidas con él. Por ejemplo, este filtro sirve para filtrar centros que tengan menos de un número mínimo de adjudicaciones:

Centro.registerF("adjmin", {
   attrs: "adj",
   func: function(opts) {
      return this.getData().adj.total < opts.min;
   }
});

Para el registro del filtro es necesario un nombre (adjmin en el código de ejemplo) y un objeto con dos propiedades:

attrs
Es la lista de propiedades de los datos involucradas en el cálculo. Debe ser un array, pero si es una propiedad sola, podemos ahorramos el array y escribir directamente el nombre de la propiedad.
func
Función que define si el centro se filtra (devuelve true) o no (devuelve false). Su contexto es la propia marca que se desea comprobar.

3.1.2.6.2. Aplicación

Para aplicar un filtro registrado, basta con pasar su nombre y cuáles son las opciones de filtro, de forma análoga a como se hace con las correcciones.

MutableMarker.filter(nombre, opts)

Método del constructor que aplica a todas las marcas de su tipo el filtro de nombre indicado con las opciones de filtro indicadas. Por ejemplo, si quisiéramos eliminar los centros sin adjudicaciones, deberíamos aplicar el filtro «adjmin» del siguiente modo:

Centro.filter("adjmin", {min: 1});

La aplicación del filtro afecta a las marcas que en ese momento se hayan creado, afectará a las futuras y se recalculará cada vez que se aplique una corrección que modifique alguna de las propiedades que listamos en attrs. Ahora bien, como en el caso de las correcciones, los cambios sólo se trasladarán al dibujo cuando refresquemos las marcas:

Centro.invoke("refresh");

3.1.2.6.3. Reversión

Para eliminar un filtro:

MutableMarker.unfilter(nombre)

Método del constructor que desplica el filtro de nombre reseñado a las marcas de su tipo:

Centro.unfilter("adjmin");

Obviamente, deberemos refrescar para trasladar el efecto de la acción al dibujo.

3.1.2.6.4. Comprobación de su estado

La marca dispone de dos métodos para hacer comprobaciones sobre los filtros aplicados:

MutableMarker.hasFilter()

Método del constructor que informa de si se ha aplicado el filtro:

Centro.hasFilter("adjcol");  // Devuelve true o false.
MutableMarker.getFilterStatus()

Métoodo del constructor qe devuelve un objeto cuyas claves son los nombres de los filtros aplicados; y cuyos valores, las opciones correspondientes de aplicación:

Centro.getFilterStatus();

3.1.2.6.5. Eventos

De manera análoga a como hay definidos eventos para la aplicación y remoción de correcciones también se definen para la aplicación y remoción de correcciones, usando la misma sintaxis.

filter:nombre

Evento del constructor que se desencadena al aplicar el filtro de nombre reseñado:

Centro.on("filter:adjmin", e => {
   console.log(`Ha aplicado el filtro ${e.name}:`, e.opts);
});

El evento dispone de los mismos atributos que en el caso de las correcciones, con la salvedad de auto, que no tiene sentido en este caso.

unfilter:nombre

Evento del constructor que se desencadena al desaplicar el filtro de nombre reseñado:

Centro.on("unfilter:adjmin", e => {
   console.log(`Ha desaplicado el filtro ${e.name}:`, e.opts);
}):

Nota

Puede usar filter:* y unfilter:* si quiere definir acciones que se desencadenen con la aplicación o desaplicación de cualquier filtro.

Además de estos estos eventos aplicables sobre el constructor, las marcas individuales definen los eventos filtered y unfiltered que se desencadenan cuando la marca cambia de no filtrada a filtrada y de filtrada o no filtrada respectivamente.

filtered

Evento asociado a cada marca individual que se desencadena al pasar un centro de no estar filtrado a estarlo:

centro.on("filtered", e => {
   console.log(`Me acaba de filtrar el filtro '${e.name}' por culpa de:`, e.opts);
});

El evento añade dos atributos:

name
Contiene el nombre del filtro que provoca el cambio.
opts
Contiene las opciones de filtro.
unfiltered
Evento asociado a cada marca individual que se desencadena al pasar un centro de estar filtrado a no estarlo.

3.1.2.6.6. Estilos

Hasta el momento, el comportamiento de las marcas filtradas es desaparecer del mapa y esto es debido a que dimos a la opción filter como valor la capa en la que se agregan las marcas. Sin embargo, existe la alternativa de no hacer desaparecer la marca, sino cambiar su aspecto para notar que está filtrada. para ello podemos usar dos valores alternativos para la opción:

  • Un nombre, que hará que el elemento HTML que representa la marca filtrada se incluya en la clase CSS de tal nombre:

    const Centro = L.MUtableMarker.extend({
       options: {
          mutable: "feature.properties",
          filter: "filtrado"
       }
    });
    

    Sin dentro del CSS, incluimos esta definición:

    .filtrado {
       filter: grayscale(100%);
    }
    

    El efecto es que las marcas filtradas aparecerán en gris, y no en color.

  • Una función que toma como contexto el elemento HTML y lo modifica a voluntad:

    const Centro = L.MutableMarker.extend({
       options: {
          mutable: "feature.properties",
          filter: function(filtered) {
             if(filtered) this.style.filter = "grayscale(100%)";
             else this.style.removeProperty("filter");
          }
       }
    });
    

que tiene el mismo efecto que el código anterior. En concreto, para este efecto, la librería ya tiene definida una función que puede usarse directamente:

const Centro = L.MutableMarker.extend({
   options: {
      mutable: "feature.properties",
      filter: L.utils.grayFilter
   }
});

Una vez definida la clase, es posible modificar el estilo posteriormente

MutableMarker.setFilterStyle(estilo)

Método del constructor que permite modificar el estilo de filtrado para los iconos de marca de su tipo. El argumento estilo puede tomar los valores descritos para la opción filter que se pasa al definir el constructor:

Centro.setFilterStyle(layer);  // Volvemos a ocultar los centros al filtrarlos.

En este caso, a diferencia de cuando se aplican filtros y correcciones, el redibujado de marca se hace automáticamente.

Nota

Cuando el estilo de filtro no elimina las marcas del mapa y se usa una capa MarkerClusterGroup, el número del cluster incluirá las marcas filtradas, ya que estas siguen en el mapa. Para evitarlo y que sólo represente las marcas no filtradas puede cambiarse la función que crea los iconos para los clusters y pasarla a través de la función iconCreateFunction. La librería trae ya una hecha con este fin:

const layer = L.markerClusterGroup({
   iconFunctionCreate: L.utils.noFilteredIconCluster
}).addTo(map);

Ejemplo de aplicación

3.1.2.7. Utilidades

L.utils.noFilteredIconCluster(cluster)

Redefine iconCreateFunction basándose en la definición original de L.MarkerClusterGroup para que el número del clúster sólo cuente los centros no filtrados.

Argumentos:
  • cluster (L.MarkerCluster) – El cluster sobre el que se aplica la función.
L.utils.grayFilter(filtered)

Pone en escala de grises un icono filtrado o elimina tal escala si ya no lo está.

Argumentos:
  • filtered (boolean) – Si el icono está filtrado o no.
L.utils.load(params)

Realiza peticiones AJAX. Las peticiones serán asíncronas, a menos que no se proporcionen función de callback ni failback.

Argumentos:
  • params (Object) – Objeto que contiene los parámetros para realizar la petición.
  • params.url (String) – URL de la petición.
  • params.method (String) – Método HTTP de petición. Por defecto, será GET, si no se envía parámetros y POST, si sí se hace.
  • params.params (Object) – Parámetros que se envían en la petición
  • params.callback (function) – Función que se ejecutará si la petición tiene éxito. La función tendrá como único argumento el objeto XMLHttpRequest.
  • params.failback (function) – Función que se ejecutará cuando la petición falle. También admite como argumento un objeto XMLHttpRequest.
  • params.context (Object) – Objeto que usará como contexto las funciones de callback y failback.

Examples:

load({
   url: 'image/centro.svg',
   callback: function(xhr) { console.log("Éxito"); },
   failback: function(xhr) { console.log("Error"); },
});

3.1.3. Variantes

Planteamos bajo este epígrafe algunas variantes interesantes sobre el uso ya ilustrado.

3.1.3.1. Leaflet.markercluster

Cuando las marcas son numerosas, es indispensable usar esta extensión que permite agrupar marcas cercanas e irlas desglosando según ampliamos la escala. leafext.js es compatible con una capa L.MarkerClusterGroup, aunque convendría tener claro cómo manejar datos en formato GeoJSON con ella. Lo más sencillo es usar una capa L.GeoJSON intermedia, que se encargue de interpretar los datos:

const layer = L.markerClusterGroup({
   showCoverageOnHover: false,
   // Al llegar a nivel 13 de zoom se ven todas las marcas.
   disableClusteringAtZoom: 13,
   spiderfyOnMaxZoom: false
   iconFunctionCreate: L.utils.noFilteredIconCluster
}).addTo(map);

// Obsérvese que no la añadimos al mapa.
const interm =  L.geoJSON(datos, {
   pointToLayer: (f, p) => new Centro(p, {
      icon: new Icono(),
      title: f.properties.name
   })
});

//Pasamos las marcas individuales a la capa de clústers
layer.addLayer(interm);

interm.clearLayers();

Tenga presente que la marca intermedia no se añade a la capa L.MarkerClusterGroup, sino las marcas individuales que se encuentran en ella. Por ese motivo, una vez pasadas las marcas, eliminamos las marcas de la capa L.GeoJSON para poder seguir utilizándola como intermediaria.

Obsérvese que utilizando el código susodicho, se construyen todas las marcas mientras se introducen en la capa intermedia y, ya creadas todas, se añaden del tirón a la capa final. Una variante, quizás más interesante, es añadirlas a la capa final, según las van creando la intermedia:

const layer = L.markerClusterGroup({
   showCoverageOnHover: false,
   // Al llegar a nivel 14 de zoom se ven todas las marcas.
   disableClusteringAtZoom: 14,
   spiderfyOnMaxZoom: false
}).addTo(map);

// Obsérvese que no la añadimos al mapa.
const interm =  L.geoJSON(datos, {
   pointToLayer: (f, p) => new Centro(p, {
      icon: new Icono(),
      title: f.properties.name
   }),
   onEachFeature: (f, l) => layer.addLayer(l)
});

interm.clearLayers();

La ventaja de este código sobre el anterior es que cada vez que creamos y añadimos de modo efectivo una marca al mapa (o sea a la capa L.MarkerClusterGroup) podemos lanzar un disparador con:

layer.on("layeradd", e => console.log(`Creado y añadido el centro ${e.layer,getData().name}`));

Con el primer codigo, en cambio, las marcas se creaban todas antes de añadirse la primera al mapa.

3.1.3.2. Datos que no son GeoJSON

Cuando los datos no están en formato GeoJSON, la capa L.GeoJSON nos sirve de poco y debemos de ser nosotros los que creemos la marca y añadamos a ella los datos. Supongamos que los datos son estos:

{
   "centros": [
      {
         "name": "Centro 1",
         "lng": -5.9526,
         "lat": 37.275475,
         "adj": ["Suprimido", "Concursillo", "Concursillo", "Interino"],
         "oferta": ["SMR", "DAM", "BACHILLERATO"],
         "tipo": "normal"
      },
      {
         "name": "Centro 2",
         "lng": -4.6389,
         "lat": 37.58434,
         "adj": ["Concursillo", "Expectativa", "Interino"],
         "oferta": ["SMR", "ASIR"],
         "tipo": "dificil"
      }
   ]
}

o sea, los mismos datos de antes, pero sin el formato GeoJSON. En ese caso, podríamos escribir un función que para cada centro creara su marca correspondiente y la añadiera a la capa:

const Icono = crearIcono();

const Centro = L.Marker.extend({
   options: {mutable: "data"}
});

map = L.map("map").setView([37.07, -6.27], 9);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 18
}).addTo(map);

const layer = L.featureGroup();  // También podria ser L.markerCluster.

funcion CrearMarca(d) {
   const m = new Centro([d.lat, data.lng]{
      icon: new Icon();
      title: d.name;
   });
   delete d.lat;
   delete d.lng;
   m.data = d;  // En consonancia con el valor de mutable.

   return m;
}

for(const d of datos) layer.addLayer(crearMarca(d));

3.1.3.3. Barra de progreso

Por hacer

Corregir esto, porque se queda suspendido el navegador hasta ue acaba la carga.

Los ejemplos que acompañan a Leaflet.Markercluster incluyen alguno con una sencilla barra de progreso que informa de cómo va el procesamiento de datos y su adición al mapa en forma de marca[5]. Se basa en la las opciones chuckedLoading y chuckProgress de L.MarkerClusterGroup, que son útiles cuando se añaden de una sola vez muchos datos a la capa, como es el caso del primer ejemplo incluido en el apartado dedicado a discutir sobre este tipo de capa. Sin embargo, si usamos otro tipo de capa o si utilizamos ésta, pero añadiendo una a una las marcas., tal solución se vuelve impracticable. Con todo, podemos construirnos nuestra propia solución con ayuda del evento layeradd. Necesitamos un HTML como este:

<div id="progress"><div id="progress-bar"></div></div>
<div id="map"></div>

Un CSS para la barra que tomamos del ejemplo original:

#progress {
    display: none;
    position: absolute;
    z-index: 1000;
    top: calc(50% - 10px);
    left: calc(50% - 100px);
    width: 200px;
    height: 20px;
    margin-top: -20px;
    margin-left: -100px;
    background-color: #fff;
    background-color: rgba(255, 255, 255, 0.7);
    border-radius: 4px;
    padding: 2px;
}

#progress-bar {
    width: 0;
    height: 100%;
    background-color: #76A6FC;
    border-radius: 4px;
}

Y añadir algo de javascript a las soluciones anteriores:

const layer = L.markerClusterGroup({
   showCoverageOnHover: false,
   // Al llegar a nivel 14 de zoom se ven todas las marcas.
   disableClusteringAtZoom: 14,
   spiderfyOnMaxZoom: false
}).addTo(map);

// Barra de progreso.
(function() {
   const progress = document.getElementById('progress'),
         progressBar = document.getElementById('progress-bar'),
         total = datos.length,
         incr = 2;      // Cada qué % se actualiza la barra.

   let i = 0;

   progressBar.style.width = "0";

   const start = Date.now(),
         step  = Math.max(Math.round(total/100/incr), 1);

   function progressB(e) {
      i++;
      if(i%step === 0 && (Date.now() - start) > 1000) {
         progress.style.display = "block";
         progressBar.style.width = Math.round(i*100/total) + "%";
      }
      if(i === total) {
         progress.style.display = "none";
         layer.off("layeradd", progressB);
      }
   }

   layer.on("layeradd", progressB);
})();

const interm =  L.geoJSON(datos, {
   pointToLayer: (f, p) => new Centro(p, {
      icon: new Icono(),
      title: f.properties.name
   }),
   onEachFeature: (f, l) => layer.addLayer(l)
});

interm.clearLayers();

Notas al pie

[1]O sea, una clase de icono que se utilizará en todas las marcas de un mismo tipo.
[2]Estamos reproduciendo la definición anterior, pero en este caso debemos añadir un contenedor <div> extra.
[3]El por qué se usa la clase «.chupachups» en este trozo de CSS se descubrirá más adelante.
[4]Tenga presente que Leaflet envuelve la definición que con html o url hayamos hecho en un elemento <div>, y es este elemento el que representa this.
[5]Lo que no se incluye es el tiempo de descarga del fichero de datos que es anterior a todo el proceso.