La stilizzazione dei vettoriali è una delle funzionalità più avanzate di OpenLayers. Tramite gli stili e le funzioni di rendering di OL si possono ottenere vestizioni cartografiche molto elaborate, tanto da poter riprodurre stili complessi come quelli delle mappe di OpenStreetMap direttamente nel browser. Cominciamo intanto a vedere cosa è uno stile in OpenLayers.

Gli oggetti di stile

In OL uno stile è una composizione gerarchica di oggetti del namespace ol.style:

  • ol.style.Circle: rappresentazione di elementi puntuali tramite simboli circolari
  • ol.style.Icon: simbolo basato su un’immagine (icona / marker)
  • ol.style.RegularShape: simbolo definito come forma geometrica regolare (stelle, quadrati, rettangoli)
  • ol.style.Stroke: gestione delle linee dei simboli (cerchi, linee)
  • ol.style.Fill: colore di riempimento dei simboli (cerchi, poligoni)
  • ol.style.Text: gestione del testo delle etichette

Alla base della creazione di uno stile c’è la definizione di una lista di oggetti ol.style.Style, ovvero una pila contenente uno o più simboli (concettualmente del tutto simile alla pila dei layer della mappa) configurati tramite uno o più oggetti ol.style.* della lista suddetta. Un esempio ci aiuta a chiarire il concetto.

Nell’esempio in cui abbiamo creato il layer contenente il centroide del comune di Firenze, abbiamo definito il seguente stile:

var style = new ol.style.Style({
    image: new ol.style.Circle({
        radius: 6,
        fill: new ol.style.Fill({
            color: [0, 153, 255, 1]
        }),
        stroke: new ol.style.Stroke({
            color: [255, 255, 255, 1],
            width: 1.5
        })
    })
})

In questo caso è stato definito un unico simbolo, del quale abbiamo usato (valorizzato) la proprietà image. In pratica abbiamo detto che quel simbolo dev’essere rappresentato tramite un qualsiasi oggetto ammesso nella proprietà image di ol.style.Style. Dalle API si evince che in quella proprietà possiamo inserire una qualsiase sottoclasse di ol.style.Image, tra cui c’è proprio ol.style.Circle. Abbiamo quindi istanziato un ol.style.Circle e abbiamo configurato alcune delle sue proprietà:

  • radius: raggio del cerchio in pixel
  • fill: colore di riempimento del cerchio, tramite l’oggetto ol.style.Fill che ha l’unica proprietà color
  • stroke: linea di contorno del cerchio, tramite l’oggetto ol.style.Stroke che ha le proprietà width (spessore in pixel della linea) e color (colore della linea)

I colori possono essere espressi come array [R,G,B,alpha] oppure in formato esadecimale “#rrggbb”.

Il risultato di questa definizione è che tutte le geometrie puntuali del layer vengono rappresente allo stesso modo, con un cerchio celeste e bordo bianco. Ma se volessimo differenziare i dati in base ad un attributo? Qui entrano in gioco le funzioni di stile.

Supponiamo di avere un layer puntuale di punti d’interesse (POI) con i campi id, tipo e quota. Vogliamo differenziare il colore del simbolo in base al tipo.

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
var style = function(feature, res) {
    var fillColor = [0, 153, 255, 1];
    switch(feature.get("tipo")) {
        case "A":
            fillColor = "#ff0000"
        break;
        case "B":
            fillColor = "#00ff00"
        break;
        case "C":
            fillColor = "#00ffff"
        break;
    };
    
    return new ol.style.Style({
        image: new ol.style.Circle({
            radius: 6,
            fill: new ol.style.Fill({
                color: fillColor
            }),
            stroke: new ol.style.Stroke({
                color: [255, 255, 255, 1],
                width: 1.5
            })
        })
    })
}
  1. Definiamo una funzione di stile (riga 1) che viene chiamata da OL ad ogni render di ogni singola feature. Gli argomenti sono la feature stessa che sta per essere disegnata e la risoluzione attuale della mappa
  2. All’interno della funzione possiamo decidere qualsiasi strategia per differenziare i dati in base alla feature e quindi alle sue proprietà, oppure in base al livello di zoom. Qui semplicemente assegniamo il colore di riempimento fillColor in base all’attributo tipo della feature (riga 3 e seguenti)
  3. La funzione deve ritornare un simbolo, per cui lo costruiamo utilizzando il valore di riempimento definito dinamicamente (riga 15 e riga 19)

Il risultato sarà il seguente:

POI

