Skip navigation
All Places > PI Developers Club > Blog > 2017 > April
2017

Introduction

 

On the first 3 blog posts (part 1, part 2 and part 3) about developing the Google Maps custom symbol for PI Vision 3 (2016, 2016 R2, 2017 and 2017 R2)., I have shown you how to create a custom symbol showing a Google Map with some markers that represent the updated location from many assets.

 

On this blog post, I will show how to show display on the map historical data by using event frames.

 

You can download the custom symbol in this GitHub repository.

 

Understanding the problem

 

In the past, I have recorded several times the geolocation coordinates when I've walked from my old home to our old OSIsoft office here in São Paulo using an Android app called RunKeeper. After I've stopped recording the activity, the app allowed me to download all routes of activities recorded by downloading a GPX file. By creating a simple console application, I was able to send all the latitude and longitude values to a PI System.

 

On my PI AF Server, I have created a new database called GMapsPart4 with only 1 element called Marcos. This element is derived from an element template called UserTemplate with 2 attribute template called Latitude and Longitude.

 

 

I've also created an event frame for each activity downloaded in the GPX format to make it easier access this kind of information programmatically. There are 3 event frames on this database. The primary referenced element in all of them is the Marcos element. Each attribute from the element map the attribute from the Marcos element as shown below. An event frame template was created since all EFs follow the same pattern.

 

 

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

 

 

 

As you can see, the Event Frames mapped with the Marcos elements are below. Once the user clicks on any event frame, the symbol retrieves the historical geolocation data within the time range defined by the event frame. Using Google Maps API, we have added a path on the map and a marker. The marker moves when the user changes the slider's value, which is above the map.

 

This will only happen if the user selects the Historical Mode on the configuration options as shown below:

 

 

 

Adding the PI Web API Client library for AngularJS to the PI Vision infrastructure

 

PI Vision does not allow you to retrieve event frames or recorded values through its native extensibility model. Although for the majority of the symbols, this is not a problem, there are some use cases which require the custom symbol to interact with PI Web API in order to retrieve additional information. Please refer to my previous blog post in order to make the piwebapi Angular service available on your PI Vision infrastructure, otherwise, this symbol won't work on your PI Vision 2017 (and 2016 R2).

 

 

RangeSlider

 

The slider above the map is an objected created with the RangeSlider.js JavaScript library. Please download the library from their official site and paste it on the \symbols\ext\libraries folder. I am using version 2.3.

 

 

Updating the sym-gmaps-template.html

 

Following the examples provided on the official site from RangeSlider.js library, I've added the horizontal slider on the top of the symbol. Below the map, I have added a div node in order to list the event frames retrieved.

 

 

<div ng-if="config.HistoricalMode" style="width:100%;height:50px;">
    <input type="range"
           min="0"
           max="{{rangeMax}}"
           step="1"
           value="0"
           data-orientation="horizontal">
</div>
<div id="container" style="width:100%;height:calc(100% - 150px);">


</div>


<div ng-if="config.HistoricalMode" class="activities-pane">
    <div ng-repeat="activity in activitiesList" ng-class="activity.WebId == selectedActivity.WebId ? 'activity-non-selected' : 'activity-selected'" ng-click="updateActivity(activity)">
        <p>
            {{activity.Name}}
        </p>
    </div>
</div>


<style>
    .activities-pane {
        background-color: white;
        padding: 1px;
        height: 100px;
        overflow-y: auto;
    }
        .activities-pane div {
            height: 31px;
            border: azure;
            margin: 4px;
            padding-top: 8px;
        }
        .activities-pane p {
            color: white;
        }
    .activity-selected {
        background: brown;
    }
    .activity-non-selected {
        background: blue;
    }
</style>

 

Finally, to make things easier, I've added a new style within the HTML itself although the best practice would be to have a new CSS file just for this purpose.

 

Updating the sym-gmaps.js

 

I won't describe all the steps required to update the sym-gmaps.js from part 3 to part 4. I will focus on the main logic for you understand what is going on under the hood.

 

The first thing is to inject the piwebapi service and add the HistoricalMode property of the main definition:

 

       inject: ['piwebapi'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 600,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)',
                LatName: 'Latitude',
                LngName: 'Longitude',
                HistoricalMode: false,
                OpenInfoBox: true,

 

 

If the HistoricalMode is false, the symbol would work very similar to the symbol developed on part 3. The new features can be viewed if HistoricalMode is true.

 

