Skip navigation
All Places > PI Developers Club > Blog > 2016 > June
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!

Motivation

 

Do you want to potentially increase the speed of your AF SDK asset searches by more than one order of magnitude? If so, please read on .

 

PI AF SDK 2016 introduced new methods to perform searches against assets, such as AF Elements and Event Frames. The goal of this blog post is to demonstrate the superior performance and usage of the new search and convince you that you should be using it

 

We will present a series of generic use cases and compare "new" and "traditional" methods for searching.

 

  1. Find and process a list of elements belonging to a template
  2. Find and process a list of elements with a certain attribute value
  3. Find and process a list of child elements
  4. How the new search obtains a consistent snapshot of a collection

 

We use the term "process" generally here to mean tasks such as modifying the properties of the element, querying its attributes' values, or signing up its attributes to a data pipe.

 

We use an example AF Database that has one root element called LeafElements and 10,000 child elements. Half of the child elements belong to the "Leaf_Rand" element template and the other half to the "Leaf_Sin" element template. Both element templates have attribute templates with a mix of static and PI Point data references. We adopted the AF database structure from our Large Asset Count Project. Please note we try here to not make the details of the AF database important, but merely provide a generic database from which to demonstrate examples.

 

Note: This post is part of the blog series on new AF SDK 2016 features.

 

 

Examples

Our examples are run on an Azure VM with the following specs:

  •      7 GB RAM
  •      AMD 2.10 GHz
  •      SQL Server 2014 Express
  •      AF Server and AF Client 2.8.0.7444

PI Data Archive, AF Server, SQL Server, and AF client application are all on the same machine. We don't guarantee the performance numbers below for your environment and network. These results are just a small data point in a large design space, but we hope it can provide you a reference and encourage you to perform your own tests.

 

 

1. Find and process a list of elements belonging to a template

 

Traditional search

const int pageSize = 1000;
int startIndex = 0;
int totalCount;
AFNamedCollectionList<AFElement> myElements;

do
{
     myElements = AFElement.FindElementsByTemplate(database,
                    null,
                    database.ElementTemplates["Leaf_Sin"],
                    false,
                    AFSortField.Name,
                    AFSortOrder.Ascending,
                    startIndex,
                    pageSize,
                    out totalCount);
     ProcessElements(myElements);

     startIndex += pageSize;
} while (startIndex < totalCount);

 

New search

AFSearchToken templateToken = new AFSearchToken(AFSearchFilter.Template, AFSearchOperator.Equal, database.ElementTemplates["Leaf_Sin"].GetPath());
AFElementSearch elementSearch = new AFElementSearch(database, "FindSinusoidElements", new[] { templateToken });
elementSearch.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

IEnumerable<AFElement> elements = elementSearch.FindElements(0, false, 1000);
foreach (AFElement element in elements)
{
     ProcessElement(element);
}

 

Code comparison

You can already notice some differences between the traditional and new patterns of searching.

  • The newer pattern for searching presents a more uniform abstraction. The class AFElementSearch represents the search. The search query is described by a list of AFSearchToken objects. Each token corresponds to an AFSearchFilter denoting what condition or property we want to filter on. The search is executed via a FindElements call and the caller receives an IEnumerable<AFElement> query result that can be looped through. LINQ aficionados should be excited about this
  • The former pattern relies on various method overloads of FindElements to specify the search criteria. These methods also have long argument lists that can be cumbersome to build. These methods also require the developer to write "wrapper" code such as a do-while loop to loop through the found elements and also keep track of the loop state. The do-while pattern tends to force the developer to think in terms of pages or batches, when in many cases, a foreach loop on individual elements can be more natural. In many cases, code using the new search will be more declarative and easier to maintain.

 

Performance comparison

I know what you are thinking. What about performance?

This search returns 50,000 elements and we restarted the SQL Server after each run. Our ProcessElement(s) methods simply write the AF Element name to the Console. For more expensive tasks, the timings below would be expected to be higher.

 

Traditional search: 3.2 minutes

New search without caching: 1.1 minutes

New search with caching: 0.15 minutes

 

