Marcos Vainer Loeff

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

Blog Post created by Marcos Vainer Loeff Employee on Jun 27, 2016

Introduction

 

When I first started to develop the Google Maps custom symbol for PI Vision 3 (2016, 2016 R2, 2017 and 2017 R2), I couldn't imagine the value that it could bring to our customers and partners. After publishing part 1 and part 2, I've received a lot of messages asking me when part 3 is going to be published. Finally, the day has come! One cool feature of part 3 is the ability to add multiple markers inside a map. I guess this is an important feature that you would like to learn and implement.

 

Here are the features we are going to enhance/implement on this blog post:

 

  • Identifying the Latitude and Longititude attributes through indexes is not a good practice. This is why we are going to do it through a string. As a result, we are going to use LngName and LatName instead of LngIndex and LatIndex.
  • Allow the user to add multiple markers (elements) into the map.
  • In case the user mouse over a marker, it should appear a infoWindow with the element name. This behavior should be enabled or disabled by the user in the configuration panel.
  • Google Maps can set the viewport to contain the given bounds to display all markers. This behaviour should also be enabled or disabled by the user in the configuration panel.

 

Before we start, remember that you can download the source code package from this GitHub repository, which has all files from this blog posts series.The typeName of the definition variable needs to be updated to 'gmaps'.

 

Adding an icon

 

A great custom symbol needs to have its own icon. As a result, we have added the file google-maps.svg on the GitHub repository. Please copy and paste this file to the PIPC\PIVision\Images folder. A new property for the definition object needs to be added as well:

 

iconUrl: 'Images/google-maps.svg',

 

By looking at the icons below, can you guess which one is the Google Maps custom symbol. My next blog post will be about the Google Street View PI Vision symbol whose icon I guess you also already know how it looks like .

 

 

google-maps.jpg

 

Special thanks to Pedro Rosa for creating those SVG files!

 

Changing the symbol configuration parameters

 

As a result of our enhancements, it is necessary to change how our scope.config will look like. Please refer to the new getDefaultConfig() method whose properties were added/changed:

 

    var definition = {
        typeName: 'gmaps-p3',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
        iconUrl: 'Images/google-maps.svg',
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)',
                LatName: 'Latitude',
                LngName: 'Longitude',
                HistoricalMode: false,
                OpenInfoBox: true,
                ZoomLevel: 8,
                DisableDefaultUI: false,
                ZoomControl: true,
                ScaleControl: true,
                StreetViewControl: true,
                MapTypeControl: true,
                MapTypeId: 'ROADMAP',
                FitBounds: true,
                ElementsList: {}
            };
        },
        visObjectType: symbolVis,
        configOptions: function () {
            return [{
                title: 'Format Symbol',
                mode: 'format'
            }];
        }
    };

 

 

 

Here are the changes compared to part 2:

 

  • LatIndex and LngIndex were replaced by LatName and LngName
  • Added the  boolean properties OpenInfoBox and FitBounds
  • The ElementsList property was added to store information about the elements and markers.

 

 

Updating the Init function

 

As multiple markers can be added to the map, the scope.marker and scope.infoWindow needs to be updated to:

 

        scope.markersList = [];
        scope.infoWindowList = [];

 

 

Concerning the scope.startGoogleMaps which is the function where the map is created, we have removed the lines of code that creates the marker. It makes more sense to create them when the dataUpdate method is called, as one of its inputs provides information of the position for each marker on the map. Please refer below to the new version of this function:

 

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

 

Creating a custom symbol with multiple elements

 

 

I think this is a good opportunity to explain how exactly PI Vision works with multiple elements on the same symbol.

 

On one of my AF databases, there are two root elements: marc.adm and OtherUser. Each one of them has an attribute for Latitude and another attribute for Longitude. Please refer to the screenshot below for more information about our AF Tree of elements.

 

 

On PI Vision web site, we have done the following steps:

  • Created a new PI Vision display
  • Found the marc.adm element
  • Dragged and dropped this element to the display after selecting the Google Maps custom symbol.
  • Opened Google Chrome Developer Tools and set a breakpoint on the first line within the dataUpdate(data) function.

 

When the breakpoint is hit on the first time, for each object on the data.Rows array there are 4 properties (including Label and Path) as shown on the Figure below.

 

 

When the following calls hit the same breakpoint, the same objects would have most of the times only two properties (Time and Value) as shown on the Figure below. Note that the element has 6 attributes, which explains why  data.Rows is an array with 6 objects.

 

