Some tracking devices are able to provide Latitude and Longitude information which can be very useful to track mobile assets.

In some situations you might want to store that information and monitor its location in real time.

OSIsoft provides a powerful tool to provide the best experience with Geolocation data. The PI Integrator for Esri® ArcGIS® enables real-time geographic data visualization by connecting your PI System with the Esri ArcGIS platform.

However if you don’t need such powerful information and is only interested in converting Latitude/Longitude information into a geographic address and vice-versa you may be interested in the content of this blog post.

 

Creating a Data Reference for Geolocation conversion.

 

If you are not familiar with developing new Data References for AF Attributes please refer to the Developing Applications with PI AF SDK course in the Custom Data References section.

Now that you have already watched all the videos, we can start with the principles of any Data Reference.

 

References

 

In order to build this project we need to add two additional .dll libraries: OSIsoft.AFSDK v4.0.0 and System.Web.Extensions.v4.0.0.

We will also use some Using directives to make our lives easier.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Time;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Web.Script.Serialization;
using System.Net;
using System.IO;

 

Header

 

Before creating any class we need to specify some header parameters to our code with the following parameters:

 

[Guid("<key>"), Serializable(), Description("Name;description")]

 

Where:

·        key can be created in Visual Studio à Tools à Create GUID àOption 5: [GUID(“xxxxxxxx-xxxx … xxxx”)].

·        Name is the DataReference name.

·        Description is the Data Reference description.

 

namespace GeolocationDR
{
    [Guid("xxxxxxxx-xxxx … xxxx"), Serializable(), Description("Geolocation;Convert coordinates")]

    public class Geolocation : AFDataReference
    {
    }
}

 

Configuration String

 

Every Data Reference has its configuration string shown in the right bottom panel whenever the attribute is selected.

The configuration string can have any format since it can be parsed and interpreted correctly.

 

In this example it will have the following format:

Latitude=<AttributeName>;Longitude=<AttributeName>;OutType=Address

This post will approach how to create a Data Reference that converts Latitude and Longitude into a Geographic Address.

Let’s go to some coding example.

 

There are some public properties that can be overwritten such the ConfigString property. Please refer to the AFSDK help file (PI System Explorer à Help à Help Topics) for more information.

It is possible to build the Connection String by using the String Builder Class.

 

//Gets or Sets the configstring
public override string ConfigString
{
    get
    {
        StringBuilder sb = new StringBuilder();

        if (String.IsNullOrEmpty(latAttribute) || String.IsNullOrEmpty(longAttribute))//verifies if the variables were already set
            sb.AppendFormat("Coordinates Not Set;");
        else
        {
            sb.AppendFormat("{0}={1};", "Latitude", latAttribute); //Shows the Latitude attribute name
            sb.AppendFormat("{0}={1};", "Longitude", longAttribute); //Shows the Longitude attribute name
        }
        sb.AppendFormat("{0}={1};", "OutType", "Address"); //Adds the output type to the connection string
        return sb.ToString();
    }

    set //called by hitting the Settings button or typing the Config String directly
    {
        if (value != null) //value contains the configuration string from PSE
        {
            var tokens = value.Split(';'); //gets each term before the ';' character

            foreach (var token in tokens)
            {
                var keyvalue = token.Split('=');

                switch (keyvalue[0].ToLower())
                {
                    case "latitude": //sets config for lattude attribute
                        latAttribute = keyvalue[1];
                        break;
                    case "longitude": //sets config for longitude attribute
                        longAttribute = keyvalue[1];
                        break;
                }
            }
        }
        SaveConfigChanges(); //makes the changes persistent
    }
}

 

Once the Configuration String is set we need to get the Input Attributes we will use for our Data Reference.

 

Setting the inputs for the Data Reference

 

In order to calculate the result from existing attribute values we need to tell AF what those attributes are. We can use the GetInputs() method to load the input attributes.

 

//Gets the input Attributes
public override AFAttributeList GetInputs(object context)
{
    AFAttributeList inputs = new AFAttributeList();

    if (!String.IsNullOrEmpty(latAttribute))
    {
        inputs.Add(this.GetAttribute(latAttribute)); //Adds the Latitude Attribute to the input attribute colletion
    }

    if (!String.IsNullOrEmpty(longAttribute))
    {
        inputs.Add(this.GetAttribute(longAttribute)); //Adds the Longitude Attribute to the input attribute colletion
    }

    return inputs;
}

 

Setting the output value

 

The GetValue() method can be also overwritten to set the value of the attribute the Data Reference was configured to.

The inputValues variable has the collection of the input attribute values in an array format and the function AFValue return parameter writes directly to the attribute value.

 

public override AFValue GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
{
    AFValue value = new AFValue();
    AFValue result = new AFValue();

    if (inputAttributes.Count == 2) //Checks if both Latitude and Longitude attributes are set
    {
        value = GetAddressByCoordinates(inputValues[0].ToString(), inputValues[1].ToString()); //Gets data from Geocoding API

        result.Timestamp = inputValues[0].Timestamp; //Sets the attribute timestamp
        result.Value = value.Value; //Sets the attribute value
    }
    else //returns bad if the inputs are not set
    {
        result.Status = AFValueStatus.Bad;
        result.Value = AFSystemStateCode.BadInput;()
    }
    return result;
}

 

Geocoding API

 

Note that GetAddressByCoordinates() method is a custom function developed to get information from the Google Maps Geocoding API. This is a very simple API provided to get geographic information.

All we need to do is to request a JSON or XML response from https://maps.googleapis.com/maps/api/geocode/

It is necessary to pass some parameters to that URL to get the data we want. So that if we want to get information from a location we can simply request:

 

https://maps.googleapis.com/maps/api/geocode/json?<location>&key=API_KEY

