facebook

Blog

Stay updated

Let's see how to show geographic data with Google Maps and Angular
Maps <3 Angular
Wednesday, June 19, 2019

Giving the right emphasis on geographic data is not always easy. Putting some markers on a map inside a web page it’s not enough to add value to our solution. In this article, we’ll show the best practices on geographical data used in Blexin.

We’ll show an Angular project using a sub-sample of a large public dataset I published on a public Github repository containing information about football stadiums around the world. The data are in JSON format. For each stadium, the following details are available:

{
 "Id":8233,
 "Name":"10 de Diciembre",
 "Nation":"Mexico",
 "Town":"Ciudad Cooperativa Cruz Azul",
 "Latitude":19.99395751953125,
 "Longitude":-99.325408935546875,
 "Capacity":"17.000"
}

We’ll use tools based on Google libraries, which need an API key that you can create on the developer console. Usually, inside the angular project, you put the key in the src/environment.ts file, in src/environments folder.

The dataset contains 46 stadiums grouped in the following way:

This grouping is indeed an interesting information to show on a map. Google Visualization Data Tools offers a Geochart component particularly interesting for our purpose. Unfortunately, there isn’t an official Angular porting of this component. Thus, we choose a wrapper available on Github. Leaving the installation details to the documentation, we can devote our attention to the following two problems:

  1. Data formatting
  2. Graphic customization

We need to transform the data according to the Google Chart standard:

geoSummary = [
  ['Indonesia', 10],
  ['Algeria', 6],
  ['Argentina', 7],
  ['Italy', 10],
  ['Australia', 4],
  ['Netherlands', 4],
  ['Mexico', 5],
];

Possible graphic customization for our chart may be the following:

We can choose for the value axis a string array containing the colors we want to use (colorAxis). In the example, [‘purple’,’blue’,’green’]. The datalessRegionColor is used as the color for the nations without data. Finally, we can customize the map background color (backgroundColor). A different visualization choice is the one using markers with a radius proportional to the value (displayMode: ‘markers’):

The following typescript code set the chart options:

private SetChart(data: any): MyChart  {
    const mychart: MyChart = {
     type: 'GeoChart',
     data,
     options: {
       colorAxis: {colors: ['purple', 'blue', 'green']},
       backgroundColor: '#81d4fa',
       datalessRegionColor: 'white'
     }
    };
    return mychart;
}

This is the HTML template:

<google-chart class="chart"
   [height]="400"
   [type]="myChangingCart.type"
   [data]="myChangingCart.data"
   [options]="myChangingCart.options"
   (select)="onSelect($event)">
</google-chart>

The google-chart component has an event for the selected nation (select) as a polygon. From the passed argument ($event), through its row property, we can extract the name that we’ll pass to a detailed map.

onSelect(event: ChartEvent) {
   if (event && event[0] && event[0].row >= 0) {
     const row = event[0].row;
     this.selectedNation = this.geoSummary[row][0];
   }
}

The project contains a detail component consisting of a Google map showing the arenas for the selected nation. Changing the selection in the google-chart, the map is repositioned on the corresponding nation and the markers are updated.

We pass to the detail component (app-details) the selected nation as an @Input():

<app-details [nation]='selectedNation'></app-details>

app-details uses a well-known component called Angular Google Maps (agm) available at the following address.

<agm-map #map [zoom]="zoom" [latitude]="latitude" [longitude]="longitude"
               [mapTypeId]="'hybrid'">
</agm-map>

app-details implements OnChanges with the ngOnChanges method where we extract from a service the stadiums belonging to the selected nation. Subscribing to the service method, we can manage the change of the map center, calculated using the arenas latitude and longitude.

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();
     });
   }
}

The calculation of latitude and longitude arithmetical average it’s not enough precise. This might work pretty well at low latitudes, but at higher latitudes it begins to give poor results and completely breaks down near the poles. The conventional method is to convert latitude (ϕi) and longitude (λi) in a three-dimensional point using the following formula:

Now we can calculate the average of these three-dimensional points (on the sphere surface) and convert it again in latitude and longitude using the following formula:

The code contains an helper class with all the arithmetic conversion formulas. The map center calculation starting from the stadium arrays is the following:

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;
}

With the agm-map component, we can insert markers on the map. In our example, every stadium has its marker. It’s possible to handle the click event on every single marker. To do this, an *ngFor directive it’s enough.

<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>

We use the markerClick event to center the map on the marker and increase the zoom.

clickedMarker(info: any) {
   this.zoom = 16;
   this.center.latitude = info.latitude;
   this.center.longitude = info.longitude;
}

Every marker has an info window (agm-info-windows) showing further details about the selected arena.

<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>

Inside the agm-info-window we can add a link to start further searches (like for example the nearby hotels):

The code repository is available at the following address.

See you next!

Written by

Salvatore Sorrentino