Why is the new search faster?

  • The new search query does not require a sort (to be executed by SQL Server). Note that FindElements requires us to specify an AFSortField, but we do not need to specify one when creating an AFElementSearch. In the new search, we can opt in to sorting by using a token with AFSearchFilter.SortField.
  • The new search allows you to opt in to caching object identifiers of found elements on the server. This is done via setting the AFElementSearch.CacheTimeout property. This effectively takes a "snapshot" of the found collection, caches it, and provides a server-side iterable that the AF client can consume. This is enabled via Line 3 of the "New search" code above. Caching of identifiers allows SQL Server to retrieve subsequent items faster by avoiding repetitive queries. The traditional search which does not implement caching will incur more overhead on the SQL Server side.
  • Should you opt in to caching when using the new search? Let's see what the docs for CacheTimeout say: If you will only be getting items from the first page, then it is best to leave the cache disabled. If you will be paging through several pages of items, then it is best to enable the cache by setting this property to a value larger than you expect to be using the search.
  • Exercise for the interested reader: You can use SQL Server Profiler to look at the differences in implementation between the traditional search and new search (with and without caching).

 

Let's look at another example (if you are not yet convinced )

 

 

2. Find and process a list of elements with a certain attribute value

 

Traditional search

const int pageSize = 1000;
int startIndex = 0;
int totalCount;
AFNamedCollectionList<AFElement> myElements;

AFElementTemplate template = database.ElementTemplates["Leaf"];
AFAttributeValueQuery query = new AFAttributeValueQuery(template.AttributeTemplates["SubTree"], AFSearchOperator.Equal, "1");
do
{
     myElements = AFElement.FindElementsByAttribute(
                    null,
                    "*",
                    new AFAttributeValueQuery[] { query },
                    true,
                    AFSortField.Name,
                    AFSortOrder.Ascending,
                    startIndex,
                    pageSize,
                    out totalCount);
     ProcessElements(myElements);

     startIndex += pageSize;
} while (startIndex < totalCount);

 

New search

AFElementTemplate template = database.ElementTemplates["Leaf"];
AFSearchToken templateToken = new AFSearchToken(AFSearchFilter.Template, AFSearchOperator.Equal, template.GetPath());
AFSearchToken valueToken = new AFSearchToken(AFSearchFilter.Value, AFSearchOperator.Equal, "1", template.AttributeTemplates["SubTree"].GetPath());
AFElementSearch elementSearch = new AFElementSearch(database, "FindSubTreeElements", new[] { templateToken, valueToken });
elementSearch.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

IEnumerable<AFElement> elements = elementSearch.FindElements(0, false, 1000);
foreach (AFElement element in elements)
{
     ProcessElement(element);
}

 

Search tokens are ANDed together

Here, we have two tokens. As mentioned in the search query syntax documentation, please note that all the search tokens are ANDed together.

 

Performance comparison

This search returns 10,000 elements.

Traditional search: 0.20 minutes

New search without caching: 0.14 minutes

New search with caching: 0.09 minutes

 

Wow, this new search is amazing. Fewer lines of code and better performance. A developer's paradise

 

 

3. Find and process a list of child elements given a parent

 

Traditional search

const int pageSize = 1000;
int startIndex = 0;
int totalCount;
AFNamedCollectionList<AFElement> myElements;

do
{
     myElements = AFElement.FindElements(database,
                    database.Elements["LeafElements"], 
                    "*",
                    AFSearchField.Name, false,
                    AFSortField.Name,
                    AFSortOrder.Ascending,
                    startIndex,
                    pageSize,
                    out totalCount);
     ProcessElements(myElements);

     startIndex += pageSize;
} while (startIndex < totalCount);

 

New search

AFSearchToken rootToken = new AFSearchToken(AFSearchFilter.Root, AFSearchOperator.Equal, database.Elements["LeafElements"].GetPath());
AFSearchToken descToken = new AFSearchToken(AFSearchFilter.AllDescendants, AFSearchOperator.Equal, "false");
AFElementSearch elementSearch = new AFElementSearch(database, "FindLeafElements", new[] { rootToken, descToken });
elementSearch.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

IEnumerable<AFElement> elements = elementSearch.FindElements(0, false, 1000);
foreach (AFElement element in elements)
{
     ProcessElement(element);
}

 

Performance comparison

This search returns 100,000 elements.

Traditional search: 9.6 minutes

New search without caching: 5.4 minutes

New search with caching: 0.32 minutes

 

 

4. How the new search obtains a consistent snapshot of a collection

 