On the init function, call the piwebapi functions to initialize the service as described on the other blog post:

 

   symbolVis.prototype.init = function init(scope, elem, piwebapi) {
piwebapi.ConfigureInstance("https://marc-web-sql.marc.net/piwebapi", true); 

 

We need to use the extensibility model to extract the AF database path and the root element name.

 

 

            if ((scope.elementName == undefined) && (scope.lastDataWithPath.Rows[0].Path.substring(0, 3) == "af:")) {
                var elementPath = (data.Rows[0].Path.split("|")[0]).substring(3);
                var stringData = elementPath.substring(2).split("\\");
                scope.databasePath = "\\\\" + stringData[0] + "\\" + stringData[1];
                scope.elementName = stringData[2];
            }

 

 

The AF database path is used to get the AF database WebId, which wil the element name, both are used as inputs for the GetEventFrames method from the AssetDatabase controller.

 

 if (scope.activitiesList == undefined) {
                        piwebapi.assetDatabase.getByPath(scope.databasePath, null, null).then(function (response) {
                            var webId = response.data.WebId;
                            piwebapi.assetDatabase.getEventFrames(webId, null, null, "*", null, 100, null, scope.elementName, "UserTemplate", true, null, null, null, null, null, null, "*-900d").then(function (response) {
                                scope.activitiesList = response.data.Items;
                                scope.selectedActivity = response.data.Items[0];
                                scope.updateActivity(scope.selectedActivity);
                            });
                        });
                    }

 

The scope.updateActivity method updates the UI for a given activity (which is an Event Frame) following the steps below:

 

  • Clean markers and paths from the map.
  • Get the attributes WebId from the user element.
  • Get interpolated values in bulk by using the GetInterpolatedAdHoc method from the StreamSet controller.
  • Polish the values to be consumed by the Google Maps API.
  • Create the path with the retrieved PI Values and add it to the Google Map.
  • Update the color of the marker if needed.
  • Instantiate the rangeSlider object using the JavaScript library methods.

 

   scope.updateActivity = function (activity) {
            if ((scope.marker != null) && (scope.marker != undefined)) {
                scope.marker.setMap(null);


            }
            if ((scope.routePath != null) && (scope.routePath != undefined)) {
                scope.routePath.setMap(null);
            }


            scope.selectedActivity = activity;
            scope.loadingGeolocation = true;
            var elementWebId = scope.selectedActivity.RefElementWebIds[0];
            piwebapi.element.getAttributes(elementWebId).then(function (response) {
                scope.attributes = response.data.Items;
                var webIds = new Array(scope.attributes.length);
                for (var i = 0; i < scope.attributes.length; i++) {
                    webIds[i] = scope.attributes[i].WebId;
                }


                piwebapi.streamSet.getInterpolatedAdHoc(webIds, activity.EndTime, null, true, "30s", null, activity.StartTime).then(function (response) {
                    for (var i = 0; i < response.data.Items.length; i++) {
                        var currentItem = response.data.Items[i];
                        if (currentItem.Name == scope.config.LatName) {
                            scope.latitudeTrack = currentItem.Items;
                        }
                        if (currentItem.Name == scope.config.LngName) {
                            scope.longitudeTrack = currentItem.Items;
                        }
                    }


                    var bounds = new google.maps.LatLngBounds();
                    var routeCoordinates = [];
                    for (var i = 0; i < scope.latitudeTrack.length; i++) {
                        if ((scope.latitudeTrack[i].Good == true) && (scope.longitudeTrack[i].Good == true)) {
                            var pos = { lat: scope.latitudeTrack[i].Value, lng: scope.longitudeTrack[i].Value };
                            routeCoordinates.push(pos);
                            var point = new google.maps.LatLng(pos.lat, pos.lng);
                            bounds.extend(point);
                        }
                    }
                    scope.rangeMax = routeCoordinates.length;


                    scope.routePath = new google.maps.Polyline({
                        path: routeCoordinates,
                        geodesic: true,
                        strokeColor: '#FF0000',
                        strokeOpacity: 1.0,
                        strokeWeight: 2
                    });


                    scope.routePath.setMap(scope.map);
                    scope.map.fitBounds(bounds);


                    scope.marker = new google.maps.Marker({
                        position: routeCoordinates[0],
                        map: scope.map
                    });


                    updateMarkerColor(scope.marker, scope.config);


                    $('input[type="range"]').rangeslider({
                        polyfill: false,
                        onSlide: function (position, value) {
                            x = Math.round(position);
                            var geoPosition = routeCoordinates[x];
                            scope.marker.setPosition(geoPosition);
                        }
                    });
                });
            });
        };

 

 

 

Conclusions

 

The first PI Coresight which has the extensiblity model is the 2016 version. It allows you to develop custom symbols for PI Coresight. The PI Coresight extensibility model keeps sending live data to be consumed by the custom symbol. Nevertheless, there are lot of use cases and interest by the PI DevClub community to integrate the custom symbol with PI Web API. This blog post provides information about how to achieve this goal in order to create richer and more valueable custom symbols for your enterprise and/or your customers.

Introduction

 

Since the release of PI Vision extensibility, I have seen a lot of questions on PI Developers Club about how to make HTTP calls against PI Web API within the custom symbol methods.

 

On my last blog post, I have shown step by step about how to generate a PI Web API Client library for AngularJS using Swagger codegen. This library is available for download on the dist folder from this GitHub repository. What I do think it would be very interesting and useful for our community is to use this library when developing custom PI Vision symbols. Who wouldn't want to use this feature?

 

Disclaimer

 

Before we start describing the procedures, it is good to remind that you that we will change PI Vision source JavaScript source code. As a result, there are a lot of risks involved, such as:

  • If the changes you've done were not well done, your PI Vision might not load anymore. In this case, you might need to repair the installation to replace all JavaScript files.
  • After a PI Vision upgrade,  all your changes will be undone. You will have to do the procedure again.
  • I strongly recommend testing on a development environment first.
  • Although I don't expect major issues, we haven't tested the problems when using this library within PI Vision.

 

All in all, there are risks involved. Just consider them before following the procedure.

 

UPDATE: A new way was published in order NOT to change the PI Vision 3 source. Click here to read a better approach.

 

 

Making the piwebapi service available on PI Vision

 

Yes, I am writing this procedure only for PI Vision 2016 R2. It should also work on PI Vision 2016 but I haven't tested. If you do test, please write a comment below!

 

Here are the steps:

 

1 - Open the browser and go to this GitHub repository. Download the source code package to a zip file. Within this file, copy the angular-piwebapi-kerberos.min.js (or piwebapi-kerberos.js) file to %PIHOME64%\Coresight\Scripts\app\editor folder (2016 R2) or %PIHOME64%\PIVision\Scripts\app\editor folder (2017).

2 - Edit the Index.html file located on the %PIHOME64%\Coresight\Views\Home folder (2016 R2) or %PIHOME64%\PIVision\Views\Home folder (2017) by adding a reference to angular-piwebapi-kerberos.min.js just below "@Scripts.Render("~/bundles/libraries/jstz")". Please refer to the code below:

 


    @Scripts.Render("~/bundles/libraries/jquery")    
    @Scripts.Render("~/bundles/jqueryui")    
    @Scripts.Render("~/bundles/jqueryui/layout")
    @Scripts.Render("~/bundles/jquery-ui-patch")
    @Scripts.Render("~/bundles/libraries/hammer")
    @Scripts.Render("~/bundles/libraries/angular")
    @Scripts.Render("~/bundles/libraries/angular-gridster")
    @Scripts.Render("~/bundles/libraries/jstz")


  <script src="/PIVision/Scripts/app/editor/angular-piwebapi-kerberos.min.js" /></script>


    @Scripts.Render("~/bundles/kendo-patch")

 

Use src="/Coresight/Scripts/..." instead if you are using 2016 R2 or older versions.

 

3 - Edit the coresight.app.js file (2016 R2) or PIVisualization.app.js (2017) located on the %PIHOME64%\PIVision\Scripts\app\editor (%PIHOME64%\Coresight\Scripts\app\editor for 2016 R2) by adding ngPIWebApi to the depedencies module list from PI Vision. Please refer to the code below:

 

    angular.module(APPNAME, ['ngAnimate', 'Chronicle', 'osi.PiDialog', 'osi.PiToast', 'coresight.routing', 'kendo.directives', 'gridster', 'ngPIWebApi'])
        .config([
            '$animateProvider',
            '$compileProvider',
            '$httpProvider',
            config])

 

Do not forget to save both files.

 

That is all! Easy, right?

 

 

Creating a custom symbol using the piwebapi service

 

Now that our PI Vision is now able to inject our piwebapi service, which is a PI Web API client library using AngularJS, let's create a custom symbol that will make an HTTP request againt the main PI Web API endpoint and display the JSON on the screen.

 

Create a new file name sym-piwebapi-basic-sample.js on the %PIHOME64%\PIVision\Scripts\app\editor\symbols\ext folder  (%PIHOME64%\Coresight\Scripts\app\editor\symbols\ext for 2016 R2) with the following content:

 

 

For 2017:

 

(function (PV) {
    function symbolVis() { }
    PV.deriveVisualizationFromBase(symbolVis);


    var definition = {
        typeName: 'piwebapi-basic-sample',
displayName: 'PI Web API Client library basic sample',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
        inject: ['piwebapi'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 400,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)'
            };
        },
        visObjectType: symbolVis
    };




    symbolVis.prototype.init = function init(scope, elem, piwebapi) {
piwebapi.ConfigureInstance("https://marc-web-sql.marc.net/piwebapi", true);


        console.log('Starting init scope: ' + scope);
        console.log('Starting init scope: ' + elem);

this.onDataUpdate = dataUpdate;
        this.onConfigChange = configChanged;
        this.onResize = resize;




        function configChanged(config, oldConfig) {
console.log('configChange called');
        };


        function resize(width, height) {
console.log('resized called');
        } 

        function dataUpdate(data) {
piwebapi.home.get().then(function (response) {
scope.data = response.data;
});
        }
    }








    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

 

Create a new file name sym-piwebapi-basic-sample-template.html on the %PIHOME64%\PIVision\Scripts\app\editor\symbols\ext folder (%PIHOME64%\Coresight\Scripts\app\editor\symbols\ext for 2016 R2) with the following content:

 

<center>
    <br /><br />
    <p style="color: white;">{{data}}</p>
</center>

 

You can find more custom PI Vision symbols samples that inject piwebapi service here.

 

Time to test our custom library. Dragging any element and dropping it on the screen, we can see that JSON response from the main PI Web API endpoint.

 

 


Conclusions

 

The custom symbol developed in this blog post is very simple. Nevertheless, its main purpose is to demonstrate that after making the changes in order to add the PI Web API client library for AngularJS to PI Vision, it is possible to inject the piwebapi service. As a result, it gets easier to make HTTP requests against PI Web API using this client library.

 

Finally, please write comments about this project and share your thoughts!

Introduction

 

The upcoming PI Web API 2017 release will come with lots of great new features. One of the them is the Swagger specification which is a JSON string that allows you to generate client-side libraries in many different programming languages including Java, C#, AngularJS etc...

On this blog post, I will show you how to have access to the PI Web API Swagger JSON specification definition and generate a PI Web API library to be used with any web site developed using AngularJS. If you want only to download the library please visit this GitHub repository. A sample web application using this library is available here.

 

What is Swagger?

 

If you visit the Swagger web site, this is how they define their product: "The goal of Swagger™ is to define a standard, language-agnostic interface to REST APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined via Swagger, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interfaces have done for lower-level programming, Swagger removes the guesswork in calling the service.".

 

Please visit this page to find more information about getting started with Swagger.

 

How to access the PI Web API Swagger definition?

 

Just make a GET request against the following url: https://servername/piwebapi/help/specification?pretty=true. You will receive a JSON response back with the Swagger definition.

Remember that you must be using at least PI Web API 2017 in order to have access to the Swagger definition. This url won't work with PI Web API 2016 R2 and older versions of the product. Nevertheless, if you download the library from our GitHub repository, it will be compatible with all PI Web API versions. Just expect to receive some exceptions in case you are using a method that is not implemented on the server side as result of not using the latest PI Web API version.

 

How to generate client libraries using the PI Web API Swagger definition?

 

You can download the Swagger Code Generator directly from this GitHub repository. This application allows generation of API client libraries automatically with the OpenAPI specification. If you visit their repository, you will see that the following languages are supported:

 

  • API clients: ActionScript, Bash, C# (.net 2.0, 4.0 or later), C++ (cpprest, Qt5, Tizen), Clojure, Dart, Elixir, Go, Groovy, Haskell, Java (Jersey1.x, Jersey2.x, OkHttp, Retrofit1.x, Retrofit2.x, Feign), Node.js (ES5, ES6, AngularJS with Google Closure Compiler annotations) Objective-C, Perl, PHP, Python, Ruby, Scala, Swift (2.x, 3.x), Typescript (Angular1.x, Angular2.x, Fetch, jQuery, Node)

 

Read the readme.md file in order to build from source with Java and Apachen Maven.

 

The next step is to run swagger-codegen using JAVA and the Command Prompt:

 

java -jar swagger-codegen/modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate -i https://servername/piwebapi/help/specification?pretty=true -l typescript-angular -o /PIWebAPIClient/swagger-project/PI-Web-API-Client-library-for-AngularJS

 

The TypeScript AngularJS library will be available under the PIWebAPIClient\swagger-project\PI-Web-API-Client-library-for-AngularJS folder.

 

Since I couldn't control very well how the library is going to be generated with the Swagger Generator, I have decided to create my own generator which is available here. I have described how to generate a client library with the Swagger code generator so you can learn how to generate libraries for many different languages and platforms.

 

 

Compiling the TypeScript libraries to generate JavaScript libraries with VS Code

 

Create a new folder under the PI-Web-API-Client-library-for-AngularJS named src and move the API folder to this new directly.

 

If you navigate through the PI-Web-API-Client-library-for-AngularJS\src\API\Client folder, you will see files with ts extension, which are TypeScript files. We need to convert them to JavaScript files since we want to generate a JavaScript AngularJS service.

 

Let's start installing TypeScript gloabally with the command:

 

npm install -g typescript@2.3.0 

 

 

Note 1: I wasn't able to compile the project using typescript 2.4.2. This is why you must be using typescript 2.3.0 to avoid errors on the angular.d.ts definitions.

Note 2: you must have NodeJS installed. Click here to download in case this product is not installed on your machine.

 

Open Visual Studio Code and click on File --> Open Folder. And select the PI-Web-API-Client-library-for-AngularJS folder. Click on View --> Integrated Terminal and type on the terminal:

 

tsc --init

 

This will create the tsconfig.json file on the root folder. This file should be used to set up options for the TypeScript compiler.

 

Next, press SHIFT + CTRL + P to show all the commands available and type "task" and click on "Tasks:Configure Task Runner". Press enter and select "TypeScript - tsconfig.json". This will create the file tasks.json under the .vscode folder. When you compile the project, Visual Studio Code will read the content of this tasks.json file and run the TypeScript compiler. If you open the tasks.json, you will realize that the "command" field is "tsc".

 

Let's compile this product by pressing SHIFT + CTRL +B and see what happens..... If you want to learn about Visual Studio Code key shortcuts, click here.

 

Nevertheless, there are more than 99 errors:

 

pastedImage_4.png

 

The main error is  "Cannot find namespace 'ng'.  This means that we need to add the AngularJS typescript library in order to compile our project."

 

Let's install Bower to download the AngularJS TypeScript library:

 

npm install -g bower 

 

And then type:

 

bower install angular-ts

 

This will create a folder bower_components and some subfolders:

 

pastedImage_2.png

 

The file we need to reference is bower_components\angular-ts\angular.d.ts which contains the ng TypeScript namespace.

 

The file that we need to change in order to add this reference is api.d.ts. All other files located on the API\Client folder reference this file.

 

Open the api.d.ts file on the editor and add the following line.

 

/// <reference path="../../../bower_components/angular-ts/angular.d.ts" />

 

Let's compile again by pressing SHIFT + CTRL +B. Now we receive a difference type of error:

 

pastedImage_3.png

 

The problem is with the following function which is present on all *Api.ts files:

 

        private extendObj<T1,T2>(objA: T1, objB: T2) {
            for(let key in objB){
                if(objB.hasOwnProperty(key)){
                    objA[key] = objB[key];
                }
            }
            return <T1&T2>objA;
        }

 

 

The solution is to replace T1 and T2 to any when defining the input variables:

 

        private extendObj<T1,T2>(objA: any, objB: any) {
            for(let key in objB){
                if(objB.hasOwnProperty(key)){
                    objA[key] = objB[key];
                }
            }
            return <T1&T2>objA;
        }

 

You can use Find & Replace in Files feature of Visual Studio Code to fix this problem on the 34 files.

 

 

pastedImage_1.png

 

 

Finally, if you compile again, you won't see any errors.

 

Creating the PI Web API AngularJS Module

 

 

Create a new PIWebApi.ts file under the src\ts folder with the following content:

 

class PIWebApi {


private basePath : string
private useKerberos : boolean
private username : string
private password : string
public defaultHeaders : any = {}
public analysis : OSIsoft.PIDevClub.PIWebApiClient.AnalysisApi
public analysisCategory : OSIsoft.PIDevClub.PIWebApiClient.AnalysisCategoryApi
public analysisRule : OSIsoft.PIDevClub.PIWebApiClient.AnalysisRuleApi
public analysisRulePlugIn : OSIsoft.PIDevClub.PIWebApiClient.AnalysisRulePlugInApi
public analysisTemplate : OSIsoft.PIDevClub.PIWebApiClient.AnalysisTemplateApi
public assetDatabase : OSIsoft.PIDevClub.PIWebApiClient.AssetDatabaseApi
public assetServer : OSIsoft.PIDevClub.PIWebApiClient.AssetServerApi
public attribute : OSIsoft.PIDevClub.PIWebApiClient.AttributeApi
public attributeCategory : OSIsoft.PIDevClub.PIWebApiClient.AttributeCategoryApi
public attributeTemplate : OSIsoft.PIDevClub.PIWebApiClient.AttributeTemplateApi
public attributeTrait : OSIsoft.PIDevClub.PIWebApiClient.AttributeTraitApi
public batch : OSIsoft.PIDevClub.PIWebApiClient.BatchApi
public calculation : OSIsoft.PIDevClub.PIWebApiClient.CalculationApi
public configuration : OSIsoft.PIDevClub.PIWebApiClient.ConfigurationApi
public dataServer : OSIsoft.PIDevClub.PIWebApiClient.DataServerApi
public element : OSIsoft.PIDevClub.PIWebApiClient.ElementApi
public elementCategory : OSIsoft.PIDevClub.PIWebApiClient.ElementCategoryApi
public elementTemplate :OSIsoft.PIDevClub.PIWebApiClient. ElementTemplateApi
public enumerationSet : OSIsoft.PIDevClub.PIWebApiClient.EnumerationSetApi
public enumerationValue : OSIsoft.PIDevClub.PIWebApiClient.EnumerationValueApi
public eventFrame : OSIsoft.PIDevClub.PIWebApiClient.EventFrameApi
public home : OSIsoft.PIDevClub.PIWebApiClient.HomeApi
public point : OSIsoft.PIDevClub.PIWebApiClient.PointApi
public securityIdentity : OSIsoft.PIDevClub.PIWebApiClient.SecurityIdentityApi
public securityMapping : OSIsoft.PIDevClub.PIWebApiClient.SecurityMappingApi
public stream : OSIsoft.PIDevClub.PIWebApiClient.StreamApi
public streamSet : OSIsoft.PIDevClub.PIWebApiClient.StreamSetApi
public system : OSIsoft.PIDevClub.PIWebApiClient.SystemApi
public table : OSIsoft.PIDevClub.PIWebApiClient.TableApi
public tableCategory : OSIsoft.PIDevClub.PIWebApiClient.TableCategoryApi
public timeRule : OSIsoft.PIDevClub.PIWebApiClient.TimeRuleApi
public timeRulePlugIn : OSIsoft.PIDevClub.PIWebApiClient.TimeRulePlugInApi
public unit : OSIsoft.PIDevClub.PIWebApiClient.UnitApi
public unitClass : OSIsoft.PIDevClub.PIWebApiClient.UnitClassApi
private httpService : any
private httpParamSerializer : any
private base64 : any




constructor(httpService : any, httpParamSerializer: any, base64 : any) {
this.httpService = httpService;
this.httpParamSerializer = httpParamSerializer;
this.base64 = base64;
}


public ConfigureInstance(basePath: string, useKerberos: boolean, username?: string, password?: string) {

this.basePath = basePath;
this.useKerberos = useKerberos;
if (this.useKerberos == false)
{
this.username = username;
this.password = password;
if (this.base64 != null)
{
var auth = this.base64.encode(this.username + ":" + this.password);
this.httpService.defaults.headers.common['Authorization'] = 'Basic ' + auth;
}
}
else
{
this.httpService.defaults.withCredentials = true;
this.httpService.defaults.useXDomain = true;
}
this.analysis = new OSIsoft.PIDevClub.PIWebApiClient. AnalysisApi(this.basePath, this.httpService, this.httpParamSerializer);
this.analysisCategory = new OSIsoft.PIDevClub.PIWebApiClient. AnalysisCategoryApi(this.basePath, this.httpService, this.httpParamSerializer);
this.analysisRule = new OSIsoft.PIDevClub.PIWebApiClient. AnalysisRuleApi(this.basePath, this.httpService, this.httpParamSerializer);
this.analysisRulePlugIn = new OSIsoft.PIDevClub.PIWebApiClient. AnalysisRulePlugInApi(this.basePath, this.httpService, this.httpParamSerializer);
this.analysisTemplate = new OSIsoft.PIDevClub.PIWebApiClient. AnalysisTemplateApi(this.basePath, this.httpService, this.httpParamSerializer);
this.assetDatabase = new OSIsoft.PIDevClub.PIWebApiClient. AssetDatabaseApi(this.basePath, this.httpService, this.httpParamSerializer);
this.assetServer = new OSIsoft.PIDevClub.PIWebApiClient. AssetServerApi(this.basePath, this.httpService, this.httpParamSerializer);
this.attribute = new OSIsoft.PIDevClub.PIWebApiClient. AttributeApi(this.basePath, this.httpService, this.httpParamSerializer);
this.attributeCategory = new OSIsoft.PIDevClub.PIWebApiClient. AttributeCategoryApi(this.basePath, this.httpService, this.httpParamSerializer);
this.attributeTemplate = new OSIsoft.PIDevClub.PIWebApiClient. AttributeTemplateApi(this.basePath, this.httpService, this.httpParamSerializer);
this.attributeTrait= new OSIsoft.PIDevClub.PIWebApiClient. AttributeTraitApi(this.basePath, this.httpService, this.httpParamSerializer);
this.batch = new OSIsoft.PIDevClub.PIWebApiClient. BatchApi(this.basePath, this.httpService, this.httpParamSerializer);
this.calculation= new OSIsoft.PIDevClub.PIWebApiClient. CalculationApi(this.basePath, this.httpService, this.httpParamSerializer);
this.configuration = new OSIsoft.PIDevClub.PIWebApiClient. ConfigurationApi(this.basePath, this.httpService, this.httpParamSerializer);
this.dataServer = new OSIsoft.PIDevClub.PIWebApiClient. DataServerApi(this.basePath, this.httpService, this.httpParamSerializer);
this.element = new OSIsoft.PIDevClub.PIWebApiClient. ElementApi(this.basePath, this.httpService, this.httpParamSerializer);
this.elementCategory= new OSIsoft.PIDevClub.PIWebApiClient. ElementCategoryApi(this.basePath, this.httpService, this.httpParamSerializer);
this.elementTemplate = new OSIsoft.PIDevClub.PIWebApiClient. ElementTemplateApi(this.basePath, this.httpService, this.httpParamSerializer);
this.enumerationSet = new OSIsoft.PIDevClub.PIWebApiClient. EnumerationSetApi(this.basePath, this.httpService, this.httpParamSerializer);
this.enumerationValue = new OSIsoft.PIDevClub.PIWebApiClient. EnumerationValueApi(this.basePath, this.httpService, this.httpParamSerializer);
this.eventFrame = new OSIsoft.PIDevClub.PIWebApiClient. EventFrameApi(this.basePath, this.httpService, this.httpParamSerializer);
this.home = new OSIsoft.PIDevClub.PIWebApiClient. HomeApi(this.basePath, this.httpService, this.httpParamSerializer);
this.point = new OSIsoft.PIDevClub.PIWebApiClient. PointApi(this.basePath, this.httpService, this.httpParamSerializer);
this.securityIdentity = new OSIsoft.PIDevClub.PIWebApiClient. SecurityIdentityApi(this.basePath, this.httpService, this.httpParamSerializer);
this.securityMapping = new OSIsoft.PIDevClub.PIWebApiClient. SecurityMappingApi(this.basePath, this.httpService, this.httpParamSerializer);
this.stream = new OSIsoft.PIDevClub.PIWebApiClient. StreamApi(this.basePath, this.httpService, this.httpParamSerializer);
this.streamSet = new OSIsoft.PIDevClub.PIWebApiClient. StreamSetApi(this.basePath, this.httpService, this.httpParamSerializer);
this.system = new OSIsoft.PIDevClub.PIWebApiClient. SystemApi(this.basePath, this.httpService, this.httpParamSerializer);
this.table = new OSIsoft.PIDevClub.PIWebApiClient. TableApi(this.basePath, this.httpService, this.httpParamSerializer);
this.tableCategory = new OSIsoft.PIDevClub.PIWebApiClient. TableCategoryApi(this.basePath, this.httpService, this.httpParamSerializer);
this.timeRule = new OSIsoft.PIDevClub.PIWebApiClient. TimeRuleApi(this.basePath, this.httpService, this.httpParamSerializer);
this.timeRulePlugIn = new OSIsoft.PIDevClub.PIWebApiClient. TimeRulePlugInApi(this.basePath, this.httpService, this.httpParamSerializer);
this.unit = new OSIsoft.PIDevClub.PIWebApiClient. UnitApi(this.basePath, this.httpService, this.httpParamSerializer);
this.unitClass = new OSIsoft.PIDevClub.PIWebApiClient. UnitClassApi(this.basePath, this.httpService, this.httpParamSerializer);
}
}

 

 

Create a new ngService.ts file under the src\ts folder with the following content:

 

/// <reference path="api.d.ts"/>

angular.module('ngPIWebApi',['base64']).factory('piwebapi', ['$http', '$httpParamSerializer', '$base64',function ($http: ng.IHttpService, $httpParamSerializer: any, $base64: any) {
    let piwebapi = new PIWebApi($http, $httpParamSerializer, $base64);
return piwebapi;
}]);

 

 

The code above creates a new AngularJS module called ngPIWebApi that required the base64 module to generate the Basic Authentication header. Therefore, let's add the base64 module to our bower components:

 

bower install angular-base64

 

The ConfigureInstance method allows you to define the PI Web API endpoint URL and set up which type of security you want to use (Anonymous, Basic or Kerberos). It will also map he piwebapi properties with the API objects. As a result, all API methods are available under the piwebapi object. If it is confused for now, don't worry. The last section of this post is about creating a web application using this library.

 

Using Gulp to concatenate and minify

 

According to Wikipedia:

 

Gulp is a build tool in JavaScript built on node streams. These streams facilitate the connection of file operations through pipelines.Gulp reads the file system and pipes the data at hand from its one single-purposed plugin to other through the .pipe() operator, doing one task at a time. The original files are not affected until all the plugins are processed. It can be configured either to modify the original files or to create new ones. This grants the ability to perform complex tasks through linking its numerous plugins. The users can also write their own plugins to define their own tasks.Unlike other task runners that run tasks by configuration, gulp requires knowledge of JavaScript and coding to define its tasks. Gulp is a build system which means apart from running tasks, it is also capable of copying files from one location to another, compiling, deploying, creating notifications, unit testing, linting etc.

 

Let's first install gulp and some other tools through npm:

 

npm install gulp gulp-concat gulp-uglify

 

Then, create a new file on the root named gulpfile.js with the following content:

 

var gulp = require('gulp');
var concat = require('gulp-concat');  
var uglify = require("gulp-uglify");


//script paths
var jsDest = 'dist';




gulp.task('default', ['concatenate', 'concatenateAndMinify', 'concatenateForPIVision', 'concatenateAndMinifyForPIVision']);


gulp.task('concatenateAndMinify', function() {  
    return gulp.src(['src/js/api/*.js','src/js/models/*.js', 'src/js/PIWebApi.js', 'src/js/ngService.js'])  
        .pipe(concat('angular-piwebapi.min.js'))
        .pipe(uglify())  
        .pipe(gulp.dest(jsDest));       
});



gulp.task('concatenate', function() {  
    return gulp.src(['src/js/api/*.js','src/js/models/*.js','src/js/PIWebApi.js', 'src/js/ngService.js'])  
        .pipe(concat('angular-piwebapi.js'))
        .pipe(gulp.dest(jsDest));       
});


gulp.task('concatenateAndMinifyForPIVision', function() {  
    return gulp.src(['src/js/api/*.js', 'src/js/models
/*.js','src/js/PIWebApi.js', 'src/js/ngServiceForPIVision.js'])  
        .pipe(concat('angular-piwebapi-kerberos.min.js'))
        .pipe(uglify())  
        .pipe(gulp.dest(jsDest));       
});




gulp.task('concatenateForPIVision', function() {  
    return gulp.src(['src/js/api/*.js', 'src/js/models/*.js','src/js/PIWebApi.js', 'src/js/ngServiceForPIVision.js']) 
        .pipe(concat('angular-piwebapi-kerberos.js'))
        .pipe(gulp.dest(jsDest));       
});

 

And change the .vscode\tasks.json content to:

 

{
    "version": "0.1.0",
    "command": "gulp",
    "isShellCommand": true,
    "args": [
        "--no-color"
    ],
    "tasks": [ {
  "taskName": "default",
  "isBuildCommand": true,
  "showOutput": "always"
  }
  ]
}

 

 

The code above will concatenate all JavaScript from src/ts  to generate the angular-piwebapi.js file to the dist folder. It also generate a minified version of this file (piwebapi.min.js). Build this project and check if both files were created on dist folder. This module requires the angular-base64.js and angular-base64.min.js JavaScript libraries. This is why I have manually copied those files to the dist folder.

 

Note: With this change, when pressing SHIFT + CTRL + B the TypeScript compiler won't be called anymore, unless you configure it on gulpfile.js to use gulp-typescript.

 

Developing a web application using AngularJS and PI Web API client libraries

 

In order to test this new client library, let's take the example from the blog post. First we need to edit the index.html:

 

    <script src="Scripts/angular.min.js"></script>
    <script src="Scripts/angular-base64.min.js"></script>
    <script src="Scripts/piwebapi.min.js"></script>
    <script src="Scripts/app.js"></script>

 

 

The original app.js is:

 

var piWebApiApp = angular.module("PiWebApiSampleApp", []);


piWebApiApp.controller("mainCtrl", function ($scope, piWebApiHttpService) {


    //declare and inicialize variables
    $scope.requestMode = true;
    $scope.getSnap = true;
    $scope.getRec = true;
    $scope.getInt = true;


    //options for the combobox on the initial page
    $scope.yesOrNoOptions = [{ "value": true, "name": "Yes" }, { "value": false, "name": "No" }];


    //update values when the default button is pressed
    $scope.defaultValues = function () {
        $scope.piServerName = "MARC-PI2014";
        $scope.piPointName = "SINUSOID";
        $scope.startTime = "*-1d";
        $scope.endTime = "*";
        $scope.interval = "1h";
        $scope.getSnap = $scope.yesOrNoOptions[0];
        $scope.getRec = $scope.yesOrNoOptions[0];
        $scope.getInt = $scope.yesOrNoOptions[0];
    }


    //get data by making http calls
    $scope.getData = function () {
        //switch div to display the results
        $scope.requestMode = false;
        //all HTTP requests are done through the  piWebApiHttpService factory object
        piWebApiHttpService.validPIServerName($scope.piServerName).then(function (response) {
            //this function will be executed in case of success
            $scope.piServerData = response.data;
            $scope.piServerExistsValue = true;
        }, function (error) {
            //this function will be executed in case of error
            $scope.piServerError = error.data;
            $scope.piServerExistsValue = false;
        });


        piWebApiHttpService.validPIPointName($scope.piServerName, $scope.piPointName).then(function (response) {
            $scope.piPointData = response.data;
            $scope.piPointExistsValue = true;
            //in case of success, we will get the webId of the PI Point which will be used by other requests
            $scope.webId = response.data.WebId;
            piWebApiHttpService.getSnapshotValue($scope.webId).then(function (response) {
                //Response of the snapshot is stored on the snapshotData
                $scope.snapshotData = response.data;
            }, function (error) {
                $scope.snapshotError = error.data;


            });
            //The following requests use the webId already stored
            piWebApiHttpService.getRecordedValues($scope.webId, $scope.startTime, $scope.endTime).then(function (response) {
                $scope.recordedData = response.data;
            }, function (error) {
                $scope.recordedError = error.data;
            });


            piWebApiHttpService.getInterpolatedValues($scope.webId, $scope.startTime, $scope.endTime, $scope.interval).then(function (response) {
                $scope.interpolatedData = response.data;
            }, function (error) {
                $scope.interpolatedError = error.data;
            });
        }, function (error) {
            $scope.piPointError = error.data;
            $scope.piPointExistsValue = false;
        });
    }
});

 

The new app.js looks like:

 

var piWebApiApp = angular.module("demo-app", ['ngPIWebApi']);


piWebApiApp.run(function (piwebapi) {
    piwebapi.ConfigureInstance("https://marc-web-sql.marc.net/piwebapi", true);
});




piWebApiApp.controller("mainCtrl", function ($scope, piwebapi) {   




    // //declare and inicialize variables
    $scope.requestMode = true;
    $scope.getSnap = true;
    $scope.getRec = true;
    $scope.getInt = true;


    // //options for the combobox on the initial page
    $scope.yesOrNoOptions = [{ "value": true, "name": "Yes" }, { "value": false, "name": "No" }];


    // //update values when the default button is pressed
    $scope.defaultValues = function () {
        $scope.piServerName = "MARC-PI2016";
        $scope.piPointName = "SINUSOID";
        $scope.startTime = "*-1d";
        $scope.endTime = "*";
        $scope.interval = "1h";
        $scope.getSnap = $scope.yesOrNoOptions[0];
        $scope.getRec = $scope.yesOrNoOptions[0];
        $scope.getInt = $scope.yesOrNoOptions[0];
    }


    // //get data by making http calls
    $scope.getData = function () {
        //switch div to display the results
        $scope.requestMode = false;
        //all HTTP requests are done through the  piWebApiHttpService factory object
        piwebapi.dataServer.getByPath('\\\\' + $scope.piServerName).then(function (response) {
            //this function will be executed in case of success
            $scope.piServerData = response.data;
            $scope.piServerExistsValue = true;
        }, function (error) {
            //this function will be executed in case of error
            $scope.piServerError = error.data;
            $scope.piServerExistsValue = false;
        });


        piwebapi.point.getByPath('\\\\' + $scope.piServerName + '\\' + $scope.piPointName, null, null).then(function (response) {
            $scope.piPointData = response.data;
            $scope.piPointExistsValue = true;
            //in case of success, we will get the webId of the PI Point which will be used by other requests
            $scope.webId = response.data.WebId;
            piwebapi.stream.getValue($scope.webId).then(function (response) {
                //Response of the snapshot is stored on the snapshotData
                $scope.snapshotData = response.data;
            }, function (error) {
                $scope.snapshotError = error.data;


            });


            piwebapi.stream.getRecorded($scope.webId, null, null, $scope.endTime, null, null, null, null, $scope.startTime).then(function (response) {
                $scope.recordedData = response.data;
            }, function (error) {
                $scope.recordedError = error.data;
            });


            piwebapi.stream.getInterpolated($scope.webId, null, $scope.endTime, null, null, $scope.interval, null, null, null, null, null, $scope.startTime).then(function (response) {
                $scope.interpolatedData = response.data;
            }, function (error) {
                $scope.interpolatedError = error.data;
            });
        }, function (error) {
            $scope.piPointError = error.data;
            $scope.piPointExistsValue = false;
        });
    }
});

 

Using the library, you don't need to write the piWebApiHttpService service. Just use this client library instead and it will work!

 

Conclusion

 

I really think that having PI Web API client libraries available for many different platforms with make our lives much easier. In this blog post I have described all the steps I had to do in order to generate the library. Nevertheless, you can just download it from this GitHub repository without having to generate it yourself. The next blog post will be about integrating this library in PI Vision! Stay tuned!

Starting in November 2016 until February 2017, we had the pleasure to have Miwa Teranishi a student at the University of New South Wales currently finishing her masters in Chemical/Biomedical Engineering.

I want to give here just the highlights of the internship.

 

The true highlight is the video she produced explaining her work:

Power Market Energy Price Forecasting Using Falkonry Pattern Recognition and the PI System

 

Training:

     As we have a history of working with the University of New South Wales, Miwa had heard of OSIsoft, but did not have a chance to use any of our tool. Thus, the first step was training. Fortunately, we have a battery of online training and she quickly mastered using our tools. Her training included various topic from:

     Creating Basic Reports with PI DataLink

     Building Basic Displays with PI ProcessBook

     Building Asset Hierarchies with PI AF

     Configuring Analytics with PI AF

Along with the VLE training available at Home Page - OSIsoft Learning

     Collecting Data using the new PI Connector for UFL

     Use Data Science for Machine Learning and Predictions Based on Your PI System Data

 

Data Collection:

     We were collecting information from public data sources, including: JEPX  and AEMO. This involved many INI files and python scripts. This work is currently being worked on so that it may be included in: GitHub - osisoft/PI-Connector-for-UFL-Samples: Documentation and supporting files to demonstrate usage of the PI Connect…

 

Falkonry:

     We were now ready to start doing some analysis of the data we had collected. There are many tools that are available to us. Our requirements were:

          - Strong integration with PI

          - No programming required

          - Strong support (as we are not experts in machine learning)

     Falkonry excelled on all those points and they gladly allowed us to use their cloud service to run analyses on this data set and supported us as we did so. This part of the story is well explained in the video above. I hope you find it interesting as we did!

 

     Falkonry also participated at our Users Conference 2017 held in San Francisco in March this year. One of the highlights of their participation was this presentation held by Ciner,

It's seems AFTime is one of my favorite blog topics.  I suppose you could consider this blog a part of a series ... that was started 5 years ago!  Here were my earlier posts:

 

My first blog - It's About Time!  (a discussion about sub-second times)

A Detailed Exploration of AFTime Precision

 

At UC SF 2017, I met lots of people and engaged in interesting discussions.  There were 2 notable questions about time.  One from a student in a lab, and the other from a work colleague.  To reword their questions:

 

  1. When I create an AFTime, sometimes it's Utc and sometimes it Local.  How can I know which is which ahead of time?
  2. I am trying to write a custom method to parse a relative time string.  Do you have any tips on the best way to do this?

 

On the surface, these 2 questions seem to only be related to AFTime in general.  But actually the same answer applies to both as it depends on the same solution: the AFTime constructor. I thought that the first question has been answered a dozen times before.  Apparently it must be answered again. 

 

There is a general rule of thumb that goes:

 

DateTimes are UTC; Strings are Local

 

Overall it's a decent rule of thumb, but not exactly correct. There are some finer details and exceptions to understand.  Before we jump right to the straight-forward solutions, let's take a brief sidetrack to have a better understanding of the objects involved.

 

Differences between AFTime and DateTime

If you ask 10 different developers to describe the difference between AFTime and DateTime, you would get 10 different replies.  For the topic being discussed, there are 3 relevant differences that I choose to discuss.

 

The first pertains to DateTime's Kind property and its notion of Utc, Local, and Unspecified.  While AFTime doesn't have a Kind property, it is quite aware of UtcTime and LocalTime but most importantly does not support any notion of Unspecified.  What this means is that if you pass anything to AFTime that falls into the Unspecified category, then AFTime must decide what to do.  It could reject it by throwing an exception, but it doesn't.  Instead AFTime makes an assumption of how unspecified time zones should be treated, yet this treatment varies depending upon the type of object being passed to AFTime.

 

In short, DateTime objects passed to AFTime will treat Unspecified as Utc, and String objects lacking time zone info will be treated as Local.

 

The second key difference between AFTime and DateTime is the MinValue for each.  While they both share the same MaxValue, AFTime.MinValue is 1/1/1970 whereas DateTime.MinValue is 1/1/1601.  This coupled with the above means there is some validation performed on the time that is input to AFTime.  That is to say at the very least the input time may be clamped to 1/1/1970 on the low end.

 

The 3rd critical difference is that while DateTime.Parse allows for a typical time string to be passed in, AFTime supports relative time formats such as "*-8h".

 

Constructors always using Utc

There are 2 AFTime constructors that will have input that is always UTC-based.

AFTime Constructor (Double)

AFTime Constructor (Int64)

 

The Double overload will accept seconds for a UTC-based DateTime.  The Int64 will accept ticks for a UTC-based DateTime.  Using either of these 2 constructors always expect the input value to be UTC-based.  It is YOUR responsibility to make sure the Double or Int64 you pass in is also UTC-based.  If you have code that isn't working correctly, look for a bug in YOUR application.

 

This code has a hard-to-find bug:

DateTime dTime = DateTime.Now;
AFTime aTime = new AFTime(dTime.Ticks);

 

Because dTime is Local but AFTime was expecting something UTC-based.  The bug is on your end and the easy fix is up to you:

 

AFTime aTime = new AFTime(dTime.ToUniversalTime().Ticks);

 

 

DateTime does allow Local

AFTime Constructor (Double)

 

The short rule of thumb "DateTimes are UTC" isn't totally correct in that you may pass a DateTime input with a Local DateTimeKind.

 

To clarify, a DateTime input to AFTime is not always treated as Utc.  If a DateTime with Kind of Local is passed in, then that Local time is honored as one would expect.  The only fuzzy area where there is a maybe is if a DateTime with Unspecified is passed in.  For those cases, the Kind is assumed to be Utc.  In other words, the Kind is simply changed to Utc and not converted to Utc.  You may think of this pseudo-code as happening when you pass a DateTime to AFTime:

 

// Unspecified is changed to Utc
// Local is converted to Utc
if (time.Kind == DateTimeKind.Unspecified)
    time = DateTime.SpecifyKind(time, DateTimeKind.Utc);
else if (time.Kind == DateTimeKind.Local)
    time = time.ToUniversalTime();

// Actually more complex than this because dates before 1900 throw an exception.
if (time < AFTime.MinValue.UtcTime)
    time = AFTime.MinValue;

// Internally time is used because its UTC-based and clamped to 1/1/1970

 

That's not the actual code that runs in the AFTime constructor, but it's appropriate example to understand some of its inner workings.  The internal value is stored in a DateTime object with Kind of Utc and clamped on the low end to 1/1/1970 (AFTime.MinValue).

 

Bottom line: if the DateTime specifies Utc or Local, it will be honored.  If the Kind is Unspecified, it is considered to be Utc.

 

String does allow UTC, time zones, and time zone offsets

AFTime Constructor (String)

 

A String input to AFTime will be treated as Local unless there is time zone specified or time zone offset information contained within the string.  If you include it, it will be honored.  If you omit it, it's treated as Local.  Just as you should expect.

 

So these time strings would be Local times as they lack any time zone offset:

"2017-04-03 16:25:00"

"2017-04-03T16:25:00"

 

Whereas these time strings would be set to UTC-based times:

"2017-04-03T16:25:00-05:00"

"2017-04-03T21:25:00Z"

 

It's nice to know that the last 2 time strings would easily be parsed by DateTime.Parse.  Whether the returned DateTime is Utc or Local, the same AFTime logic in the previous section applies.  This answers the first question I received at UC: When I create an AFTime, sometimes it's Utc and sometimes it Local.  How can I know which is which ahead of time?

 

Relative Time Format strings

See the Remarks for AFTime Constructor (String)

 

Notice that our string examples looked like typical date and time strings.  A critical difference that AFTime has over DateTime is its support of Relative Time Format strings.  Relative Time Format strings are Local based.  This should not be surprising given that concepts as Today and Yesterday are Local in context.  My Today in Houston is very different from someone else's Today in Europe.

 

For my colleague,  Thyagarajan Ramachandran also known as Thyag,  who said "I am trying to write a custom method to parse a relative time string.  Do you have any tips on the best way to do this?"  My best advice was: Don't.  There is no need to reinvent-the-wheel to parse a relative time format string yourself.  The  AFTime Constructor (String) already does this.  Save yourself lots of coding and debugging time, and instead rely on AFTime.  To think of it another way, if your goal is to code a method that parses a relative time string in a manner that is compatible with AFTime, why not just let AFTime perform all the heavy lifting because in the simplest of wisdom: whatever AFTime does is 100% compatible with AFTime!

 

I mentioned to Thyag the fact that AFTime supports multiple relative time formats such as "t+5h-30m" to have my local Today at 4:30 AM.  This initially surprised him and he admitted his method did not take this into account.  That news immediately means his custom method was not fully compatible with AFTime's parsing capabilities. Sometimes reinventing-the-wheel is not such an easy task!

 

To make your head spin even more, at the bottom of the AFTime Constructor (String) help is a quite curious example of "Sat, 01 Nov 2008 19:35:00 GMT + 2y+5d-12h+30.55s" to fully demonstrate relative time formats. Note only can you have multiple relative time intervals, it can accept floating point values (see 30.55s).

 

There are also some interesting corner cases with AFTime and input time strings that Thyagarajan Ramachandran (Thyag) discovered.

 

For example, new AFTime() returns AFTime.MinValue.

 

Whereas, new AFTime(null) or new AFTime(timestring) where timestring is null or empty returns AFTime.Now.

 

A time string of "-12" is equivalent to "*-12h".  That is to say if the initial time portion is omitted, it is assumed to be "*".  Likewise if time units are omitted, it is assumed to be "h".

 

There's also another assumption for something omitted though it's not apparent in the previous examples.  If there is no number specified, it is assumed to be zero.  This leads to the quite curious but very valid instances of:

 

new AFTime("-") is same as "*-0h"

new AFTime("+") is same as "*+0h"

 

Again, both instances are quite legal and would return AFTime.Now.

 

I would say that sufficiently answers the 2nd question I heard at UC SF 2017.  If you have anymore regarding AFTime, drop me a line.

Filter Blog

By date: By tag: