The PI Integrator for Esri ArcGIS (https://livelibrary.osisoft.com/LiveLibrary/content/en/Integrators-v1/GUID-9D6CF267-1959-4AAF-839D-4112EB905F84) can do fantastic things with PI data and position information--assuming you have position information already on hand.  And while it is very feasible to record the static positions of immobile assets, like generators, buildings, and the like, it's not as clear how one would get position information (longitude and latitude, for example) for moving assets into the PI System.

 

This web application (attached as index.html) is one example of how HTML5 and the PI Web API can easily make this happen.  In this example, a web page has been written using HTML and JavaScript, and that web page specifically uses HTML5 geolocating functions (a fantastic blog post on this is here: Geolocation in HTML5) to very precisely determine the position of the client device--for example, if you download this code, and save it to your computer as a .html file, and then open it, it will very precisely determine the location of your laptop or desktop (and leveraging the position chips in phones, it fares even better, determining position down to a few meters).

AppScreenShot.png

 

Having obtained that information, though, it's very easy to display it on the web page, and then even to display it as a map--this has been done before by many other sites.  The challenge, though, is sending these live position values (which refresh at a configurable interval) to the PI System.  This is possible, fortunately, using the PI Web API 2014, which allows a web application to both read and write data to PI tags.  The advantage here is that since the PI Web API is REST-based, the entire application can be written in simple HTML and JavaScript, without having to write C# or C server-side code (thanks to this, the entire web app fits into a single file of just under 500 lines of code).

 

Specifically, then, after determining the location, the web app makes an Ajax call using the PI Web API, sending the latitude and longitude--along with a unique identifier string, which the user can set themselves (allowing multiple users to use the app)--as updates to PI AF Attributes located in a PI AF database (the unique identifier allows the PI Web API to build a path to write to the correct attributes for the given user).  Those attributes reference PI Tags, and thus the continuous stream of position data is archived, allowing a user to track the position of the asset over time (or allowing that data to be easily sent via the PI Integrator for Esri ArcGIS to Esri's ArcGIS geospatial platform).  This required, therefore, that I create a PI AF database (attached), and within it, an element template with Longitude and Latitude attributes (which furthermore allows automatic creation of the required PI tags; for example, I'll create an element for my user name, dlopez@osisoft.com, and the element template allows me to easily create the required PI tags).  But after that, the app lets me set the interval for my data updates, so the values I see in AF will update as fast as I need them to.


AFScreenshot.png

 

Requiring only the PI Web API and a PI System, this application can make it very easily to archive positions of phones, tablets, and computers in the PI System.  This occurs, of course, so long as the user is actually visiting the web app page in their browser, and int he case of a phone or tablet, that is remains unlocked.  Feel free to expand on that, or to use this as an example for a native application; ultimately, the goal of providing this project code is to help with your research and concept proofs, so that you can get started quickly with a very low-cost, low-footprint method of sending live position data to a PI System.

 

As a runner on OSIsoft's racing team, this app lets me send my live position, during an actual run, to the PI System during my training runs, but it could also help track the position of cars, carts, and more.  I hope it's helpful for your research!

 

__________________________________________________________________________________________________________________________

 

<!DOCTYPE html>
<html>
<head>
<title>Geolocator App</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>


// ***********************************************************************
// * DISCLAIMER:
// *
// * All sample code is provided by OSIsoft for illustrative purposes only.
// * These examples have not been thoroughly tested under all conditions.
// * OSIsoft provides no guarantee nor implies any reliability,
// * serviceability, or function of these programs.
// * ALL PROGRAMS CONTAINED HEREIN ARE PROVIDED TO YOU "AS IS"
// * WITHOUT ANY WARRANTIES OF ANY KIND. ALL WARRANTIES INCLUDING
// * THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY
// * AND FITNESS FOR A PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED.
// ************************************************************************


// ***********************************************************************
// * Instructions:
// *
// * This page prompts a user for their unique identifier, then it determines
// * their position using html5 geolocation, after which that unique user ID, the
// * current time, and the location information is sent as an Ajax POST
// * to a PI Web API instance.  The specific PI Web API command given will
// * directly write to PI tags contained in a PI AF database; thus, to use this
// * script, in addition to installing the PI Web API, you must also create a
// * pair of float32 PI tags for each user, of the name <uniqueUserID>-Latitude
// * <uniqueUserID>-Longitude (make sure to set exception and compression
// * settings to 0.000001 and 0.000002, respectively).  Then you must
// * Create a PI AF database where, directly beneath the root element, there
// * are child elements for each user, where each child element is named
// * using the <uniqueUserID> and has within it two double-type attributes
// * named Latitude and Longitude, referencing those aforementioned PI tags.
// * While running this web page, you'll see new location data be written to
// * those tags at a regular interval.
// ************************************************************************




// Used for the global auto-refresh timer
var MyTimer;
// Define global variables; these can be changed via the settings page
var enableGeolocation ;
var MyUserID;
var MyWebAPIURL;
var MyUpdateFrequencySeconds;
var MyPIAFRootElementPath;


// ------------------------------------------------------------------------------------------------------------------------------


// Runs when the page loads
function MyOnloadFunction()
{
  // Perform geolocation and post the location data as JSON
  PerformGeolocationAndSendData();

  // Apply CSS styling
  ApplyMyCSSStyling();
  // Center contents on the page
  centerOnPage("MyContent");
  // Load default values for global variables
  LoadDefaultValues();
  // Check cookies and load values from them if found, overwriting the defaults
  LoadValuesFromCookies();
  // Prompt user for ID if there is no user name stored in a cookie
  promptUserForNameIfNeeded();
  // Save all of the current variable values to the cookies, including the user name
  saveGlobalVariableValuesToCookies();
  // Update form
  UpdateFormWithNewValues();
  // Start recurring refresh timer
  startAutoRefreshTimer();

}


// ------------------------------------------------------------------------------------------------------------------------------


// This is where a user can specify the default values for the update frequency and destination endpoint
function LoadDefaultValues()
{
  // Set the value of the global variables
  enableGeolocation = "Enabled";
  MyWebAPIURL = "https://danlopezlaptop/piwebapi/";
  MyUpdateFrequencySeconds = 30;
  MyPIAFRootElementPath = "\\\\danlopezlaptop\\Geolocation\\Users";
  MyUserID = "dlopez@osisoft.com";
  console.log("Default global variable values loaded...");
}


// ------------------------------------------------------------------------------------------------------------------------------

// Check cookies; if a value from cookies
function LoadValuesFromCookies()
{
  // If there is a cookie value, then use it in place of the default
  if (getCookieValue("cookie_enableGeolocation")=="") {}
  else
  {
  enableGeolocation = getCookieValue("cookie_enableGeolocation");
  }
  if (getCookieValue("cookie_MyWebAPIURL")=="") {}
  else
  {
  MyWebAPIURL = getCookieValue("cookie_MyWebAPIURL");
  }
  if (getCookieValue("cookie_MyUpdateFrequencySeconds")=="") {}
  else
  {
  MyUpdateFrequencySeconds = getCookieValue("cookie_MyUpdateFrequencySeconds");
  }
  if (getCookieValue("cookie_MyPIAFRootElementPath")=="") {}
  else
  {
  MyPIAFRootElementPath = getCookieValue("cookie_MyPIAFRootElementPath");
  }
  if (getCookieValue("cookie_MyUserID")=="") {}
  else
  {
  MyUserID = getCookieValue("cookie_MyUserID");
  }
  console.log("Cookie check complete...");
}


// ------------------------------------------------------------------------------------------------------------------------------


// If the cookie for the user name is blank, then prompt the user for a user name
function promptUserForNameIfNeeded()
{
  // If there is no cookie value for the user name, ask the user for a name; otherwise, use the cookie
  if (getCookieValue("cookie_MyUserID")=="")
  {
  console.log("User ID cookie is blank; user needs to enter in their own ID...");
  MyUserID = prompt("Please enter your user ID (your OSIsoft email)", "user@osisoft.com");
  }
}


// ------------------------------------------------------------------------------------------------------------------------------


// Center the contents on the page by getting the content height, the window heigh, and then calculating the right top margin
function centerOnPage(elementName)
{
  var heightOfContentsDiv = document.getElementById(elementName).clientHeight;
  var heightOfWindow = window.innerHeight;
  document.getElementById(elementName).style.marginTop = (heightOfWindow - heightOfContentsDiv)/2 + "Px";
  document.getElementById(elementName).style.marginLeft = "auto";
  document.getElementById(elementName).style.marginRight = "auto";
}


// ------------------------------------------------------------------------------------------------------------------------------


// Take the current values of the global variables and update the current values in the form fields
function UpdateFormWithNewValues()
{
  // Apply these values to the form
  document.getElementById("enable").value = enableGeolocation;
  document.getElementById("userID").value = MyUserID;
  document.getElementById("webAPIURL").value = MyWebAPIURL;
  document.getElementById("updateFrequency").value = MyUpdateFrequencySeconds;
  document.getElementById("AFRootElementPath").value = MyPIAFRootElementPath;
  console.log("Global variable values written to form fields.");
}


// ------------------------------------------------------------------------------------------------------------------------------


function PerformGeolocationAndSendData()
{
  console.log("Geolocation function entered...");
  // Get the current position
  navigator.geolocation.getCurrentPosition(function (position)
  {
  // Capture the longitude, latitude, and current time
  var MyLatitude = position.coords.latitude;
  var MyLongitude = position.coords.longitude;
  var MyCurrentDateTimeUTC = ((new Date().getTime()) + "");
  MyCurrentDateTimeUTC  = MyCurrentDateTimeUTC.substring(0,MyCurrentDateTimeUTC.length - 3);

  // Format the user ID, longitude, latitude, and current time into a text string for transmission
  var MyResultString = "userIDStart," + MyUserID + ",userIDEnd," + MyCurrentDateTimeUTC + "," + MyLongitude + "," + MyLatitude;
  console.log("Geolocation results: " + MyResultString);


  // Write the user ID, longitude, latitude, and current time display string to an element on the page
  document.getElementById("MyResultStringLink").innerHTML = ("Current Values: [ USER: " + MyUserID + " | TIME: " + getCurrentTimeString() + " | LATITUDE: " + MyLongitude + " | LONGITUDE: " + MyLatitude + " ]");


  // Define default web ID's
  var MyLatitudeWebID  = "A0ETax-J3X7EUKprN8J_ntNpw1OEnU6O75BGCcpy3DasC0QZb6qKEHmm1QWueLZpJvB0gREFOTE9QRVpMQVBUT1BcR0VPTE9DQVRJT05cVVNFUlNcRExPUEVaQE9TSVNPRlQuQ09NfExBVElUVURF";
  var MyLongitudeWebID = "A0ETax-J3X7EUKprN8J_ntNpw1OEnU6O75BGCcpy3DasC0Quob-aaMROFYtHMwd2lmHEAREFOTE9QRVpMQVBUT1BcR0VPTE9DQVRJT05cVVNFUlNcRExPUEVaQE9TSVNPRlQuQ09NfExPTkdJVFVERQ";

  // Call a function to get the web ID for each attribute; that function will then post the data using that web ID
  GetWebID("GET", MyWebAPIURL + "attributes?path=" + MyPIAFRootElementPath + "\\" + MyUserID + "|" + "Latitude", ("{'Value': " + MyLatitude + "}"));
  GetWebID("GET", MyWebAPIURL + "attributes?path=" + MyPIAFRootElementPath + "\\" + MyUserID + "|" + "Longitude", ("{'Value': " + MyLongitude + "}"));


  // Post data (user ID, longitude, latitude, and current time) to an endpoint
  //POSTToEndPoint("POST", MyWebAPIURL, MyResultString);
  //POSTToEndPoint("POST",(MyWebAPIURL + "streams/" + MyLatitudeWebID  + "/value"), ("{'Value': " + MyLatitude + "}"));
  //POSTToEndPoint("POST",(MyWebAPIURL + "streams/" + MyLongitudeWebID + "/value"), ("{'Value': " + MyLongitude + "}"));

  });
}


// ------------------------------------------------------------------------------------------------------------------------------


function getCurrentTimeString()
{
  var MyCurrentDateTime = new Date();
  var MyCurrentDateTimeString = ""
  + MyCurrentDateTime.getFullYear()
  + "-"
  + AppendZeroIfNecessary(MyCurrentDateTime.getMonth())
  + "-"
  + AppendZeroIfNecessary(MyCurrentDateTime.getDate())
  + "T"
  + AppendZeroIfNecessary(MyCurrentDateTime.getHours())
  + ":" 
  + AppendZeroIfNecessary(MyCurrentDateTime.getMinutes())
  + ":"
  + AppendZeroIfNecessary(MyCurrentDateTime.getSeconds())
  +"Z";
  return (MyCurrentDateTimeString);
}


// ------------------------------------------------------------------------------------------------------------------------------


// Get the web ID for an attribute; upon success, use this web ID to post the passed-in data to that attribute
function GetWebID(MyMethod, MyURL, MyContents)
{
  console.log("Get attempted!");
  $.ajax({
  headers: {                           
  'Content-Type': 'application/json'
  },
  type: MyMethod,
  url: MyURL,
  cache: false,
  async: true,
  dataType: 'JSON',
  success: (function (returnedData) {
  console.log("Web ID get from endpoint succeeded: " + returnedData.WebId);
  // Use that web ID to post to the data to the correct stream
  POSTToEndPoint("POST",(MyWebAPIURL + "streams/" + returnedData.WebId  + "/value"), MyContents);
  }),
  error: (function () { console.log("Get from endpoint failed!"); })
  });
}


// ------------------------------------------------------------------------------------------------------------------------------


// Post data to a REST endpoint
function POSTToEndPoint(MyMethod, MyURL, MyContents)
{
  console.log("Post attempted!");
  $.ajax({
  headers: {                           
  'Content-Type': 'application/json'
  },
  type: MyMethod,
  url: MyURL,
  data: MyContents,
  cache: false,
  async: true,
  success: (function () {
  console.log("Post to endpoint succeeded!");
  document.getElementById("MyStatusDiv").innerHTML = getCurrentTimeString() + ": Data successfully sent!";
  }),
  error: (function () {
  console.log("Post to endpoint failed!");
  document.getElementById("MyStatusDiv").innerHTML = getCurrentTimeString() + ": Data send failed!";
  })
  });
}


// ------------------------------------------------------------------------------------------------------------------------------


// If the hour, minute, or second is less than 10, add a zero in front of it
function AppendZeroIfNecessary(input)
{
  if (input < 10)
  {
  return ("0" + input);
  }
  else
  {
  return input;
  }
}


// ------------------------------------------------------------------------------------------------------------------------------


// Apply CSS styling; this is done via javascript so that the app only requires a single file
function ApplyMyCSSStyling ()
{
  // Overall styling
  $("body").css("font-family", "arial");
  // Button Styling
  $(".button").css("color", "#383838 ");
  $(".button").css("background-color", "#CCC");
  $(".button").css("width", "100%");
  $(".button").css("margin-bottom", "10px");
  $(".button").css("padding", "10px");
  // Content div styling
  $("#MyContent").css("width", "80%");
  // Styling for the settings
  $(".fieldWrapper").css("height", "30px");
  $(".fieldWrapper").css("width", "100%");
  $(".fieldWrapper").css("color", "black");
  $(".inputField").css("float", "right");
  // Styling for the results
  $("#MyResultStringLink").css("background-color", "green");
  $("#MyResultStringLink").css("color", "white");
}






// ------------------------------------------------------------------------------------------------------------------------------


function ShowOrHideSettings()
{
  var NameOfSettingsElement = "MySettings";
  // Get the current display state of the desired element
  element = document.getElementById(NameOfSettingsElement);
  style = window.getComputedStyle(element),
  CurrentDisplayState = style.getPropertyValue('display');
  // Toggle the display settings from visible to not visible
  if (CurrentDisplayState == "none")
  {
  document.getElementById(NameOfSettingsElement).style.display = 'block';
  }
  else
  {
  document.getElementById(NameOfSettingsElement).style.display = 'none';
  }
  // Since the size of the contents has changed, center the contents vertically
  centerOnPage("MyContent");
}


// ------------------------------------------------------------------------------------------------------------------------------


// Writes the current global variables values to the cookies
function saveGlobalVariableValuesToCookies()
{
  // Update the cookies that store these values
  document.cookie = ("cookie_enableGeolocation=" + enableGeolocation);
  document.cookie = ("cookie_MyUserID=" + MyUserID);
  document.cookie = ("cookie_MyWebAPIURL=" + MyWebAPIURL);
  document.cookie = ("cookie_MyUpdateFrequencySeconds=" + MyUpdateFrequencySeconds);
  document.cookie = ("cookie_MyPIAFRootElementPath=" + MyPIAFRootElementPath);
  // Read the cookies to the log, to confirm the write:
  console.log("Stored cookie values: " + document.cookie);
}


// ------------------------------------------------------------------------------------------------------------------------------


// Accept the values entered in the settings
function LoadValuesFromForm()
{
  console.log("Old settings: Enable: " + enableGeolocation + " | User ID: " + MyUserID + " | Endpoint URL: " + MyWebAPIURL + " | Update Frequency: " + MyUpdateFrequencySeconds + " | Root path: " + MyPIAFRootElementPath);
  // Update the global variables with the values of the settings form fields
  enableGeolocation = document.getElementById("enable").value;
  MyUserID = document.getElementById("userID").value;
  MyWebAPIURL = document.getElementById("webAPIURL").value;
  MyUpdateFrequencySeconds = document.getElementById("updateFrequency").value;
  MyPIAFRootElementPath = document.getElementById("AFRootElementPath").value;
  console.log("New settings: Enable: " + enableGeolocation + " | User ID: " + MyUserID + " | Endpoint URL: " + MyWebAPIURL + " | Update Frequency: " + MyUpdateFrequencySeconds + " | Root path: " + MyPIAFRootElementPath);
  // Update the cookies using these new values from the form
  saveGlobalVariableValuesToCookies();
  // Reset the auto-refresh, in case the update timer variable changed
  startAutoRefreshTimer();
}


// ------------------------------------------------------------------------------------------------------------------------------


// Gets the value of an individual cookie
function getCookieValue(cname)
{
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++)
  {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1);
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}


