Source: osh/osh-Sensor.js

/**
 * @class
 * @classdesc
 *
 */
OSH.Sensor = Class.create({
  initialize: function (jsonix_offering) {
    this.server = null;
    this.identifier = jsonix_offering.identifier;
    this.name = jsonix_offering.name[0].value;
    this.description = jsonix_offering.description;
    this.procedure = jsonix_offering.procedure;

    var timePeriod = null;
    if (typeof jsonix_offering.phenomenonTime != 'undefined')
      timePeriod = jsonix_offering.phenomenonTime.timePeriod;

    this.timeRangeStart = (timePeriod != null && timePeriod.beginPosition.value.length > 0) ? timePeriod.beginPosition.value[0] : 'now';
    this.timeRangeEnd = (timePeriod != null && timePeriod.endPosition.value.length > 0) ? timePeriod.endPosition.value[0] : 'now';

    if (this.timeRangeEnd == 'now') {
      var d = new Date();
      d.setUTCFullYear(2030);
      this.timeRangeEnd = d.toISOString();
    }

    this.observableProperties = [];
    this.outputs = [];
    this.featuresOfInterest = [];
    this.dataConnectors = [];

    //collect the observableProperty names that can be observed on this sensor
    if (typeof jsonix_offering.observableProperty != 'undefined') {
      for (var i = 0; i < jsonix_offering.observableProperty.length; i++) {
        this.observableProperties.push(jsonix_offering.observableProperty[i]);
      }
    }

    if (typeof jsonix_offering.relatedFeature != 'undefined') {
      for (i = 0; i < jsonix_offering.relatedFeature.length; i++) {
        this.featuresOfInterest.push(jsonix_offering.relatedFeature[i].featureRelationship.target.href);
      }
    }
  },

  /**
   * describe sensor retrieves data about a sensor's observable properties and metadata
   * @instance
   * @memberof OSH.Sensor
   */
  describeSensor: function () {
    var req = this.server.url + 'sensorhub/sos?service=SOS&version=2.0&request=DescribeSensor&procedure=' + this.procedure;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4 && xhr.status == 200) {
        console.log(this.name);
        var desc = OSH.Utils.jsonix_XML2JSON(xhr.responseText);
        this.onDescribeSensor(desc);
      }
    }.bind(this);
    xhr.open('GET', req, true);
    xhr.send();
  },

  /**
   * get result template for single observable prop
   * @param observabeProp
   * @instance
   * @memberof OSH.Sensor
   */
  getResultTemplate: function (observabeProp) {
    if (this.hasObservableProperty(observabeProp)) {
      var req = this.server.url + 'sensorhub/sos?service=SOS&version=2.0&request=GetResultTemplate&offering=' + this.identifier + '&observedProperty=' + observabeProp;
      var xhr = new XMLHttpRequest();
      xhr.prop = observabeProp;
      xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
          var resultTemplate = OSH.Utils.jsonix_XML2JSON(xhr.responseText);
          
          //First get the encoding type for each field
          resEncoding = {};
          resEncoding.fields = [];
          resEncoding.type = resultTemplate.value.resultEncoding.abstractEncoding.name.localPart;
          if (resEncoding.type == 'BinaryEncoding') {
            var binaryEncodingOpts = resultTemplate.value.resultEncoding.abstractEncoding.value.member;
            for (i = 0; i < binaryEncodingOpts.length; i++) {
              //this is the variable/field that the encoding affects, the reference may be nested so there is some parsing to do
              var ref = binaryEncodingOpts[i].component.ref;
              refToks = ref.split('/');
              var dataTypeToks = binaryEncodingOpts[i].component.dataType.split('/');
              resEncoding.fields.push({name: ref, type: dataTypeToks[dataTypeToks.length - 1]});
            }
          } else if (resEncoding.type == 'TextEncoding') {
            var txtEncodingOpts = resultTemplate.value.resultEncoding.abstractEncoding.value;
            resEncoding.collapseWhiteSpaces = txtEncodingOpts.collapseWhiteSpaces;
            resEncoding.tokenSeparator = txtEncodingOpts.tokenSeparator;
            resEncoding.decimalSeparator = txtEncodingOpts.decimalSeparator;
          } else {
            //TODO: handle xml encoding if necessary
          }
          
          //Build the result structure
          var resStruct = {fields:[]};
          resStruct.findFieldByName = function(name) {
            for(var f = 0; f < this.fields.length; f++) {
              if(this.fields[f].name == name)
                return this.fields[f];
            }
          }
          var fields = resultTemplate.value.resultStructure.abstractDataComponent.value.field;
          
          //the fields read from the json object may be complex such as a nested array
          //or an array of vectors etc. buildDataField is a recursive function that will take
          //a given field and produce the correct data structure for it
          for (var i = 0; i < fields.length; i++) {
            this.buildDataFields(fields[i], resStruct);
          }

          for(var i = 0; i < resEncoding.fields.length; i++) {
            this.setFieldEncoding(resStruct, resEncoding.fields[i]);
          }
          
          //build up rest of result structure
          this.onGetResultTemplate(observabeProp, resStruct, resEncoding);
        }
      }.bind(this);
      xhr.open('GET', req, true);
      xhr.send();
    }
  },

  /**
   *
   * @param fieldStruct
   * @param fieldEncoding
   * @instance
   * @memberof OSH.Sensor
   */
  setFieldEncoding: function(fieldStruct, fieldEncoding) {
    var path = fieldEncoding.name;
    var pathToks = path.split('/');
    pathToks.shift();  //first item is always empty because the string starts with slash character
    
    currFieldStruct = fieldStruct;
    while(pathToks.length > 0) {
      for(var i = 0; i < currFieldStruct.fields.length; i++) {
        if(currFieldStruct.fields[i].name == pathToks[0]) {
          if(pathToks.length == 1) {
            currFieldStruct.fields[i].type = fieldEncoding.type;
            //console.log(fieldEncoding.type)
          } else {
            currFieldStruct = currFieldStruct.fields[i];
          }
          break;
        }
      }
      pathToks.shift();
    }
  },

  /**
   *
   * @param field
   * @param resultStruct
   * @instance
   * @memberof OSH.Sensor
   */
  buildDataFields: function(field, resultStruct) {
    var dataComp = field.abstractDataComponent;
    if(typeof dataComp != 'undefined' && dataComp != null) {

      if(dataComp.name.localPart == 'DataArray') {
        //get the element type and count of the array
        var elemType = dataComp.value.elementType;
        var elemCount = dataComp.value.elementCount;
        var countVal = 0;
        
        //Check if the count is referencing another field for its value
        //or if there is a static value provided already
        if(typeof elemCount.href != 'undefined')
          countVal = elemCount.href.split('#')[1];
        else
          countVal = elemCount.count.value;
        
        var field = {name: field.name, val:[], count: countVal, fields:[]};
        resultStruct.fields.push(field);

        //recurse
        this.buildDataFields(elemType, field);

      } else if(dataComp.name.localPart == 'Vector') {
        var field = {name: field.name, fields:[]};
        resultStruct.fields.push(field);
       
        for(var i = 0; i < dataComp.value.coordinate.length; i++) {
          this.buildDataFields(dataComp.value.coordinate[i], field);
        }
      } else {
        resultStruct.fields.push({name: field.name, val : null, fields:[]});
        if(typeof dataComp.value.id != 'undefined') {
          //This map holds references between ids and the fields that they represent. 
          //This is used to reference values in one field from another. Example: A field
          //that represents an array of values may have its count stored in another field
          resultStruct.id2FieldMap = {};
          var id = dataComp.value.id;
          resultStruct.id2FieldMap[id] = field.name;
        }
      }
    }
    else {
      resultStruct.fields.push({name: field.name, val : null, fields:[]});
    }
  },

  /**
   * get result template for all properties
   * @instance
   * @memberof OSH.Sensor
   */
  getResultTemplateAll: function () {
    for (var i = 0; i < this.observableProperties.length; i++) {
      this.getResultTemplate(this.observableProperties[i]);
    }
  },

  /**
   * creates a data connector based on specified parameters
   * @param observableProp
   * @param featureOfInterest
   * @param spatialFilter
   * @param startTime
   * @param endTime
   * @param playbackSpeed
   * @returns {*}
   * @instance
   * @memberof OSH.Sensor
   */
  createDataConnector: function (observableProp, featureOfInterest = null, spatialFilter=null, startTime=this.timeRangeStart, endTime=this.timeRangeEnd, playbackSpeed=1) {
    if (observableProp == null || typeof observableProp == 'undefined' || !this.hasObservableProperty(observableProp)) {
      console.log('Could not create data connector! Property: ' + observableProp + ' does not exist.');
      return null;
    }

    var url = this.server.getUrl();
    url = url.replace('http://', 'ws://');
    url += 'sensorhub/sos?service=SOS&version=2.0&request=GetResult&offering=' + this.identifier;
    url += '&observedProperty=' + observableProp;

    //ensure time validity (this can break request so we return null if requested time range is invalid)
    if (this.isValidTime(startTime) && this.isValidTime(endTime)) {
      url += '&temporalFilter=phenomenonTime,' + startTime + '/' + endTime;
    }
    else {
      console.log('Could not create data connector! Invalid time range');
      return null;
    }

    //check playback speed, a value < 0 will return all observations over the specified time period
    if (playbackSpeed > 0) {
      url += '&replaySpeed=' + playbackSpeed;
    }

    //check features of interest (bad feature of interest will not break request)
    if (featureOfInterest != null && hasFeatureOfInterest(featureOfInterest)) {
      url += '&featureOfInterest=' + featureOfInterest;
    }
    else {
      console.log('Warning! Feature Of Interest: ' + featureOfInterest + ' does not exist. Ignoring and returning all data');
    }

    var conn = new OSH.DataConnector.WebSocketDataConnector(url);
    this.dataConnectors.push(conn);
    return conn;
  },

  /**
   * creates a data connection for each observable property with the following params
   * @param featureOfInterest
   * @param spatialFilter
   * @param startTime
   * @param endTime
   * @param playbackSpeed
   * @returns {Array}
   * @instance
   * @memberof OSH.Sensor
   */
  createDataConnectorAll: function (featureOfInterest=null, spatialFilter=null, startTime=this.timeRangeStart, endTime=this.timeRangeEnd, playbackSpeed=1) {
    var conns = [];
    for (var i = 0; i < this.observableProperties.length; i++) {
      conns.push(this.createDataConnector(this.observableProperties[i], featureOfInterest, spatialFilter, startTime, endTime, playbackSpeed));
    }
    return conns;
  },

  /**
   * checks if observable property exists for this sensor
   * @param prop
   * @returns {boolean}
   * @instance
   * @memberof OSH.Sensor
   */
  hasObservableProperty: function (prop) {
    for (var i = 0; i < this.observableProperties.length; i++) {
      if (this.observableProperties[i] == prop)
        return true;
    }
    return false;
  },

  /**
   * checks if feature of interest exists for this sensor
   * @param foi
   * @returns {boolean}
   * @instance
   * @memberof OSH.Sensor
   */
  hasFeatureOfInterest: function (foi) {
    for (var i = 0; i < this.featuresOfInterest.length; i++) {
      if (this.featuresOfInterest[i] == foi)
        return true;
    }
    return false;
  },

  /**
   * checks if the time is within range defined for this sensor
   * @param timeStr
   * @returns {boolean}
   * @instance
   * @memberof OSH.Sensor
   */
  isValidTime: function (timeStr) {
    var d;
    if (timeStr == 'now')
      d = new Date();
    else
      d = new Date(timeStr);

    var start;
    if (this.timeRangeStart == 'now')
      start = new Date();
    else
      start = new Date(this.timeRangeStart);

    var end;
    if (this.timeRangeEnd == 'now')
      end = new Date();
    else
      end = new Date(this.timeRangeEnd);

    return (d >= start && d <= end);
  },

  /**
   * callback for checking when a sensor description has returned
   * @param data
   * @instance
   * @memberof OSH.Sensor
   */
  onDescribeSensor: function (data) {

  },

  /**
   *
   * @param obsProperty
   * @param resultStruct
   * @param resultEncoding
   * @instance
   * @memberof OSH.Sensor
   */
  onGetResultTemplate: function (obsProperty, resultStruct, resultEncoding) {

  }
});