Marcos Vainer Loeff

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

Blog Post created by Marcos Vainer Loeff Employee on May 31, 2016

Introduction

 

PI Vision 2017 is already released and one of its great features is the ability to develop custom symbols and custom tool panes. There are a lot of interesting JavaScript libraries available to help you create your own custom PI Vision symbol. On this blog post series, I will show you how to develop the Google Maps custom symbol for PI Vision 3 (2016, 2016 R2, 2017 and 2017 R2).

 

The idea is that when the user drags an element and drops it on the PI Vision display, a Google Maps 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.

 

Below there is a screenshot taken using the version from part 3:

 

 

 

 

Setting up your environment

 

Here is a suggestion for you to set up your environment in order to develop and debug any custom symbol in an efficient manner. Let's suppose you have a client machine where you have the IDE/editor installed to develop your library and a web server machine with PI Vision installed.

 

Because I am really used to using all the great Visual Studio features, I don't feel like using any other IDE/editor such as Visual Studio Code, Notepad++ or Sublime. I have Visual Studio 2015 and Google Chrome already installed on my client machine so here are the steps I had to do to set up my development environment:

 

  • Edit the web.config file on the PI Vision installation by changing the compilation tag under system.web from debug="false" to debug="true". This will help you debug your library using Google Chrome. Please refer to the PI Vision 2017 Custom Extension Creation documentation for more information about this step.
  • On the PI Vision server, open the Windows Explorer and navigate to the folder: C:\Program Files\PIPC\PIVision\Scripts\app\editor\symbols. Right-click on the ext folder and select Properties. Under the Sharing tab, you will find options to share your folder on the network. Make sure that your user account has read and write privileges.
  • On the client machine, open Windows Explorer and navigate to the PI Vision server machine. You will find the ext folder that you have just shared. Right-click on it, select  "Map network drive..." and follow the steps. After that, you should have a new drive mapped to the PI Vision server ext folder.
  • Create two blank files on the shared ext folder: sym-gmaps-p1.js and sym-gmaps-p1-template.html. Open Visual Studio but do not create any new project. Just drag those two files and and drop into the IDE. This is all! Whenever you save a file using Visual Studio, this file will be saved on the PI Vision server. If you are browsing using Google Chrome, just refresh the page by pressing CTRL + F5 to clear the cache each time you save a new version of the file.

 

Getting started developing the PI Vision symbol

 

Besides the PI Vision extensibility document, there is a great OSIsoft GitHub repository called "PI Vision: Extensibility Samples and Tutorials" with many useful examples. Please refer to the "Simple Value Symbol" document as it provides more information in case you have never created a custom symbol before.

 

Before we start, you can download the source code package from this GitHub repository.

 

  • We have already created the two files on the ext folder. Open the sym-gmaps-p1.js file and add the following code to it.

 

(function (PV) {
    function symbolVis() { }
    PV.deriveVisualizationFromBase(symbolVis);
    var definition = {
        typeName: 'gmaps-p1',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
iconUrl: 'Images/google-maps.svg',
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400           
            };
        },
        visObjectType: symbolVis
    };
    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

  • The code above creates an IIFE (immediately invoked function expression), which is a JavaScript function that runs as soon as it is defined.  A Javascript object is created with some properties that describe the symbol, including typeName and dataSourceBehaviour. This object is the input for the CS.symbolCatalog.register() function which makes this symbol available for PI Vision users.
  • The datasourceBehaviour is set to Multiple since we want to drag and drop AF Elements and monitor two of their attributes (latitude and longitude). This is why CS.DatasourceBehaviours.Single shouldn't be used.
  • Another property is added to the definition object called getDefaultConfig() which will return the default configuration values to save in the database.
  • If you read the extensibility document, you will find that the DataShape field from the getDefaultConfig() function is used to define how data should be retrieved by PI Vision. This documents defines each option for this property. Table seems to be the most suitable for this use case since we are working with multiple data sources and not interested in historical data.
  • The Width and Height properties were added to the object returned by the getDefaultConfig() and used to determine the width and height of the symbols when they are added to the PI Vision display.
  • The final property is init where the init function should be defined. Please refer to the code below:

 

(function (PV) {


    function symbolVis() { }
    PV.deriveVisualizationFromBase(symbolVis);


    var definition = {
        typeName: 'gmaps-p1',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
iconUrl: 'Images/google-maps.svg',
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400           
            };
        },
        visObjectType: symbolVis
    };


    symbolVis.prototype.init = function init(scope, elem) {
        this.onDataUpdate = dataUpdate;
        this.onConfigChange = configChanged;
        this.onResize = resize;    


        var container = elem.find('#container')[0];
        var id = "gmaps_" + Math.random().toString(36).substr(2, 16);
        container.id = id;
        scope.id = id;


        function configChanged(config, oldConfig) {


        };




        function resize(width, height) {


        }  


       
        function dataUpdate(data) {


        }
    }


    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

  • The first 3 lines of the init function changes the id of the node of the symbol to make it unique by using random function. This is possible as one of the inputs of the init function is elem which is the node of the current symbol.
  • The content of the sym-gmapspartone-template.html file is:

 

