text: /**
   * @author David Gossow - dgossow@willowgarage.com
   */
  
  /**
   * A TF Client that listens to TFs from tf2_web_republisher.
   *
   *  @constructor
   *  @param options - object with following keys:
   *   * ros - the ROSLIB.Ros connection handle
   *   * fixedFrame - the fixed frame, like /base_link
   *   * angularThres - the angular threshold for the TF republisher
   *   * transThres - the translation threshold for the TF republisher
   *   * rate - the rate for the TF republisher
   *   * goalUpdateDelay - the goal update delay for the TF republisher
   */
  ROSLIB.TFClient = function(options) {
    options = options || {};
    this.ros = options.ros;
    this.fixedFrame = options.fixedFrame || '/base_link';
    this.angularThres = options.angularThres || 2.0;
    this.transThres = options.transThres || 0.01;
    this.rate = options.rate || 10.0;
    this.goalUpdateDelay = options.goalUpdateDelay || 50;
  
    this.currentGoal = false;
    this.frameInfos = {};
    this.goalUpdateRequested = false;
  
    // Create an ActionClient
    this.actionClient = new ROSLIB.ActionClient({
      ros : this.ros,
      serverName : '/tf2_web_republisher',
      actionName : 'tf2_web_republisher/TFSubscriptionAction'
    });
  };
  
  /**
   * Process the incoming TF message and send them out using the callback
   * functions.
   *
   * @param tf - the TF message from the server
   */
  ROSLIB.TFClient.prototype.processFeedback = function(tf) {
    var that = this;
    tf.transforms.forEach(function(transform) {
      var frameID = transform.child_frame_id;
      if (frameID[0] === '/') {
        frameID = frameID.substring(1);
      }
      var info = that.frameInfos[frameID];
      if (info !== undefined) {
        info.transform = new ROSLIB.Transform({
          translation : transform.transform.translation,
          rotation : transform.transform.rotation
        });
        info.cbs.forEach(function(cb) {
          cb(info.transform);
        });
      }
    });
  };
  
  /**
   * Create and send a new goal to the tf2_web_republisher based on the current
   * list of TFs.
   */
  ROSLIB.TFClient.prototype.updateGoal = function() {
    // Anytime the list of frames changes, we will need to send a new goal.
    if (this.currentGoal) {
      this.currentGoal.cancel();
    }
  
    var goalMessage = {
      source_frames : [],
      target_frame : this.fixedFrame,
      angular_thres : this.angularThres,
      trans_thres : this.transThres,
      rate : this.rate
    };
  
    for (var frame in this.frameInfos) {
      goalMessage.source_frames.push(frame);
    }
  
    this.currentGoal = new ROSLIB.Goal({
      actionClient : this.actionClient,
      goalMessage : goalMessage
    });
    this.currentGoal.on('feedback', this.processFeedback.bind(this));
    this.currentGoal.send();
    this.goalUpdateRequested = false;
  };
  
  /**
   * Subscribe to the given TF frame.
   *
   * @param frameID - the TF frame to subscribe to
   * @param callback - function with params:
   *   * transform - the transform data
   */
  ROSLIB.TFClient.prototype.subscribe = function(frameID, callback) {
    // remove leading slash, if it's there
    if (frameID[0] === '/') {
      frameID = frameID.substring(1);
    }
    // if there is no callback registered for the given frame, create emtpy callback list
    if (this.frameInfos[frameID] === undefined) {
      this.frameInfos[frameID] = {
        cbs : []
      };
      if (!this.goalUpdateRequested) {
        setTimeout(this.updateGoal.bind(this), this.goalUpdateDelay);
        this.goalUpdateRequested = true;
      }
    } else {
      // if we already have a transform, call back immediately
      if (this.frameInfos[frameID].transform !== undefined) {
        callback(this.frameInfos[frameID].transform);
      }
    }
    this.frameInfos[frameID].cbs.push(callback);
  };
  
  /**
   * Unsubscribe from the given TF frame.
   *
   * @param frameID - the TF frame to unsubscribe from
   * @param callback - the callback function to remove
   */
  ROSLIB.TFClient.prototype.unsubscribe = function(frameID, callback) {
    // remove leading slash, if it's there
    if (frameID[0] === '/') {
      frameID = frameID.substring(1);
    }
    var info = this.frameInfos[frameID];
    if (info !== undefined) {
      var cbIndex = info.cbs.indexOf(callback);
      if (cbIndex >= 0) {
        info.cbs.splice(cbIndex, 1);
        if (info.cbs.length === 0) {
          delete this.frameInfos[frameID];
        }
        this.needUpdate = true;
      }
    }
  };
  
  