Introduction

 

On the first 3 blog posts (part 1, part 2 and part 3) about developing the Google Maps PI Coresight custom symbol, I have shown you how to create a custom symbol showing a Google Map with some markers that represent the updated location from many assets.

 

On this blog post, I will show how to show display on the map historical data by using event frames.

 

You can download the custom symbol in this GitHub repository.

 

Understanding the problem

 

In the past, I have recorded several times the geolocation coordinates when I've walked from my old home to our old OSIsoft office here in São Paulo using an Android app called RunKeeper. After I've stopped recording the activity, the app allowed me to download all routes of activities recorded by downloading a GPX file. By creating a simple console application, I was able to send all the latitude and longitude values to a PI System.

 

On my PI AF Server, I have created a new database called GMapsPart4 with only 1 element called Marcos. This element is derived from an element template called UserTemplate with 2 attribute template called Latitude and Longitude.

 

 

I've also created an event frame for each activity downloaded in the GPX format to make it easier access this kind of information programmatically. There are 3 event frames on this database. The primary referenced element in all of them is the Marcos element. Each attribute from the element map the attribute from the Marcos element as shown below. An event frame template was created since all EFs follow the same pattern.

 

 

Below there is a screenshot taken using the final version of the custom symbol developed on this blog post (part 4):

 

 

 

As you can see, the Event Frames mapped with the Marcos elements are below. Once the user clicks on any event frame, the symbol retrieves the historical geolocation data within the time range defined by the event frame. Using Google Maps API, we have added a path on the map and a marker. The marker moves when the user changes the slider's value, which is above the map.

 

This will only happen if the user selects the Historical Mode on the configuration options as shown below:

 

 

Migrating from PI Coresight 2016 and PI Coresight 2016 R2 to PI Vision 2017

 

If you compare the source code between the version described in Part 3 with this new version, you will see some architectural changes due to the fact that part 3 was developed for PI Vision 2016 and part 4 was developed for PI Vision 2016 R2. Please refer to the PI-Coresight-2016-R2-(CTP)-Extensibility-Documentation  for more information about how to upgrade existing symbols from PI CS 2016 to 2016 R2.

 

One important change I want to point it out is the AngularJS directive for color picker. In 2016, this is how you would define on your HTML:

 

<format-color-picker id="markerColor" property="MarkerColor" config="config"></format-color-picker>

 

In 2016 R2, you would use:

 

<cs-color-picker id="markerColor" ng-model="config.MarkerColor"></cs-color-picker>

 

 

 

Adding the PI Web API Client library for AngularJS to the PI Vision infrastructure

 

PI Vision does not allow you to retrieve event frames or recorded values through its native extensibility model. Although for the majority of the symbols, this is not a problem, there are some use cases which require the custom symbol to interact with PI Web API in order to retrieve additional information. Please refer to my previous blog post in order to make the piwebapi Angular service available on your PI Vision infrastructure, otherwise, this symbol won't work on your PI Vision 2016 R2.

 

 

RangeSlider

 

The slider above the map is an objected created with the RangeSlider.js JavaScript library. Please download the library from their official site and paste it on the \symbols\ext\libraries folder. I am using version 2.3.

 

 

Updating the sym-gmaps-template.html

 

Following the examples provided on the official site from RangeSlider.js library, I've added the horizontal slider on the top of the symbol. Below the map, I have added a div node in order to list the event frames retrieved.

 

 

<div ng-if="config.HistoricalMode" style="width:100%;height:50px;">
    <input type="range"
           min="0"
           max="{{rangeMax}}"
           step="1"
           value="0"
           data-orientation="horizontal">
</div>
<div id="container" style="width:100%;height:calc(100% - 150px);">


</div>


<div ng-if="config.HistoricalMode" class="activities-pane">
    <div ng-repeat="activity in activitiesList" ng-class="activity.WebId == selectedActivity.WebId ? 'activity-non-selected' : 'activity-selected'" ng-click="updateActivity(activity)">
        <p>
            {{activity.Name}}
        </p>
    </div>
</div>


<style>
    .activities-pane {
        background-color: white;
        padding: 1px;
        height: 100px;
        overflow-y: auto;
    }
        .activities-pane div {
            height: 31px;
            border: azure;
            margin: 4px;
            padding-top: 8px;
        }
        .activities-pane p {
            color: white;
        }
    .activity-selected {
        background: brown;
    }
    .activity-non-selected {
        background: blue;
    }
</style>

 

Finally, to make things easier, I've added a new style within the HTML itself although the best practice would be to have a new CSS file just for this purpose.

 

Updating the sym-gmaps.js

 

I won't describe all the steps required to update the sym-gmaps.js from part 3 to part 4. I will focus on the main logic for you understand what is going on under the hood.

 

The first thing is to inject the piwebapi service and add the HistoricalMode property of the main definition:

 

       inject: ['piwebapi'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)',
                LatName: 'Latitude',
                LngName: 'Longitude',
                HistoricalMode: false,
                OpenInfoBox: true,

 

 

If the HistoricalMode is false, the symbol would work very similar to the symbol developed on part 3. The new features can be viewed if HistoricalMode is true.

 