For example:

https://maps.googleapis.com/maps/api/geocode/json?OSIsoft&key=API_KEY

 

Where API_KEY is found in the Google Maps Geocoding API page by clicking in GET A KEY.

By using the same idea, we can get information from coordinate values by requesting:

 

https://maps.googleapis.com/maps/api/geocode/json?latlng=<LatitudeValue>,<LongitudeValue>&key=API_KEY

For Example:

https://maps.googleapis.com/maps/api/geocode/json?latlng=37.7196078,-122.161488&key=API_KEY

 

In our example we are passing Latitude and Longitude values from two AF Attributes. So that we can use the GetAddressByCoordinates() and the LoadJson() functions below to get the desired information.

The LoadJson() function is responsible for requesting data and breaking down the JSON response.

 

public AFValue GetAddressByCoordinates(String latitude, String longitude)
{
    AFValue address = new AFValue();

    string url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=" + latitude + "," + longitude + "&key=<Your Key Here>";

    Dictionary<string, dynamic> result = LoadJson(url);//Handles the JSON response from url

    try
    {
        String textResult = result["results"][0]["formatted_address"];
        address.Value = textResult;
    }
    catch
    {
        address.Status = AFValueStatus.Bad;
        address.Value = AFSystemStateCode.BadInput;
    }
    return address;
}

static public Dictionary<string, dynamic> LoadJson(String url)
{
    WebRequest request = WebRequest.Create(url);//Creates a request to the server           
    WebResponse webResponse = request.GetResponse();//Gets the web response           
    Stream dataStream = webResponse.GetResponseStream();//Gets a stream with the JSON response           
    StreamReader reader = new StreamReader(dataStream);//Gets a StreamReader with the JSON response           
    String text = reader.ReadToEnd();//creates a string with the JSON text

    var jss = new JavaScriptSerializer();//Deserializes the JSON string
    var result = jss.Deserialize<Dictionary<string, dynamic>>(text);

    return result;
}

 

Registering the Data Reference .dll

 

After all you just need to open the cmd as administrator, navigate to %pihome64%\AF and run the following command:

RegPlugIn64.exe "<.dll path>"

Once it is done you will be able to find it listed in the Data Reference drop down list:

 

 

 

Using the new Data Reference

 

The last step is to configure the Configuration String correctly on the attribute according to what we defined in the code.

 

 

 

Adding a custom UI editor for the settings

 

This property/function is responsible for calling a Windows Form whenever we hit the settings button.

In order to do it correctly we need to initiate the Form constructor with two parameters as shown below.

 

public DREditor(Geolocation dr, bool isReadOnly)
{
    InitializeComponent();
}

 

Where the Geolocation Class is our actual Data Reference Class.

 

Building the Form

 

We can build our Form as we want with common components such ListBoxes, ComboBoxes, DatePickers, etc.. And we can also use AFSDK UI components such AFDatabasePickers, AFTreeViews, etc...

We won't talk about logics in this part but how to communicate between the UI and our Data Reference main class.

So that we can build a UI like the one below where we will choose a parameter by using RadioButtons and two other parameters by using two ComboBoxes.

 

 

Calling the Form

 

Since you passed an instance of your Class to your Form constructor you can simple let it public to all the Form members by using the example below:

 

public partial class DREditor : Form
{
    Geolocation myDR = new Geolocation();
    public DREditor(Geolocation dr, bool isReadOnly)
    {
        InitializeComponent();
        myDR = dr;
    }
}

 

Then we can build some properties to pass the parameters to the Data Reference Class.

 

private void btnOK_Click(object sender, EventArgs e)
{
  if (attributeType == 0)
  {
    if (cbLatitude.SelectedIndex != -1) //checks if the ComboBox is still empty
      myDR.sourceLatitude = cbLatitude.SelectedItem.ToString(); //Sets the sourceLatitude property
    else
      return;
  if (cbLongitude.SelectedIndex != -1) //checks if the ComboBox is still empty
    myDR.sourceLongitude = cbLongitude.SelectedItem.ToString(); //Sets the sourceLongitude property
  else
    return;
  }
  else if (attributeType == 1) //checks which Radio Button is selected
    myDR.sourceAddress = cbLatitude.SelectedItem.ToString(); //Sets the sourceAddress property
  myDR.targetAttributeType = attributeType;
  this.Close();
  this.Dispose();
}
//Gets or Sets the source longitude attribute
public String sourceLongitude
{
  get
  {
    return longAttribute;
  }
  set
  {
    if (longAttribute != value)
    {
      longAttribute = value;
      SaveConfigChanges();
    }
  }
}

 

We can repeat this for all the components we need to get data from in the UI.

 

private void btnOK_Click(object sender, EventArgs e)
{
  if (attributeType == 0)
  {
    if (cbLatitude.SelectedIndex != -1) //checks if the ComboBox is still empty
      myDR.sourceLatitude = cbLatitude.SelectedItem.ToString(); //Sets the sourceLatitude property
    else
      return;

  if (cbLongitude.SelectedIndex != -1) //checks if the ComboBox is still empty
    myDR.sourceLongitude = cbLongitude.SelectedItem.ToString(); //Sets the sourceLongitude property
  else
    return;
  }
  else if (attributeType == 1) //checks which Radio Button is selected
    myDR.sourceAddress = cbLatitude.SelectedItem.ToString(); //Sets the sourceAddress property

  myDR.targetAttributeType = attributeType;

  this.Close();
  this.Dispose();
}

//Gets or Sets the source longitude attribute
public String sourceLongitude
{
  get
  {
    return longAttribute;
  }
  set
  {
    if (longAttribute != value)
    {
      longAttribute = value;
      SaveConfigChanges();
    }
  }
}

 

Please access this Github repository for the full example.