This behavior is well documented on the PI Vision extensibility document: "Some properties of a data item change infrequently, such as the data item name or its unit of measure. To reduce the response size and improve performance, these metadata fields are returned on the first request and only periodically afterward."

 

Please refer to the figure below. After dragging and dropping the OtherUser element into the same symbol, the first dataUpdate() call will Label and Path properties appear again. The size of the Rows array is now 12. Now we are monitoring 12 attributes from 2 elements (each element has 6 attributes).

 

 

Updating the dataUpdate function

 

As you have noticed reviewing the content of the data input from the dataUpdate() function is the only way to know that a new elemen/marker was added to the symbol. When the objects from data.Rows has the Label and Path fields, it is important to save those settings in order to be able to process the upcoming data updates that don't come with those properties. Finally, when the display is loaded (from the SQL database), it can take some time for PI Vision to send an update with the Label and Path fields on the objects within data.Rows. This is why the symbol saves some important information on the scope.config.elementList in order to be able to find the information it needs to process the data object properly. Let's try to summarize how the dataUpdate should work:

 

  1. If the data object is null or if it doesn't come with PI values to be updated, the function stops its execution.
  2. If the map was not created yet, the execution is also stopped.
  3. If this is the first dataUpdate() call and if there are already objects in the scope.config.ElementsList array, this means that the display needs to create the markers using the settings stored on the SQL database (scope.config).
  4. If the objects from the data.Rows comes with the Label and Path properties, the symbol will create new markers in case they were not yet created.
  5. The location of the markers are also updated with the scope.updateMarkersLocation() function.

 

 

 

        scope.forceFirstUpdate = true;


        scope.dataUpdate = function (data) {
            if ((data == null) || (data.Rows.length == 0)) {
                return;
            }
            if (scope.map != undefined) {
                if ((scope.forceFirstUpdate == true) && (Object.keys(scope.config.ElementsList).length > 0)) {
                    scope.forceFirstUpdate = false;
                    scope.createMarkers(data, true);
                }
                if (data.Rows[0].Path) {
                    scope.createMarkers(data, false);
                }
                scope.updateMarkersLocation(data);


            }
        }

 

Let's try to understand the logic of the scope.createMarkers. This method can work on the following scenarios:

  1. When the markers are loaded from a saved display.
  2. When the markers are added by reviewing the content of the data input of the dataUpdate function and checking if a new marker needs to be added.

 

The first part of the function updates the scope.config.ElementList dictionary, where each object stores information about an element/marker and has some properties:

 

  • LatIndex --> Index of the data.Rows to find the latitude attribute of the mapped element.
  • LngIndex- -> Index of the data.Rows to find the longitude attribute of the mapped element.
  • MarkerIndex --> Index to find the marker within the scope.markersList array. The marker will be added to the array as soon as it is created.
  • MarkerCreated --> Shows if the marker was created or not.

 

As you have realized, having scope.config.ElementList available is enough to process the data input on the dataUpdate function, even not having the Label and Path properties available. This is especially useful when the symbol is loaded from a saved display. In this scenario, it can take some time to receive objects in the data.Rows array with Label and Path fields. Without the scope.config.ElementList object and the Label and Path fields, the symbol cannot map the data.Rows objects with the element attributes for finding the current geolocation.

 

The key of the scope.config.ElementList is the element name. Using the element path would be a better option but PI Vision seems to have some issues with some of its characters.

 