<div id="container" style="width:100%;height:100%;">


</div>


 

  • Finally, we have defined the updateGoogleMapsConfig, resizeGoogleMaps and dataUpdate functions which are going to be explained on the following blog posts about this topic.

 

It is time for us to initialize the Google Maps JavaScript API libraries.

 

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. Our first problem is that it is not a good practice the edit the Index file of the PI Vision in order to add a script tag to load the external Google Maps library. Ideally, the symbol code should be able 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 the symbol code?
  3. Users can add as many instances of this symbol as they want. On the other hand, if the GMaps libraries were already loaded, the symbol should not try to load it again.

 

 

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

 

                var script_tag = document.createElement('script');
                script_tag.setAttribute("type", "text/javascript");
                script_tag.setAttribute("src", "http://maps.google.com/maps/api/js?sensor=false&callback=gMapsCallback");
                (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);

 

Solving problem 2: 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:

 

    window.gMapsCallback = function () {
        $(window).trigger('gMapsLoaded');
    }

 

We have also bound the gMapsLoaded event with the scope.startGoogleMaps function:

 

        $(window).bind('gMapsLoaded', scope.startGoogleMaps);

 

The scope.startGoogleMaps function will actually create the map on the display..

 

Solving problem 3: Properties of the window object can be used as global variables.

 

By assigning window.googleRequested = true, all other symbol instances will know that the web page has already requested to transfer the JavaScript libraries required. By checking if the window.google is undefined or not, the library can know if the GMaps libraries were already loaded or not.

 

We need to be prepared to handle the following scenario:

  1. User adds the first instance GMaps symbol to his display.
  2. The symbol starts loading the GMaps libraries (window.googleRequested = true) and it will create a map when window.gMapsCallback is called.
  3. User adds a second instance of this custom symbol. At this point, the GMaps libraries are still loading (window.google = undefined). The second instance shouldn't try to load the external libraries again because they are already being loaded.
  4. If the second instance tries to create a map, an exception will be thrown because the second instance needs to wait until window.google is different than undefined.

 

Finally, here is definition from scope.startGoogleMaps:

 

        scope.startGoogleMaps = function () {
            if (scope.map == undefined) {
                scope.map = new window.google.maps.Map(document.getElementById(scope.id), {
                    center: { lat: 0, lng: 0 },
                    zoom: 1
                });
            }
            scope.updateGoogleMapsConfig(scope.config);
        };

 

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

 

(function (PV) {


    function symbolVis() { }
    PV.deriveVisualizationFromBase(symbolVis);


    var definition = {
        typeName: 'gmaps-p1',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
iconUrl: 'Images/google-maps.svg',
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400           
            };
        },
        visObjectType: symbolVis
    };






    window.gMapsCallback = function () {
        $(window).trigger('gMapsLoaded');
    }


    function loadGoogleMaps() {
        if (window.google == undefined) {
            if (window.googleRequested) {
                setTimeout(function () {
                    window.gMapsCallback();
                }, 3000);


            }
            else {
                var script_tag = document.createElement('script');
                script_tag.setAttribute("type", "text/javascript");
                script_tag.setAttribute("src", "http://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback");
                (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
                window.googleRequested = true;
            }
        }
        else {
            window.gMapsCallback();
        }
    }




    symbolVis.prototype.init = function init(scope, elem) {




        this.onDataUpdate = dataUpdate;
        this.onConfigChange = configChanged;
        this.onResize = resize;
     


        var container = elem.find('#container')[0];
        var id = "gmaps_" + Math.random().toString(36).substr(2, 16);
        container.id = id;
        scope.id = id;


        function configChanged(config, oldConfig) {


        };


        scope.startGoogleMaps = function () {
            if (scope.map == undefined) {
                scope.map = new window.google.maps.Map(document.getElementById(scope.id), {
                    center: { lat: 0, lng: 0 },
                    zoom: 1
                });
            }
            configChanged(scope.config);
        };




        function resize(width, height) {


        }  


       
        function dataUpdate(data) {
            if ((data == null) || (data.Rows.length == 0)) {
                return;
            }
        }


        $(window).bind('gMapsLoaded', scope.startGoogleMaps);
        loadGoogleMaps();
    }


    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

 

Save both files, open PI Vision using Google Chrome and create a new display. Select the Google Maps symbol on the left pane and drag and drop any element to the new PI Vision display. Make sure not only that the Google Maps symbol is created and that no exception is thrown when multiple symbols are added on a single display.

 

 

 

Conclusions

 

When developing custom PI Vision symbols, it might be required to load external JavaScript and CSS libraries dynamically. On this first blog post of this series, it was shown how to get started developing the Google Maps PI Vision symbol. The next blog post I will show you how to add a marker on the map, according to the latitude and longitude of the dragged element.

 

If you have any question or suggestion, please post it on the comments below.

Outcomes