Marcos Vainer Loeff

Developing the Google Maps custom symbol for PI Vision 4 - Part 1

Blog Post created by Marcos Vainer Loeff Employee on Mar 26, 2018

Introduction

 

Although PI Vision 4 (2018) is not released yet, the participants of the Virtualization Virtual Hackathon 2018 need to learn the extensibility model of this product in order to raise their chances of creating a valuable custom symbol. As a result, I've decided to write the first blog post of the PI Vision 4 version of my "Developing the Google Maps custom symbol for PI Vision 3" blog post series.

 

The ultimate idea is that when the user drags an element and drops it on the PI Vision display, a Google Map will be created with a marker located according to the values of the latitude and longitude attributes of the dropped element. If another element is dropped on the map, another marker should be created accordingly.

 

This blog post (part 1) will focus on creating the map only, which is not something trivial.

 

 

Disclaimer

 

Again, PI Vision 4 is not released yet. The hackathon participants are working with a preview version. Therefore, this library might not be compatible with the released version of PI Vision 4. I will update this article and library as soon as PI Vision 4 is released though.

PI Vision 4 public preview is planned to start at PI World San Francisco 2018. You will have the opportunity to test yourself this new extensibility model! The PI Vision 4 release is planned for Q4 2018.

 

Setting up your environment

 

I will comment this topic in details after PI Vision 4 is released. For now, the hackathon participants will access their Virtual Machine with the environment already set up. In order to develop your custom symbol, the following products are used:

  • Visual Studio Code
  • Google Chrome
  • Node.js/npm
  • Git

 

If you take a look at the Virtual Machine, you will realize that the PI Vision Extension Library Seed Project was already cloned to the C:\src\pi-vision-extensions folder.

 

Getting started developing the PI Vision symbol

 

Open the command prompt and navigate to the C:\src\pi-vision-extensions folder. Then type "code .". Visual Studio Code with the PI Vision Extensions project will be opened.

 

First rename the \src\example folder and its files. They should start with gmaps instead of example as shown on the screenshot below.

 

 

 

Don't worry, the gmaps-loader.service.ts will be created later. The code to get started for the gmaps.component.ts is below:

 

import { Component, OnChanges, OnInit } from '@angular/core';


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})
export class GoogleMapsComponent implements OnInit, OnChanges {
  constructor()
  {
  }


  ngOnInit() {
  } 
  
  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }
  
}

 

The custom symbol is actually an Angular component whose decorator (@Component) is its metadata. It describes the selector, templateUrl and styleUrls. You can find more information about Angular in its official web site.

 

The content of HTML template for this component (gmaps.component.html) file is:

 

<div #gmap style="width:100%;height:100%"></div>

 

In order to create a map you just need a div HTML node. The rest is handled by JavaScript.

 

The next step is to write the Google Maps JavaScript code. Since we are using TypeScript, the definition of the Google Maps classes of needs to be downloaded and installed through the command below:

 

npm install --save @types/googlemaps

 

 

 

Getting started with Google Maps JavaScript API

 

Google provides a programming reference and samples for Google Maps JavaScript API, which were really useful to write this blog post. Let's take a look at the most basic example. Their HTML page has the following source code:

 

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #map {
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>

var map;
function initMap() {
  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: -34.397, lng: 150.644},
    zoom: 8
  });
}

    </script>
    <script src="https://maps.googleapis.com/maps/api/js?&callback=initMap" async defer></script>
  </body>
</html>

 

 

Ok, we have some problems to solve:

 

  1. Editing the index file from the PI Vision 4 web site in order to load the Google Maps JavaScript API is not a recommended practice. The symbol will have to do this task.
  2. The second problem is that url which refers to the GMaps (Google Maps) library has the name of the callback function to be called after this library is loaded. How can we make this work within Angular 5?
  3. Users can add as many instances of this symbol as they want. On the other hand, the GMaps libraries needs to be loaded only once. How to make sure there won't be any conflict?

 

 

Solving problem 1: After some research, I found this interesting StackOverflow page, which allows us to dynamically load external JavaScript scripts using Typescript. After making some changes, here is the code that makes the trick:

 

const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
let node = document.createElement('script');
node.src = url;
node.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(node);

 

 

Solving problem 2 and 3: If we define a function as property of the window JavaScript object, GMaps will be able to call it. Therefore, we have defined the window['gMapsCallback'] function as:

 

GoogleMapsLoader.promise = new Promise( resolve => {
    
    // Set callback for when google maps is loaded.
    window['gMapsCallback'] = (ev) => {
         resolve();
    };


    let node = document.createElement('script');
    node.src = url;
    node.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(node);
});

 

We have created a JavaScript Promise that when it is solved it will define the window['gMapsCallback'] and then call the Google Maps JavaScript API which will call the window['gMapsCallback'] method. The beauty of this approach is that the promise is resolved only once which means that the Google Maps will be loaded also once.

 