On the init function, call the piwebapi functions to initialize the service as described on the other blog post:

 

 

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


        piwebapi.SetServiceBaseUrl("https://marc-web-sql.marc.net/piwebapi");
        piwebapi.SetKerberosAuth();
        piwebapi.CreateObjects();

 

We need to use the extensibility model to extract the AF database path and the root element name.

 

 

            if ((scope.elementName == undefined) && (scope.lastDataWithPath.Rows[0].Path.substring(0, 3) == "af:")) {
                var elementPath = (data.Rows[0].Path.split("|")[0]).substring(3);
                var stringData = elementPath.substring(2).split("\\");
                scope.databasePath = "\\\\" + stringData[0] + "\\" + stringData[1];
                scope.elementName = stringData[2];
            }

 

 

The AF database path is used to get the AF database WebId, which wil the element name, both are used as inputs for the GetEventFrames method from the AssetDatabase controller.

 

 if (scope.activitiesList == undefined) {
                        piwebapi.assetDatabase.assetDatabaseGetByPath(scope.databasePath, null, null).then(function (response) {
                            var webId = response.data.WebId;
                            piwebapi.assetDatabase.assetDatabaseGetEventFrames(webId, null, null, "*", null, 100, null, scope.elementName, "UserTemplate", true, null, null, null, null, null, null, "*-900d").then(function (response) {
                                scope.activitiesList = response.data.Items;
                                scope.selectedActivity = response.data.Items[0];
                                scope.updateActivity(scope.selectedActivity);
                            });
                        });
                    }

 

The scope.updateActivity method updates the UI for a given activity (which is an Event Frame) following the steps below:

 

  • Clean markers and paths from the map.
  • Get the attributes WebId from the user element.
  • Get interpolated values in bulk by using the GetInterpolatedAdHoc method from the StreamSet controller.
  • Polish the values to be consumed by the Google Maps API.
  • Create the path with the retrieved PI Values and add it to the Google Map.
  • Update the color of the marker if needed.
  • Instantiate the rangeSlider object using the JavaScript library methods.

 

   scope.updateActivity = function (activity) {
            if ((scope.marker != null) && (scope.marker != undefined)) {
                scope.marker.setMap(null);


            }
            if ((scope.routePath != null) && (scope.routePath != undefined)) {
                scope.routePath.setMap(null);
            }


            scope.selectedActivity = activity;
            scope.loadingGeolocation = true;
            var elementWebId = scope.selectedActivity.RefElementWebIds[0];
            piwebapi.element.elementGetAttributes(elementWebId).then(function (response) {
                scope.attributes = response.data.Items;
                var webIds = new Array(scope.attributes.length);
                for (var i = 0; i < scope.attributes.length; i++) {
                    webIds[i] = scope.attributes[i].WebId;
                }


                piwebapi.streamSet.streamSetGetInterpolatedAdHoc(webIds, activity.EndTime, null, true, "30s", null, activity.StartTime).then(function (response) {
                    for (var i = 0; i < response.data.Items.length; i++) {
                        var currentItem = response.data.Items[i];
                        if (currentItem.Name == scope.config.LatName) {
                            scope.latitudeTrack = currentItem.Items;
                        }
                        if (currentItem.Name == scope.config.LngName) {
                            scope.longitudeTrack = currentItem.Items;
                        }
                    }


                    var bounds = new google.maps.LatLngBounds();
                    var routeCoordinates = [];
                    for (var i = 0; i < scope.latitudeTrack.length; i++) {
                        if ((scope.latitudeTrack[i].Good == true) && (scope.longitudeTrack[i].Good == true)) {
                            var pos = { lat: scope.latitudeTrack[i].Value, lng: scope.longitudeTrack[i].Value };
                            routeCoordinates.push(pos);
                            var point = new google.maps.LatLng(pos.lat, pos.lng);
                            bounds.extend(point);
                        }
                    }
                    scope.rangeMax = routeCoordinates.length;


                    scope.routePath = new google.maps.Polyline({
                        path: routeCoordinates,
                        geodesic: true,
                        strokeColor: '#FF0000',
                        strokeOpacity: 1.0,
                        strokeWeight: 2
                    });


                    scope.routePath.setMap(scope.map);
                    scope.map.fitBounds(bounds);


                    scope.marker = new google.maps.Marker({
                        position: routeCoordinates[0],
                        map: scope.map
                    });


                    updateMarkerColor(scope.marker, scope.config);


                    $('input[type="range"]').rangeslider({
                        polyfill: false,
                        onSlide: function (position, value) {
                            x = Math.round(position);
                            var geoPosition = routeCoordinates[x];
                            scope.marker.setPosition(geoPosition);
                        }
                    });
                });
            });
        };

 

 

 

Conclusions

 

The first PI Coresight which has the extensiblity model is the 2016 version. It allows you to develop custom symbols for PI Coresight. The PI Coresight extensibility model keeps sending live data to be consumed by the custom symbol. Nevertheless, there are lot of use cases and interest by the PI DevClub community to integrate the custom symbol with PI Web API. This blog post provides information about how to achieve this goal in order to create richer and more valueable custom symbols for your enterprise and/or your customers.