The content of the scope.createMarkers function is:

 

 

 

     scope.createMarkers = function (data, useConfigData) {
            if (useConfigData == false) {
                for (var i = 0; i < data.Rows.length; i++) {
                    var splitResult = data.Rows[i].Label.split('|');
                    if ((splitResult[1] == scope.config.LatName) || (splitResult[1] == scope.config.LngName)) {
                        if (scope.config.ElementsList[splitResult[0]] == undefined) {
                            scope.config.ElementsList[splitResult[0]] = new Object();
                            scope.config.ElementsList[splitResult[0]].MarkerCreated = false;
                            scope.config.ElementsList[splitResult[0]].MarkerIndex = -1;
                            scope.config.ElementsList[splitResult[0]].LatIndex = null;
                            scope.config.ElementsList[splitResult[0]].LngIndex = null;
                        }
                        if (splitResult[1] == scope.config.LatName) {
                            scope.config.ElementsList[splitResult[0]].LatIndex = i;
                        }


                        if (splitResult[1] == scope.config.LngName) {
                            scope.config.ElementsList[splitResult[0]].LngIndex = i;
                        }
                    }
                }
            }


            for (var key in scope.config.ElementsList) {
                var currentElement = scope.config.ElementsList[key];
                if ((currentElement.MarkerCreated == false) || (scope.markersList[currentElement.MarkerIndex] == undefined)) {
                    if ((currentElement.LatIndex != null) && (currentElement.LngIndex != null)) {
                        var marker = new google.maps.Marker({
                            position: { lat: parseFloat(data.Rows[currentElement.LatIndex].Value), lng: parseFloat(data.Rows[currentElement.LngIndex].Value) },
                            map: scope.map,
                            title: key
                        });
                        var infowindow = new google.maps.InfoWindow();
                        scope.markersList.push(marker);
                        scope.infoWindowList.push(infowindow);
                        scope.updateMarkersSettings(marker, infowindow, key, scope.config);
                        currentElement.MarkerCreated = true;
                        currentElement.MarkerIndex = scope.markersList.length - 1;
                    }
                    else {
                        alert('Could not find the latitude and longitude attributes');
                    }
                }
            }
        }

 

The updateMarkersLocation is responsible for:

  1. Moving the markers to their new position according to the data input and scope.config.ElementsList dictionary using the marker.setPosition() method.
  2. Select a suitable latitude, longitude and zoom for the map. For multiple markers, the symbol can fit all markers added to the map if this option is enabled.

 

        scope.updateMarkersLocation = function (data) {
            var markersCount = 0;
            var currentLatLng = null;
            for (var key in scope.config.ElementsList) {
                var currentElement = scope.config.ElementsList[key];
                if (currentElement.MarkerCreated == true) {
                    currentLatLng = { lat: parseFloat(data.Rows[currentElement.LatIndex].Value), lng: parseFloat(data.Rows[currentElement.LngIndex].Value) };
                    var marker = scope.markersList[parseInt(currentElement.MarkerIndex)];
                    marker.setPosition(currentLatLng);
                    markersCount++;
                }
            }
            if (markersCount == 1) {
                scope.map.setCenter(currentLatLng);
            }
            if (markersCount > 1) {
                var latlngbounds = new google.maps.LatLngBounds();
                for (var i = 0; i < scope.markersList.length; i++) {
                    latlngbounds.extend(scope.markersList[i].getPosition());
                }
                if (scope.config.FitBounds == true) {
                    scope.map.fitBounds(latlngbounds);
                }
            }
        }

 

 

 

Updating the updateGoogleMapsConfig function

 

As the properties of the scope.config were changed, the scope.updateGoogleMapsConfig() function also needs to be changed. As the symbol might need to manage multiple markers, for each marker the scope.updateMarkersSettings() function is called. Please refer to both functions below:

 

        scope.updateGoogleMapsConfig = function (config) {
            if (scope.map != undefined) {
                scope.map.setOptions({
                    disableDefaultUI: config.DisableDefaultUI,
                    zoomControl: config.ZoomControl,
                    scaleControl: config.ScaleControl,
                    streetViewControl: config.StreetViewControl,
                    mapTypeControl: config.MapTypeControl,
                    mapTypeId: scope.getMapTypeId(config.MapTypeId)
                });
                if ((config.FitBounds == false) || (scope.markersList.length == 1)) {
                    scope.map.setOptions({
                        zoom: parseInt(config.ZoomLevel)
                    });
                }
            }


            for (var i = 0; i < scope.markersList.length; i++) {
                var marker = scope.markersList[i];
                scope.updateMarkersSettings(marker, scope.infoWindowList[i], scope.getInfowindowContent(i), config);
            }
        };

 

 

        scope.updateMarkersSettings = function (marker, infowindow, infowindowContent, config) {


            if (config.MarkerColor != 'rgb(255,0,0)') {
                marker.setIcon('http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + config.MarkerColor.substr(1));
            }
            infowindow.close();
            google.maps.event.addListener(marker, 'mouseover', (function (marker) {
                return function () {
                    if ((scope.config.OpenInfoBox == true) && (infowindowContent != null)) {
                        infowindow.setContent(infowindowContent);
                        infowindow.open(scope.map, marker);
                    }
                }
            })(marker));
        }

 

 

Conclusions

 

I hope you have learned a lot about custom PI Vision symbols development by reading these three blog posts. In case you have found any bugs/known issues please let me know. The next blog post will be about Google Street View PI Vision custom symbol. Stay tuned!

Outcomes