We mentioned earlier that the new search has the ability to cache the search results. See for example the properties under AFElementSearch Class. If your application is using the traditional paging pattern and another client is modifying that collection, you may miss items or see duplicates. If you use the new search and opt in to server-side caching, then upon the initial search call, the server will take a "snapshot" of the found items and cache their identifiers. The server will use this cache to provide items as the client iterates. Thus, the client will see a consistent snapshot of the collection at the time of the query and be immune from any modifications to the query result set that could occur as it iterates through the results.

 

 

AFElementSearchBuilder

 

I'm a fan of using the Builder Pattern to construct complex objects. Notice in the above, we have to first construct the filters before constructing the search object. This seems a little backwards. Intuitively, we'd like to be able to construct the search object and then add our filters.

 

We provide an AFElementSearchBuilder class to help with this. Example usage is below:

AFElementSearch elementSearch = AFElementSearchBuilder.Create()
     .SetDatabase(database)
     .SetName("FindLeafElements")
     .AddToken(new AFSearchToken(AFSearchFilter.Root, AFSearchOperator.Equal, elementPath))
     .AddToken(new AFSearchToken(AFSearchFilter.AllDescendants, AFSearchOperator.Equal, "false"))
     .Build();

List<AFElement> elementsList = elementSearch.FindElements(0, false, 1000).ToList();

 

You can follow a similar pattern to write your own builders for AFEventFrameSearch.

 

As mentioned in the search query syntax documentation, please note that all the search tokens are ANDed together.

 

 

Conclusion

 

I hope this post demonstrates that using the new search in AF SDK can be a valuable investment and that it is not that much code to transition. It is desirable when some of the following are true:

  • Your AF database contains large collections (10,000 elements or event frames+)
  • Your applications perform processing on large collections of elements or event frames.
  • Your applications don't require the returned collection to be sorted. You can still opt-in to sorting using AFSearchFilter.SortField.
  • You find that some of your current asset search implementations are slow
  • You want to be ensured that the server provides consistent snapshots of your collections

 

For more information, please consult the PI AF SDK Reference:

Search Query Syntax Overview

AFSearch Class

 

Thank you for reading and as always, questions and comments are welcome below. Please look forward to the next post in this series on asynchronous data access!

Hello Everyone, today we are presenting to you the beginning of a blog series that will cover the most recent enhancements and features in the PI AF SDK 2016 (2.8.0.7444) and greater. My colleagues and myself, with the help of the AF SDK Development Team, will prepare and publish those posts in the upcoming weeks. Our initial thought on this is that there are many new features that are worth an explanation; we also believe this may help you in:

  • Learning the best practices when using the AF SDK
  • Understanding .NET features you may not be aware of and don't really know when to use

 

Topics

 

We already prepared a few topics we want to cover, and you are more than welcome to propose ideas and suggest which topics you would like to be covered first.

Below are the current topics we are preparing to work on. The number is just a reference so you can refer to them in your comments. The order in which the posts will come may differ.

 

#
Post Topic
Summary
1Why you should use the new AF SDK SearchAF SDK 2016 (2.8.0.7444) introduces a new and faster search mechanism for elements and event frames. This post will explain how to leverage the new search features.
2Async with PI AF SDK: IntroductionIntroduction to the new async methods that may help improving performances in specific circumstances. We will answer questions such as when should I use async AF SDK calls? And how to use async and await in .NET?
3Async with PI AF SDK: From Tasks to ObservablesMany async use cases involve launching multiple async tasks and processing them as they complete. This post demonstrates how to do so effectively using an Observable pattern provided by Rx.NET. IObservable is the asynchronous complement to IEnumerable found in the (synchronous) AF SDK bulk calls.
4Event frame searching using the AFEventFrameSearch classDiscuss how to go from legacy AF SDK FindEventFrames search criteria to the newer AFEventFrameSearch class.
5New PI Data Archive RPCs - ReplaceValues and RecordedValuesAtTimesLearn about the newly available RPCs on the PI Data Archive and how to leverage them to maximize your application performances.
6New Attributes TraitsAF Attributes may be identified with specific traits or behaviors, such as Limits, Forecast and Analysis Start Triggers to better allow applications to find, display, and analyze related attributes. This post will look into possibilities offered by these new traits from a developer perspective.

 

Feel free to comment and let us know what topics have the most value for you. Based on your comments we may change / add topics into this list!

 

We hope you'll enjoy reading and discovering new features

 

The PI Developers Club Team

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).

Filter Blog

By date: By tag: