Introduction

 

On my previous blog post, I have shown you how to get started developing the Google Maps custom symbol for PI Vision 3 (2016, 2016 R2, 2017 and 2017 R2), how load the external libraries properly and how to show a simple map. On this blog post, we will add a marker to the map, center the map and marker according to the PI values received and create some options for the users to choose. Finally, we will understand how we can save some configuration options so they are going to be loaded in case the display is refreshed or reloaded.

 

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

 

 

 

Adding a marker

 

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-p2'.

 

We already have a map on our symbol. Let's add a marker with an infoWindow. Please refer to this sample for the marker and this sample for the infowindow for more information about how to use Google Maps JavaScript API.

 

Now, it is really easy to change the scope.startGoogleMaps function to complete our task.

 

        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.marker = new google.maps.Marker({
                    position: { lat: 0, lng: 0 },
                    map: scope.map,
                });

                scope.infowindow = new google.maps.InfoWindow();
            }   
        };

 

Note that the marker was added to a fixed position on the map (latitude = 0, longitude = 0). The dataUpdate function will move the center of the map and marker according to the PI data received as it is going to be shown on the following sections.

 

Options for the custom symbol

 

The google.maps.Map class allows you to set some options using the setOptions(MapOptions) method. The MapOptions object specification has many options available to select some options programmatically. Here are some of them which we will be using:

 

Option for MapOptions Description
Zoom LevelThe Map zoom level.
DisableDefaultUIEnables/disables all default UI. May be overridden individually.
ZoomControlThe enabled/disabled state of the Zoom control.
ScaleControlThe initial enabled/disabled state of the Scale control.
StreetViewControlThe initial enabled/disabled state of the Street View Pegman control. This control is part of the default UI, and should be set to false when displaying a map type on which the Street View road overlay should not appear (e.g. a non-Earth map type).
MapTypeControlThe initial enabled/disabled state of the Map type control.
MapTypeIdThe initial Map mapTypeId. Defaults to ROADMAP.

 

On top of the list above, three more options are going to added as options of the this symbol:

 

OptionDescription
MarkerColorColor of the marker
LatIndexIndex of the Data.Rows array to find the object for the latitude.
LngIndexIndex of the Data.Rows array to find the object for the longitude.

 

As you are going to note on the following sections, LatIndex and LngIndex store useful information for finding the correct coordinates (latitude and longitude) within the data object which is the input of the dataUpdate function.

 

Update the definition and the configuration HTML

 

Now that the options were already selected, the getDefautConfig() method from the definition variable needs to be updated with the default values on the options available. The default values are going to be used once the symbol is added to the display.  The configOptions property was also added to the definition variable which will allow the user to right click on the symbol, select 'Format symbol' and change the symbol options. Please refer to the new definition below:

 

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

 

It is time to create the configuration HTML. As the typeName property of the definition object is 'gmaps-p2' on this blog post, please create a new file named sym-gmaps-p2-config.html with the following content.

 

<div class="c-side-pane t-toolbar">
    <span style="color:#fff; margin-left:15px">Attributes Indexes Settings</span>
</div>


<div class="c-config-content">Latitude attribute index:
    <input type="text" ng-model="config.LatIndex">
</div>
<div class="c-config-content">Longitude attribute index:
    <input type="text" ng-model="config.LngIndex">
</div>

<div class="c-side-pane t-toolbar">
    <span style="color:#fff; margin-left:15px">Marker Color</span>
</div>
<div class="c-side-pane t-toolbar">
    <pv-color-picker id="markerColor" ng-model="config.MarkerColor"></pv-color-picker>
</div>


<div class="c-side-pane t-toolbar">
    <span style="color:#fff; margin-left:15px">Map Options</span>
</div>


<div class="c-config-content">
    Show Zoom Control:
    <input type="checkbox" ng-model="config.ZoomControl">
</div>


<div class="c-config-content">
    Select Zoom level:
    <input type="text" ng-model="config.ZoomLevel">
</div>




<div class="c-config-content">
    Disable Default UI:
    <input type="checkbox" ng-model="config.DisableDefaultUI">
</div>


<div class="c-config-content">
    Show Scale Control:
    <input type="checkbox" ng-model="config.ScaleControl">
</div>


<div class="c-config-content">
    Show Street View Control:
    <input type="checkbox" ng-model="config.StreetViewControl">
</div>


<div class="c-config-content">
    Show Map Type Control:
    <input type="checkbox" ng-model="config.MapTypeControl">
</div>


<div class="c-config-content">
    Select Map Type:
    <select name="singleSelect" ng-model="config.MapTypeId">
        <option value="HYBRID">Hibrid</option>
        <option value="TERRAIN">Terrain</option>
        <option value="ROADMAP">Roadmap</option>
        <option value="SATELLITE">Satellite</option>
    </select><br>
</div>

 