// ------------------------------------------------------------------------------------------------------------------------------


// Enables automatic geolocation updates every few seconds by reloading the page
function startAutoRefreshTimer()
{
  console.log("Auto-refresh timer function called at time: " + getCurrentTimeString());
  window.clearTimeout(MyTimer);
  // If the correct variable is enabled
  if (enableGeolocation == "Enabled")
  {
  // Start the auto refresh; every time this elapses, execute the function
  MyTimer = setInterval(function(){
  console.log("Timer elapsed!");
  //location.reload();
  PerformGeolocationAndSendData();
  },(MyUpdateFrequencySeconds*1000));
  console.log("Auto-refresh timer enabled at interval: " + MyUpdateFrequencySeconds + " seconds.");
  }
  else
  {
  // Otherwise, cancel the auto-refresh
  console.log("Auto-refresh timer disabled.");
  window.clearTimeout(MyTimer);
  }
}


// ------------------------------------------------------------------------------------------------------------------------------
</script>
</head>


<body onload="MyOnloadFunction()">
<div id="MyContent">
  <div id="MyStatusDiv" class="button">...</div>
  <div id="MyResultStringLink" class="button">Loading...</div>
  <div id="settingsButton" class="button" onclick="ShowOrHideSettings()">Show / Hide Settings</div>
  <div id="MySettings" style="display:none">
  <div class="fieldWrapper">
  <label for="enable">Enable Geolocation?</label>
  <select class="inputField" name="enable" id="enable" size="1">
  <option value="Enabled" selected="selected">Enabled</option>
  <option value="Disabled">Disabled</option>
  </select>
  </div>
  <div class="fieldWrapper">
  <label for="userID">User ID</label>
  <input class="inputField" type="text" name="userID" id="userID" value="User0" />
  </div>
  <div class="fieldWrapper">
  <label for="AFRootElementPath">AF Root Element Path</label>
  <input class="inputField" type="text" name="AFRootElementPath" id="AFRootElementPath" value="" />
  </div>
  <div class="fieldWrapper">
  <label for="webAPIURL">PI Web API URL</label>
  <input class="inputField" type="text" name="webAPIURL" id="webAPIURL" value="" />
  </div>
  <div class="fieldWrapper">
  <label for="updateFrequency">Refresh (s)</label>
  <input class="inputField" type="text" name="updateFrequency" id="updateFrequency" value="30" />
  </div>
  <div id="updateButton" class="button" onclick="LoadValuesFromForm()">Save Settings</div>
  </div>
</div>


</body>
</html>