Se alla proprietà style di ol.layer.Vector passiamo una array di istanze ol.style.Style (simboli) questi verranno disegnati nell’ordine dell’array, con il primo simbolo in fondo e l’ultimo in alto.

L’altro aspetto utilissimo ai fini della rappresentazione cartografica è il fatto che nella funzione di generazione di uno stile possiamo sapere la risoluzione attuale della mappa. Questo consente ad esempio di

  • selezionare le feature da rappresentare in base al livello di zoom
  • calcolare le dimensioni dei simboli e delle loro proprietà in base allo zoom, ad esempio per aumentare/diminuire la dimensione dei simboli circolari proporzionalmente alla scala
  • arricchire lo stile di simboli via via che la scala cartografica aumenta (zoom maggiori)
  • ecc.

Modificare la geometria da usare per la vestizione

Le potenzialità dei simboli di OL si completano con la possibilità di generare al volo la geometria da usare per l’applicazione dello stile. Nel prossimo esempio useremo, nel quale utilizziamo un layer poligonale di alcuni comuni toscani, oltre a definirne il colore in base ad un attributo e a rappresentarne il nome con un’etichetta (oltre un certo zoom), ne alteriamo la rappresentazione geometrica quando la risoluzione è al di sopra di 200 m/pixel, usandoe il centro dell’extent del poligono anziché la geometria originale.

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
var style = function(feature, resolution){
	var color = null;
	switch(feature.get('COD_PRO')){
		case 47:
			color = [255,0,0,0.5]
			break;
		case 48:
			color = [0,255,0,0.5]
			break;
		case 100:
			color = [0,0,255,0.5]
			break;
	}
	
	var text = null;
	if (resolution < 100) {
		text = new ol.style.Text({
			text: feature.get('COMUNE'),
			font: 'bold 14px sans-serif',
			fill: new ol.style.Fill({
				color: [100,100,100,1]
			})
		});
	}
	
	if (resolution > 200) {
		var extent = feature.getGeometry().getExtent();
		var center = ol.extent.getCenter(extent);
		var geometry = new ol.geom.Point(center);
		
		style = new ol.style.Style({
			image: new ol.style.Circle({
				radius: 6,
				fill: new ol.style.Fill({
					color: color
				}),
				stroke: new ol.style.Stroke({
					color: [100,100,100,1],
					width: 1
				})
			}),
			geometry: geometry
		});
	}
	else {
		var style = new ol.style.Style({
			fill: new ol.style.Fill({
				color: color
			}),
			stroke: new ol.style.Stroke({
				color: [100,100,100,1],
				width: 1
			}),
			text: text,
			geometry: geometry
		});
	}
	
	return [style];
};

Le possibilità diventano infinite, il limite è dato solo dalle performance del motore geometrico browser, che comunque ai giorni nostri permette di ottenere risultati avanzati anche su numeri elevati di feature. In questo ulteriore esempio, utilizzato nella rappresentazione di un grafo fluviale, ritorno un array di simboli: uno per rappresentare gli elementi lineari ed uno per rappresentare, tramite un’immagine, il verso del flusso idrico. In questa porzione di codice mostro solo una parte del codice usata per generare questo simbolo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(...)
var geometry = feature.getGeometry();
geometry.forEachSegment(function(start, end) {
    var dx = end[0] - start[0];
    var dy = end[1] - start[1];
    var rotation = Math.atan2(dy, dx);
    styles.push(
        new ol.style.Style({
            geometry: new ol.geom.Point(end),
            image: new ol.style.Icon({
                src: '../img/arrow.png',
                anchor: [0.75, 0.5],
                rotateWithView: false,
                rotation: -rotation
            })
        })
    );
});
(...)
  • Sfrutto il metodo ol.geom.LineString.forEachSegment() che mi permette di eseguire una funzione ciclando su ogni segmento di linea che della geometria lineare
  • calcolo l’angolo orientato di ogni segmento
  • aggiungo all’array degli stili un simbolo per ogni segmento, usando come geometria puntuale il vertice finale del segmento

ottenendo il seguente risultato:

Complex style

Nel caso in cui anche questo non bastasse OpenLayers dà la possibilità di disegnare manualmente i simboli usando le funzioni di basso livello del motore geometrico del canvas. Per farlo è pssibile definire una funzione ol.style.StyleRenderFunction da passare alla proprietà renderer di ol.style.Style. Ma noi ci fermiamo qua!

Chiudiamo con il riferimento ad un esempio in cui viene definito uno stile simile ad OSM applicato ad un layer vettoriale di tile MVT.