Here are some comments:

  • Under the Format Configuration page, there are three sections: Attributes Index Settings, Marker Color and Map Options. Each section has its title on a div using c-side-pane t-toolbar classes.
  • Each option has a div with c-config-content. There are three type of options:
    • Combobox which uses the select tag.
    • Check box which uses the input of type checkbox.
    • Text box which uses the input of type text.
  • All of these options above store the new value according to what it is defined on the ng-model attribute.
  • The pv-color-picker is an Angular directive already exposed by PI Vision.

 

 

Applying selected options to the map

 

Time to test! I've opened a PI Vision display, added the symbol and tried to right-click on the map. To my surprise nothing happened!

 

I have then realized that if I right click on the map, it seems the Google Maps libraries will be responsible to handle that action and not PI Vision. As a workaround, I've reduced the space of the map within the symbol, create some empty space to right click using CSS and select to format the option. Here is the new sym-gmaps-p2-template.html.

 

<div id="container" style="width:100%;height:calc(100% - 50px);">


</div>
<center>
    <br /><br />
    <p style="color: white;">Right click here to format this symbol</p>
</center>

 

 

By the time I make a change on the configuration window, the scope.updateGoogleMapsConfig is called with the new config variable as input. Then, we call map.setOptions() and scope.marker.setIcon() to update the map and the marker with the new configuration. Note that all properties are string. This is why the parseInt method needs to be called when an integer is needed.

 

 


        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),
                    zoom: parseInt(config.ZoomLevel)
                });
                if (config.MarkerColor != 'rgb(255,0,0)') {
                    scope.marker.setIcon('http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + config.MarkerColor.substr(1));
                }
            }
        };

 

It is not possible to set up a color for the marker but it is possible to define an image to be used instead. Using the Chart API url above with the markerColor as an query string input, we can point to an image and it will behave as if the default marker could have its color changed.

Changing the LatIndex and LngIndex will only alter the behavior of the dataUpdate function. The map and marker will not be modified.

 

Finally, the mapTypeId property of the MapOptions object needs to be an enumeration of google.maps.MapTypeId (and not a string). The getMapTypeId function is responsable for making the convertion.

 

        scope.getMapTypeId = function (mapTypeIdString) {
            if (mapTypeIdString == 'HYBRID') {
                return google.maps.MapTypeId.HYBRID;
            }
            else if (mapTypeIdString == 'ROADMAP') {
                return google.maps.MapTypeId.ROADMAP;
            }
            else if (mapTypeIdString == 'SATELLITE') {
                return google.maps.MapTypeId.SATELLITE;
            }
            else if (mapTypeIdString == 'TERRAIN') {
                return google.maps.MapTypeId.TERRAIN
            }
            else {
                return null;
            }
        }

 

 

Updating the data

 

When PI Vision symbol receives a new value from the PI Data Archive, it will call dataUpdate function with the data variable as input. The array data.Rows has as many objects as the number of attributes or PI Points which the symbol is monitoring. If we drag and drop an element with 6 attributes to the display, data.Rows would be an array of 6 objects. By using the correct scope.config.LatIndex and scope.config.LngIndex, the symbol ill find the current correct position for the marker. Please refer to the code snippet below:

 

        scope.dataUpdate = function (data) {
            if ((data == null) || (data.Rows.length == 0)) {
                return;
            }
            if (scope.map != undefined) {
                var infowindowContent = 'Last timestamp: ' + data.Rows[parseInt(scope.config.LatIndex)].Time;
                var currentLatLng = { lat: parseFloat(data.Rows[parseInt(scope.config.LatIndex)].Value), lng: parseFloat(data.Rows[parseInt(scope.config.LngIndex)].Value) };
                scope.marker.setPosition(currentLatLng);
                scope.map.setCenter(currentLatLng);
                scope.infowindow.close();
                var marker = scope.marker;
                google.maps.event.addListener(marker, 'mouseover', (function (marker) {
                    return function () {
                        scope.infowindow.setContent(infowindowContent);
                        scope.infowindow.open(scope.map, marker);
                    }
                })(marker));
            }
        }

Note that we need to check if scope.map was already defined, as the dataUpdate function could be called before the map was even created.

 

What does it happen when a saved display is loaded or refreshed?

 

When the display is saved, only the scope.config variable is going to be saved on the SQL Server database. The content from all other variables will be lost. Therefore, when a display is loaded, the updateGoogleMapsConfig() method needs to be called in order to apply fast the options stored on the database to the map and marker. Please refer to the code snippet below:

 

 

        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.marker = new google.maps.Marker({
                    position: { lat: 0, lng: 0 },
                    map: scope.map,
                });

                scope.infowindow = new google.maps.InfoWindow();


            }
            scope.updateGoogleMapsConfig(scope.config);
        };

 

 

Conclusions

 

We were able to add some nice features to our custom symbols. But there are some problems still to be solved:

 

  • As we are using LatIndex and LngIndex, if some attributes were added, deleted or renamed, the symbol might get a value from another attribute.
  • The symbol could search for an attribute name instead of using LatIndex and LngIndex variables as a suggestion to solve this issue.
  • It is not possible to add another marker to the map yet.

 

Those are the problems that are going to be solved on the upcoming blog post (part 3).