An Angular service named GoogleMapsLoader is created to load the Google Maps JavaScript library with the following code snippet:

 

 

import { Injectable } from '@angular/core';


const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
@Injectable()
export class GoogleMapsLoader {
  private static promise;
  public static load() {
      // First time 'load' is called?
      if (!GoogleMapsLoader.promise) {
          // Make promise to load
          GoogleMapsLoader.promise = new Promise( resolve => {
              // Set callback for when google maps is loaded.
              window['gMapsCallback'] = (ev) => {
                  resolve();
              };


              let node = document.createElement('script');
              node.src = url;
              node.type = 'text/javascript';
              document.getElementsByTagName('head')[0].appendChild(node);
          });
      }
      // Always return promise. When 'load' is called many times, the promise is already resolved.
      return GoogleMapsLoader.promise;
  }
}

 

On the custom symbol (Angular component), this is how you would load the library by calling the GoolgeMapsLoader service:

 

      GoogleMapsLoader.load().then(res => {


      });

 

 

With all these concepts and restrictions in mind, here is the final version of this blog post (part 1).

 

import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { GoogleMapsLoader } from './gmaps-loader.service'


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})


export class GoogleMapsComponent implements OnInit, OnChanges {
  @ViewChild('gmap') gmapElement: any;
  private map : google.maps.Map


  constructor(private mapLoader : GoogleMapsLoader)
  {


  }


  ngOnInit() {


    GoogleMapsLoader.load().then(res => {
        console.log('GoogleMapsLoader.load.then', res);
        this.initMap();
    });
  } 


  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }


  private initMap() {
    var mapProp = {
      center: new google.maps.LatLng(18.5793, 73.8143),
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };


    this.map = new google.maps.Map(this.gmapElement.nativeElement, mapProp);
  }
}

 

 

When the custom symbol is created, the ngOnInit() method is called. This function calls initMap() which creates a map with a specific center and zoom. The ngOnChanges() method is called whenever the PI System receives a new value for the associated attribute or element.

The focus of this blog post is just to create a map for each custom symbol added to the display. This method will be used on the following parts of this blog post series.

 

The last step is to update the module.ts on the root folder with the following information:

Rename the ExampleComponent to GoogleMapsComponent which is present on the declaration, exports and entryComponents fields  of the NgModule declarator. Add the GoogleMapsLoader to the providers field which should contain all the services. In order to be successful you need to import those modules properly.

 

Finally, we need to rename the symbol properties on the ExtensionLibrary class. We have copied the google-maps.svg from this GitHub repository and pasted into the \src\assets\images folder. We have deleted all the items from the configProps array of the generalConfig object as at this point we are not interested in setting up the configuration pane of the symbol.

 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgLibrary, SymbolType, SymbolInputType, ConfigPropType } from './framework';
import { LibModuleNgFactory } from './module.ngfactory';
import {GoogleMapsLoader} from './gmaps/gmaps-loader.service'
import { GoogleMapsComponent} from './gmaps/gmaps.component';


@NgModule({
  declarations: [ GoogleMapsComponent ],
  imports: [ CommonModule ] ,
  providers:  [GoogleMapsLoader],
  exports: [ GoogleMapsComponent],
  entryComponents: [ GoogleMapsComponent ]
})
export class LibModule { }


export class ExtensionLibrary extends NgLibrary {
  module = LibModule;
  moduleFactory = LibModuleNgFactory;
  symbols: SymbolType[] = [
    {
      name: 'gmaps-symbol',
      displayName: 'Google Maps Symbol',
      dataParams: { shape: 'single' },
      thumbnail: '^/assets/images/google-maps.svg',
      compCtor: GoogleMapsComponent,
      inputs: [
        SymbolInputType.Data,
        SymbolInputType.PathPrefix
      ],
      generalConfig: [
        {
          name: 'Google Maps Options',
          isExpanded: true,
          configProps: [ ]
        }
      ],
      layoutWidth: 200,
      layoutHeight: 200
    }
  ];
}

 

Save all files, run your local web pack server using "npm start run" and open PI Vision 4 using Google Chrome. In order to load the custom symbols, you need to go to the PI Vision landing page, select Options on the left pane and turn on the developer mode, according to the screenshot below:

 

 

 

Create a new display. Click on the Google Maps symbol on the left pane. Each click will create a new instance of the Google Maps custom symbol and add it to the PI Vision display. Make sure not only that the Google Maps symbol is added to the top-left pane but also that no exception is thrown when multiple symbols are added on a single display (please check the Google Chrome developer tools).

 

 

Conclusions

If you are a hackathon participant reading this blog post, I hope this material will help you create valueable custom symbols. If this is not the case, I hope you will have a good idea about the new extensibility model of PI Vision 4 and Angular 5.

Outcomes