
Dare il giusto rilievo a dati di natura geografica non è sempre semplice. Non basta, cioè, inserire una mappa con qualche marker in una pagina per aggiungere valore alla nostra soluzione. In questo articolo parleremo delle best practices riguardanti dati geografici che usiamo in Blexin.
L’esempio sarà un progetto Angular e useremo come dati di esempio un sottoinsieme di un dataset che ho pubblicato in una repository pubblica di Github contenente informazioni sugli stadi di calcio nel mondo. Il formato di dati è JSON e per ogni stadio sono disponibili le seguenti informazioni:
{
"Id":8233,
"Name":"10 de Diciembre",
"Nation":"Mexico",
"Town":"Ciudad Cooperativa Cruz Azul",
"Latitude":19.99395751953125,
"Longitude":-99.325408935546875,
"Capacity":"17.000"
}
Useremo strumenti messi a disposizione di Google per i quali è necessario creare una chiave API sulla developer console. La chiave può essere inserita nel progetto angular all’interno del file environment.ts nel folder src/environments.
Il campione di dati a disposizione contiene 46 stadi ripartiti nella maniera seguente:

Questa suddivisione è già un’informazione importante che può essere mostrata su una mappa. Google offre all’interno della sua suite di strumenti di visualizzazione dati un Geochart particolarmente adatto allo scopo. In mancanza di una libreria di componenti Angular ufficiale, abbiamo scelto un wrapper della libreria Google Chart. Lasciando i dettagli dell’installazione alla documentazione disponibile su Github, veniamo ai due problemi principali:
- La formattazione dei dati
- La customizzazione dell’aspetto grafico
I dati vanno trasformati secondo lo standard dettato dalla libreria Google Chart:
geoSummary = [
['Indonesia', 10],
['Algeria', 6],
['Argentina', 7],
['Italy', 10],
['Australia', 4],
['Netherlands', 4],
['Mexico', 5],
];
Una possibile scelta delle impostazioni grafiche del chart può essere la seguente:

Per l’asse dei valori, è possibile scegliere un array di stringhe indicanti colori (colorAxis). Nell’esempio ho utilizzato [‘purple’,’blue’,’green’]. E’ possibile indicare il colore per le nazioni che non hanno rappresentanza (datalessRegionColor) e poi il colore di sfondo della mappa (backgroundColor). Una scelta alternativa è quella dei markers che hanno un raggio proporzionale al valore da mostrare (displayMode: ‘markers’):

Il codice typescript che imposta il componente è il seguente:
private SetChart(data: any): MyChart {
const mychart: MyChart = {
type: 'GeoChart',
data,
options: {
colorAxis: {colors: ['purple', 'blue', 'green']},
backgroundColor: '#81d4fa',
datalessRegionColor: 'white'
}
};
return mychart;
}
Questo è invece il template html:
<google-chart class="chart"
[height]="400"
[type]="myChangingCart.type"
[data]="myChangingCart.data"
[options]="myChangingCart.options"
(select)="onSelect($event)">
</google-chart>
Il componente google-chart offre l’evento di selezione di una nazione (intesa come poligono) sulla mappa (select). L’argomento passato offre una proprietà row collegabile all’array di dati utilizzato. La nazione selezionata (come nome) verrà utilizzata per mostrare la mappa di dettaglio.
onSelect(event: ChartEvent) {
if (event && event[0] && event[0].row >= 0) {
const row = event[0].row;
this.selectedNation = this.geoSummary[row][0];
}
}
Il progetto contiene un componente di dettaglio consistente in una mappa di Google mostrante gli stadi della nazione selezionata. Al cambiare della selezione nel componente principale, la mappa viene centrata sulla nazione corrispondente e i marker degli stadi vengono aggiornati.

Al componente di dettaglio (app-details), quindi, viene passata come @Input() la stringa indicante la nazione selezionata:
<app-details [nation]='selectedNation'></app-details>
app-details utilizza un componente molto noto chiamato Angular Google Maps (agm) disponibile al seguente indirizzo.
<agm-map #map [zoom]="zoom" [latitude]="latitude" [longitude]="longitude"
[mapTypeId]="'hybrid'">
</agm-map>
app-details implementa OnChanges nel metodo ngOnChanges, dove estraiamo da un servizio gli stadi appartenenti alla nazione selezionata. Nel subscribe al metodo del servizio gestiamo anche il cambio del centro della mappa, calcolato usando le latitudini e longitudini degli stadi.
ngOnChanges(changes: SimpleChanges): void {
this.zoom = 4;
this.nation = changes.nation.currentValue;
if (this.nation.length > 0) {
this.data.getArenas(this.nation).subscribe(x => {
this.arenas = x;
this.center = this.centerMap();
});
}
}
Non basta però calcolare le medie aritmetiche delle latitudini e longitudini. Questo metodo funziona bene a basse latitudini ma tende a peggiorare a latitudini più alte fino a divergere all’avvicinarsi ai poli. Il metodo convenzionale è quello di convertire latitudine (ϕi) e longitudine (λi) in un punto tridimensionale usando la seguente formula:

Ora è possibile trovare la media di questi punti tridimensionali (ossia un punto sulla sfera) e convertirla nuovamente in latitudine longitudine a partire dalla seguente relazione:


Il codice contiene una classe helper per gestire tutte queste operazioni aritmetiche di conversione. Il calcolo del centro della mappa a partire dall’array degli stadi è il seguente:
private centerMap(): MyCoordinates {
const helper = new GeographicalHelper();
const total = this.arenas.length;
if (total > 0) {
const points = this.ThreeDimensionalPointsOfArenas(this.arenas);
const averagePoint = helper.calculateAveragePoint(points);
return helper.convertThreeDimensionalPointToLatitudeAndLongitude(averagePoint);
}
return { latitude : 0, longitude : 0};
}
private ThreeDimensionalPointsOfArenas(arenas: Arena[]): ThreeDimensionalPoint[] {
const helper = new GeographicalHelper();
const points = [];
arenas.forEach( arena => {
points.push(helper.convertLatitudeAndLongitudeToThreeDimensionalPoint({latitude: arena.Latitude, longitude: arena.Longitude}));
});
return points;
}
Il componente agm-map consente di inserire dei marker sulla mappa. Nel nostro esempio, per ogni arena inseriamo un marker. E’ possibile anche gestire l’evento di click del singolo marker. E’ sufficiente innestare nel componente agm-map un *ngFor.
<agm-map [zoom]="zoom" [latitude]="center.latitude" [longitude]="center.longitude"
[mapTypeId]="'hybrid'">
<agm-marker *ngFor="let locationItem of arenas;"
[latitude]="locationItem.Latitude" [longitude]="locationItem.Longitude"
(markerClick)="clickedMarker($event)">
</agm-marker>
</agm-map>
Possiamo sfruttare l’evento markerClick per centrare la mappa sul marker e aumentare lo zoom.
clickedMarker(info: any) {
this.zoom = 16;
this.center.latitude = info.latitude;
this.center.longitude = info.longitude;
}
Ad ogni marker, infine, possiamo associare una finestra informativa (agm-info-window) per mostrare informazioni più dettagliate sullo stadio selezionato.
<agm-marker *ngFor="let locationItem of arenas;"
[latitude]="locationItem.Latitude" [longitude]="locationItem.Longitude"
(markerClick)="clickedMarker($event)">
<agm-info-window #info [disableAutoPan]="true">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{locationItem.Name}}</h5>
<h6 class="card-subtitle mb-1">Capacity:{{locationItem.Capacity}}</h6>
<a (click)="findHotels(locationItem)" class="card-link">
Click to find nearby hotels</a>
</div>
</div>
</agm-info-window>
</agm-marker>
Nella agm-info-window possiamo aggiungere un link per avviare ulteriori ricerche (ad esempio gli alberghi in zona):

La repository del progetto è disponibile al seguente indirizzo.
Alla prossima!