<commit_before>// FIXME works for View2D only
// FIXME should possibly show tooltip when dragging?
// FIXME should possibly be adjustable by clicking on container

goog.provide('ol.control.ZoomSlider');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.fx.Dragger');
goog.require('goog.style');
goog.require('ol.FrameState');
goog.require('ol.MapEventType');
goog.require('ol.control.Control');

/**
 * @constructor
 * @extends {ol.control.Control}
 * @param {ol.control.ZoomSliderOptions} zoomSliderOptions Zoom options.
 */
ol.control.ZoomSlider = function(zoomSliderOptions) {
  // FIXME these should be read out from a map if not given, and only then
  //       fallback to the constants if they weren't defined on the map.
  /**
   * The minimum resolution that this control that one can set with this
   * control.
   *
   * @type {number}
   * @private
   */
  this.maxResolution_ = goog.isDef(zoomSliderOptions.maxResolution) ?
      zoomSliderOptions.maxResolution :
      ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION;

  /**
   * The maximum resolution that this control that one can set with this
   * control.
   *
   * @type {number}
   * @private
   */
  this.minResolution_ = goog.isDef(zoomSliderOptions.minResolution) ?
      zoomSliderOptions.minResolution :
      ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION;

  goog.asserts.assert(
      this.minResolution_ < this.maxResolution_,
      'minResolution must be smaller than maxResolution.'
  );

  /**
   * The range of resolutions we are handling in this slider.
   *
   * @type {number}
   * @private
   */
  this.range_ = this.maxResolution_ - this.minResolution_;

  /**
   * Will hold the current resolution of the view.
   *
   * @type {number}
   * @private
   */
  this.currentResolution_;

  /**
   * The direction of the slider. Will be determined from actual display of the
   * container and defaults to ol.control.ZoomSlider.direction.VERTICAL.
   *
   * @type {ol.control.ZoomSlider.direction}
   * @private
   */
  this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;

  /**
   * @private
   * @type {Array.<?number>}
   */
  this.mapListenerKeys_ = null;

  /**
   * @private
   * @type {Array.<?number>}
   */
  this.draggerListenerKeys_ = null;

  var elem = this.createDom_();
  this.dragger_ = this.createDraggable_(elem);

  // FIXME currently only a do nothing function is bound.
  goog.events.listen(elem, [
    goog.events.EventType.TOUCHEND,
    goog.events.EventType.CLICK
  ], this.handleContainerClick_, false, this);

  goog.base(this, {
    element: elem,
    map: zoomSliderOptions.map
  });
};
goog.inherits(ol.control.ZoomSlider, ol.control.Control);

/**
 * The enum for available directions.
 *
 * @enum {number}
 */
ol.control.ZoomSlider.direction = {
  VERTICAL: 0,
  HORIZONTAL: 1
};

/**
 * The CSS class that we'll give the zoomslider container.
 *
 * @const {string}
 */
ol.control.ZoomSlider.CSS_CLASS_CONTAINER = 'ol-zoomslider';

/**
 * The CSS class that we'll give the zoomslider thumb.
 *
 * @const {string}
 */
ol.control.ZoomSlider.CSS_CLASS_THUMB =
    ol.control.ZoomSlider.CSS_CLASS_CONTAINER + '-thumb';

/**
 * The default value for minResolution_ when the control isn't instanciated with
 * an explicit value. The default value is the resolution of the standard OSM
 * tiles at zoomlevel 18.
 *
 * @const {number}
 */
ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION = 0.5971642833948135;

/**
 * The default value for maxResolution_ when the control isn't instanciated with
 * an explicit value.  The default value is the resolution of the standard OSM
 * tiles at zoomlevel 0.
 *
 * @const {number}
 */
ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION = 156543.0339;

/**
 * @inheritDoc
 */
ol.control.ZoomSlider.prototype.setMap = function(map) {
  goog.base(this, 'setMap', map);
  this.currentResolution_ = map.getView().getResolution();
  this.initMapEventListeners_();
  this.initSlider_();
  this.positionThumbForResolution_(this.currentResolution_);
};

/**
 * Initializes the event listeners for map events.
 *
 * @private
 */
ol.control.ZoomSlider.prototype.initMapEventListeners_ = function() {
  if (!goog.isNull(this.mapListenerKeys_)) {
    goog.array.forEach(this.mapListenerKeys_, goog.events.unlistenByKey);
    this.mapListenerKeys_ = null;
  }
  if (!goog.isNull(this.getMap())) {
    this.mapListenerKeys_ = [
      goog.events.listen(this.getMap(), ol.MapEventType.POSTRENDER,
          this.handleMapPostRender_, undefined, this)
    ];
  }
};

/**
 * Initializes the slider element. This will determine and set this controls
 * direction_ and also constrain the dragging of the thumb to always be within
 * the bounds of the container.
 *
 * @private
 */
ol.control.ZoomSlider.prototype.initSlider_ = function() {
  var container = this.element,
      thumb = goog.dom.getFirstElementChild(container),
      elemBounds = goog.style.getBounds(container),
      elemPaddings = goog.style.getPaddingBox(container),
      elemBorderBox = goog.style.getBorderBox(container),
      thumbBounds = goog.style.getBounds(thumb),
      thumbMargins = goog.style.getMarginBox(thumb),
      thumbBorderBox = goog.style.getBorderBox(thumb),
      w = elemBounds.width -
          elemPaddings.left - elemPaddings.right -
          thumbMargins.left - thumbMargins.right -
          thumbBorderBox.left - thumbBorderBox.right -
          thumbBounds.width,
      h = elemBounds.height -
          elemPaddings.top - elemPaddings.bottom -
          thumbMargins.top - thumbMargins.bottom -
          thumbBorderBox.top - thumbBorderBox.bottom -
          thumbBounds.height,
      limits;
  // set the direction_ of the zoomslider and the allowed bounds for dragging
  if (elemBounds.width > elemBounds.height) {
    this.direction_ = ol.control.ZoomSlider.direction.HORIZONTAL;
    limits = new goog.math.Rect(0, 0, w, 0);
  } else {
    this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;
    limits = new goog.math.Rect(0, 0, 0, h);
  }
  this.dragger_.setLimits(limits);
};

/**
 * @param {ol.MapEvent} mapEvtObj The ol.MapEvent object.
 * @private
 */
ol.control.ZoomSlider.prototype.handleMapPostRender_ = function(mapEvtObj) {
  var res = mapEvtObj.frameState.view2DState.resolution;
  if (res !== this.currentResolution_) {
    this.currentResolution_ = res;
    this.positionThumbForResolution_(res);
  }
};

/**
 * @param {goog.events.BrowserEvent} browserEvent The browser event to handle.
 * @private
 */
ol.control.ZoomSlider.prototype.handleContainerClick_ = function(browserEvent) {
  // TODO implement proper resolution calculation according to browserEvent
};

/**
 * Positions the thumb inside its container according to the given resolution.
 *
 * @param {number} res The res.
 * @private
 */
ol.control.ZoomSlider.prototype.positionThumbForResolution_ = function(res) {
  var amount = this.amountForResolution_(res),
      dragger = this.dragger_,
      thumb = goog.dom.getFirstElementChild(this.element);

  if (this.direction_ == ol.control.ZoomSlider.direction.HORIZONTAL) {
    var left = dragger.limits.left + dragger.limits.width * amount;
    goog.style.setPosition(thumb, left);
  } else {
    var top = dragger.limits.top + dragger.limits.height * amount;
    goog.style.setPosition(thumb, dragger.limits.left, top);
  }
};

/**
 * Calculates the amount the thumb has been dragged to allow for calculation
 * of the corresponding resolution.
 *
 * @param {goog.fx.DragDropEvent} e The dragdropevent.
 * @return {number} The amount the thumb has been dragged.
 * @private
 */
ol.control.ZoomSlider.prototype.amountDragged_ = function(e) {
  var draggerLimits = this.dragger_.limits,
      amount = 0;
  if (this.direction_ === ol.control.ZoomSlider.direction.HORIZONTAL) {
    amount = (e.left - draggerLimits.left) / draggerLimits.width;
  } else {
    amount = (e.top - draggerLimits.top) / draggerLimits.height;
  }
  return amount;
};

/**
 * Calculates the corresponding resolution of the thumb by the amount it has
 * been dragged from its minimum.
 *
 * @param {number} amount The amount the thumb has been dragged.
 * @return {number} a resolution between this.minResolution_ and
 *     this.maxResolution_.
 * @private
 */
ol.control.ZoomSlider.prototype.resolutionForAmount_ = function(amount) {
  var saneAmount = goog.math.clamp(amount, 0, 1);
  return this.minResolution_ + this.range_ * saneAmount;
};

/**
 * Determines an amount of dragging relative to this minimum position by the
 * given resolution.
 *
 * @param {number} res The resolution to get the amount for.
 * @return {number} an amount between 0 and 1.
 * @private
 */
ol.control.ZoomSlider.prototype.amountForResolution_ = function(res) {
  var saneRes = goog.math.clamp(res, this.minResolution_, this.maxResolution_);
  return (saneRes - this.minResolution_) / this.range_;
};

/**
 * Handles the user caused changes of the slider thumb and adjusts the
 * resolution of our map accordingly. Will be called both while dragging and
 * when dragging ends.
 *
 * @param {goog.fx.DragDropEvent} e The dragdropevent.
 * @private
 */
ol.control.ZoomSlider.prototype.handleSliderChange_ = function(e) {
  var map = this.getMap(),
      amountDragged = this.amountDragged_(e),
      res = this.resolutionForAmount_(amountDragged);
  goog.asserts.assert(res >= this.minResolution_ && res <= this.maxResolution_,
      'calculated new resolution is in allowed bounds.');
  if (res !== this.currentResolution_) {
    this.currentResolution_ = res;
    map.getView().setResolution(res);
  }
};

/**
 * Actually enable draggable behaviour for the thumb of the zoomslider and bind
 * relvant event listeners.
 *
 * @param {Element} elem The element for the slider.
 * @return {goog.fx.Dragger} The actual goog.fx.Dragger instance.
 * @private
 */
ol.control.ZoomSlider.prototype.createDraggable_ = function(elem) {
  if (!goog.isNull(this.draggerListenerKeys_)) {
    goog.array.forEach(this.draggerListenerKeys_, goog.events.unlistenByKey);
    this.draggerListenerKeys_ = null;
  }
  var dragger = new goog.fx.Dragger(elem.childNodes[0]);
  this.draggerListenerKeys_ = [
    goog.events.listen(dragger, [
      goog.fx.Dragger.EventType.DRAG,
      goog.fx.Dragger.EventType.END
    ], this.handleSliderChange_, undefined, this)
  ];
  return dragger;
};

/**
 * Setup the DOM-structure we need for the zoomslider.
 *
 * @param {Element=} opt_elem The element for the slider.
 * @return {Element} The correctly set up DOMElement.
 * @private
 */
ol.control.ZoomSlider.prototype.createDom_ = function(opt_elem) {
  var elem,
      sliderCssCls = ol.control.ZoomSlider.CSS_CLASS_CONTAINER +
          ' ol-unselectable',
      thumbCssCls = ol.control.ZoomSlider.CSS_CLASS_THUMB +
          ' ol-unselectable';

  elem = goog.dom.createDom(goog.dom.TagName.DIV, sliderCssCls,
      goog.dom.createDom(goog.dom.TagName.DIV, thumbCssCls));

  return elem;
};
<commit_msg>Correct API documentation.<commit_after>// FIXME works for View2D only
// FIXME should possibly show tooltip when dragging?
// FIXME should possibly be adjustable by clicking on container

goog.provide('ol.control.ZoomSlider');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.fx.Dragger');
goog.require('goog.style');
goog.require('ol.FrameState');
goog.require('ol.MapEventType');
goog.require('ol.control.Control');

/**
 * @constructor
 * @extends {ol.control.Control}
 * @param {ol.control.ZoomSliderOptions} zoomSliderOptions Zoom options.
 */
ol.control.ZoomSlider = function(zoomSliderOptions) {
  // FIXME these should be read out from a map if not given, and only then
  //       fallback to the constants if they weren't defined on the map.
  /**
   * The minimum resolution that one can set with this control.
   *
   * @type {number}
   * @private
   */
  this.maxResolution_ = goog.isDef(zoomSliderOptions.maxResolution) ?
      zoomSliderOptions.maxResolution :
      ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION;

  /**
   * The maximum resolution that one can set with this control.
   *
   * @type {number}
   * @private
   */
  this.minResolution_ = goog.isDef(zoomSliderOptions.minResolution) ?
      zoomSliderOptions.minResolution :
      ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION;

  goog.asserts.assert(
      this.minResolution_ < this.maxResolution_,
      'minResolution must be smaller than maxResolution.'
  );

  /**
   * The range of resolutions we are handling in this slider.
   *
   * @type {number}
   * @private
   */
  this.range_ = this.maxResolution_ - this.minResolution_;

  /**
   * Will hold the current resolution of the view.
   *
   * @type {number}
   * @private
   */
  this.currentResolution_;

  /**
   * The direction of the slider. Will be determined from actual display of the
   * container and defaults to ol.control.ZoomSlider.direction.VERTICAL.
   *
   * @type {ol.control.ZoomSlider.direction}
   * @private
   */
  this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;

  /**
   * @private
   * @type {Array.<?number>}
   */
  this.mapListenerKeys_ = null;

  /**
   * @private
   * @type {Array.<?number>}
   */
  this.draggerListenerKeys_ = null;

  var elem = this.createDom_();
  this.dragger_ = this.createDraggable_(elem);

  // FIXME currently only a do nothing function is bound.
  goog.events.listen(elem, [
    goog.events.EventType.TOUCHEND,
    goog.events.EventType.CLICK
  ], this.handleContainerClick_, false, this);

  goog.base(this, {
    element: elem,
    map: zoomSliderOptions.map
  });
};
goog.inherits(ol.control.ZoomSlider, ol.control.Control);

/**
 * The enum for available directions.
 *
 * @enum {number}
 */
ol.control.ZoomSlider.direction = {
  VERTICAL: 0,
  HORIZONTAL: 1
};

/**
 * The CSS class that we'll give the zoomslider container.
 *
 * @const {string}
 */
ol.control.ZoomSlider.CSS_CLASS_CONTAINER = 'ol-zoomslider';

/**
 * The CSS class that we'll give the zoomslider thumb.
 *
 * @const {string}
 */
ol.control.ZoomSlider.CSS_CLASS_THUMB =
    ol.control.ZoomSlider.CSS_CLASS_CONTAINER + '-thumb';

/**
 * The default value for minResolution_ when the control isn't instanciated with
 * an explicit value. The default value is the resolution of the standard OSM
 * tiles at zoomlevel 18.
 *
 * @const {number}
 */
ol.control.ZoomSlider.DEFAULT_MIN_RESOLUTION = 0.5971642833948135;

/**
 * The default value for maxResolution_ when the control isn't instanciated with
 * an explicit value.  The default value is the resolution of the standard OSM
 * tiles at zoomlevel 0.
 *
 * @const {number}
 */
ol.control.ZoomSlider.DEFAULT_MAX_RESOLUTION = 156543.0339;

/**
 * @inheritDoc
 */
ol.control.ZoomSlider.prototype.setMap = function(map) {
  goog.base(this, 'setMap', map);
  this.currentResolution_ = map.getView().getResolution();
  this.initMapEventListeners_();
  this.initSlider_();
  this.positionThumbForResolution_(this.currentResolution_);
};

/**
 * Initializes the event listeners for map events.
 *
 * @private
 */
ol.control.ZoomSlider.prototype.initMapEventListeners_ = function() {
  if (!goog.isNull(this.mapListenerKeys_)) {
    goog.array.forEach(this.mapListenerKeys_, goog.events.unlistenByKey);
    this.mapListenerKeys_ = null;
  }
  if (!goog.isNull(this.getMap())) {
    this.mapListenerKeys_ = [
      goog.events.listen(this.getMap(), ol.MapEventType.POSTRENDER,
          this.handleMapPostRender_, undefined, this)
    ];
  }
};

/**
 * Initializes the slider element. This will determine and set this controls
 * direction_ and also constrain the dragging of the thumb to always be within
 * the bounds of the container.
 *
 * @private
 */
ol.control.ZoomSlider.prototype.initSlider_ = function() {
  var container = this.element,
      thumb = goog.dom.getFirstElementChild(container),
      elemBounds = goog.style.getBounds(container),
      elemPaddings = goog.style.getPaddingBox(container),
      elemBorderBox = goog.style.getBorderBox(container),
      thumbBounds = goog.style.getBounds(thumb),
      thumbMargins = goog.style.getMarginBox(thumb),
      thumbBorderBox = goog.style.getBorderBox(thumb),
      w = elemBounds.width -
          elemPaddings.left - elemPaddings.right -
          thumbMargins.left - thumbMargins.right -
          thumbBorderBox.left - thumbBorderBox.right -
          thumbBounds.width,
      h = elemBounds.height -
          elemPaddings.top - elemPaddings.bottom -
          thumbMargins.top - thumbMargins.bottom -
          thumbBorderBox.top - thumbBorderBox.bottom -
          thumbBounds.height,
      limits;
  // set the direction_ of the zoomslider and the allowed bounds for dragging
  if (elemBounds.width > elemBounds.height) {
    this.direction_ = ol.control.ZoomSlider.direction.HORIZONTAL;
    limits = new goog.math.Rect(0, 0, w, 0);
  } else {
    this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;
    limits = new goog.math.Rect(0, 0, 0, h);
  }
  this.dragger_.setLimits(limits);
};

/**
 * @param {ol.MapEvent} mapEvtObj The ol.MapEvent object.
 * @private
 */
ol.control.ZoomSlider.prototype.handleMapPostRender_ = function(mapEvtObj) {
  var res = mapEvtObj.frameState.view2DState.resolution;
  if (res !== this.currentResolution_) {
    this.currentResolution_ = res;
    this.positionThumbForResolution_(res);
  }
};

/**
 * @param {goog.events.BrowserEvent} browserEvent The browser event to handle.
 * @private
 */
ol.control.ZoomSlider.prototype.handleContainerClick_ = function(browserEvent) {
  // TODO implement proper resolution calculation according to browserEvent
};

/**
 * Positions the thumb inside its container according to the given resolution.
 *
 * @param {number} res The res.
 * @private
 */
ol.control.ZoomSlider.prototype.positionThumbForResolution_ = function(res) {
  var amount = this.amountForResolution_(res),
      dragger = this.dragger_,
      thumb = goog.dom.getFirstElementChild(this.element);

  if (this.direction_ == ol.control.ZoomSlider.direction.HORIZONTAL) {
    var left = dragger.limits.left + dragger.limits.width * amount;
    goog.style.setPosition(thumb, left);
  } else {
    var top = dragger.limits.top + dragger.limits.height * amount;
    goog.style.setPosition(thumb, dragger.limits.left, top);
  }
};

/**
 * Calculates the amount the thumb has been dragged to allow for calculation
 * of the corresponding resolution.
 *
 * @param {goog.fx.DragDropEvent} e The dragdropevent.
 * @return {number} The amount the thumb has been dragged.
 * @private
 */
ol.control.ZoomSlider.prototype.amountDragged_ = function(e) {
  var draggerLimits = this.dragger_.limits,
      amount = 0;
  if (this.direction_ === ol.control.ZoomSlider.direction.HORIZONTAL) {
    amount = (e.left - draggerLimits.left) / draggerLimits.width;
  } else {
    amount = (e.top - draggerLimits.top) / draggerLimits.height;
  }
  return amount;
};

/**
 * Calculates the corresponding resolution of the thumb by the amount it has
 * been dragged from its minimum.
 *
 * @param {number} amount The amount the thumb has been dragged.
 * @return {number} a resolution between this.minResolution_ and
 *     this.maxResolution_.
 * @private
 */
ol.control.ZoomSlider.prototype.resolutionForAmount_ = function(amount) {
  var saneAmount = goog.math.clamp(amount, 0, 1);
  return this.minResolution_ + this.range_ * saneAmount;
};

/**
 * Determines an amount of dragging relative to this minimum position by the
 * given resolution.
 *
 * @param {number} res The resolution to get the amount for.
 * @return {number} an amount between 0 and 1.
 * @private
 */
ol.control.ZoomSlider.prototype.amountForResolution_ = function(res) {
  var saneRes = goog.math.clamp(res, this.minResolution_, this.maxResolution_);
  return (saneRes - this.minResolution_) / this.range_;
};

/**
 * Handles the user caused changes of the slider thumb and adjusts the
 * resolution of our map accordingly. Will be called both while dragging and
 * when dragging ends.
 *
 * @param {goog.fx.DragDropEvent} e The dragdropevent.
 * @private
 */
ol.control.ZoomSlider.prototype.handleSliderChange_ = function(e) {
  var map = this.getMap(),
      amountDragged = this.amountDragged_(e),
      res = this.resolutionForAmount_(amountDragged);
  goog.asserts.assert(res >= this.minResolution_ && res <= this.maxResolution_,
      'calculated new resolution is in allowed bounds.');
  if (res !== this.currentResolution_) {
    this.currentResolution_ = res;
    map.getView().setResolution(res);
  }
};

/**
 * Actually enable draggable behaviour for the thumb of the zoomslider and bind
 * relvant event listeners.
 *
 * @param {Element} elem The element for the slider.
 * @return {goog.fx.Dragger} The actual goog.fx.Dragger instance.
 * @private
 */
ol.control.ZoomSlider.prototype.createDraggable_ = function(elem) {
  if (!goog.isNull(this.draggerListenerKeys_)) {
    goog.array.forEach(this.draggerListenerKeys_, goog.events.unlistenByKey);
    this.draggerListenerKeys_ = null;
  }
  var dragger = new goog.fx.Dragger(elem.childNodes[0]);
  this.draggerListenerKeys_ = [
    goog.events.listen(dragger, [
      goog.fx.Dragger.EventType.DRAG,
      goog.fx.Dragger.EventType.END
    ], this.handleSliderChange_, undefined, this)
  ];
  return dragger;
};

/**
 * Setup the DOM-structure we need for the zoomslider.
 *
 * @param {Element=} opt_elem The element for the slider.
 * @return {Element} The correctly set up DOMElement.
 * @private
 */
ol.control.ZoomSlider.prototype.createDom_ = function(opt_elem) {
  var elem,
      sliderCssCls = ol.control.ZoomSlider.CSS_CLASS_CONTAINER +
          ' ol-unselectable',
      thumbCssCls = ol.control.ZoomSlider.CSS_CLASS_THUMB +
          ' ol-unselectable';

  elem = goog.dom.createDom(goog.dom.TagName.DIV, sliderCssCls,
      goog.dom.createDom(goog.dom.TagName.DIV, thumbCssCls));

  return elem;
};
<|endoftext|>


<commit_before>// FIXME cache expiration

goog.provide('ol.source.ServerVector');

goog.require('goog.object');
goog.require('ol.extent');
goog.require('ol.loadingstrategy');
goog.require('ol.source.FormatVector');
goog.require('ol.structs.RBush');

/**
 * @classdesc
 * A vector source in one of the supported formats, using a custom function to
 * read in the data from a remote server.
 *
 * @constructor
 * @extends {ol.source.FormatVector}
 * @param {olx.source.ServerVectorOptions} options Options.
 * @api
 */
ol.source.ServerVector = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    format: options.format,
    logo: options.logo,
    projection: options.projection
  });

  /**
   * @private
   * @type {ol.structs.RBush.<{extent: ol.Extent}>}
   */
  this.loadedExtents_ = new ol.structs.RBush();

  /**
   * @private
   * @type {function(this: ol.source.ServerVector, ol.Extent, number,
   *                 ol.proj.Projection)}
   */
  this.loader_ = options.loader;

  /**
   * @private
   * @type {function(ol.Extent, number): Array.<ol.Extent>}
   */
  this.strategy_ = goog.isDef(options.strategy) ?
      options.strategy : ol.loadingstrategy.bbox;

  /**
   * @private
   * @type {Object.<number|string, boolean>}
   */
  this.loadedFeatures_ = {};

};
goog.inherits(ol.source.ServerVector, ol.source.FormatVector);

/**
 * @inheritDoc
 */
ol.source.ServerVector.prototype.addFeaturesInternal = function(features) {
  /** @type {Array.<ol.Feature>} */
  var notLoadedFeatures = [];
  var i, ii;
  for (i = 0, ii = features.length; i < ii; ++i) {
    var feature = features[i];
    var featureId = feature.getId();
    if (!goog.isDef(featureId)) {
      notLoadedFeatures.push(feature);
    } else if (!(featureId in this.loadedFeatures_)) {
      notLoadedFeatures.push(feature);
      this.loadedFeatures_[featureId] = true;
    }
  }
  goog.base(this, 'addFeaturesInternal', notLoadedFeatures);
};

/**
 * @inheritDoc
 */
ol.source.ServerVector.prototype.clear = function() {
  goog.object.clear(this.loadedFeatures_);
  this.loadedExtents_.clear();
  goog.base(this, 'clear');
};

/**
 * @inheritDoc
 */
ol.source.ServerVector.prototype.loadFeatures =
    function(extent, resolution, projection) {
  var loadedExtents = this.loadedExtents_;
  var extentsToLoad = this.strategy_(extent, resolution);
  var i, ii;
  for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
    var extentToLoad = extentsToLoad[i];
    var alreadyLoaded = loadedExtents.forEachInExtent(extentToLoad,
        /**
         * @param {{extent: ol.Extent}} object Object.
         * @return {boolean} Contains.
         */
        function(object) {
          return ol.extent.containsExtent(object.extent, extentToLoad);
        });
    if (!alreadyLoaded) {
      this.loader_.call(this, extentToLoad, resolution, projection);
      loadedExtents.insert(extentToLoad, {extent: extentToLoad.slice()});
    }
  }
};

/**
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @return {Array.<ol.Feature>} Features.
 * @api
 */
ol.source.ServerVector.prototype.readFeatures;
<commit_msg>Pass on opt_fast to parent clear function in ol.source.ServerVector<commit_after>// FIXME cache expiration

goog.provide('ol.source.ServerVector');

goog.require('goog.object');
goog.require('ol.extent');
goog.require('ol.loadingstrategy');
goog.require('ol.source.FormatVector');
goog.require('ol.structs.RBush');

/**
 * @classdesc
 * A vector source in one of the supported formats, using a custom function to
 * read in the data from a remote server.
 *
 * @constructor
 * @extends {ol.source.FormatVector}
 * @param {olx.source.ServerVectorOptions} options Options.
 * @api
 */
ol.source.ServerVector = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    format: options.format,
    logo: options.logo,
    projection: options.projection
  });

  /**
   * @private
   * @type {ol.structs.RBush.<{extent: ol.Extent}>}
   */
  this.loadedExtents_ = new ol.structs.RBush();

  /**
   * @private
   * @type {function(this: ol.source.ServerVector, ol.Extent, number,
   *                 ol.proj.Projection)}
   */
  this.loader_ = options.loader;

  /**
   * @private
   * @type {function(ol.Extent, number): Array.<ol.Extent>}
   */
  this.strategy_ = goog.isDef(options.strategy) ?
      options.strategy : ol.loadingstrategy.bbox;

  /**
   * @private
   * @type {Object.<number|string, boolean>}
   */
  this.loadedFeatures_ = {};

};
goog.inherits(ol.source.ServerVector, ol.source.FormatVector);

/**
 * @inheritDoc
 */
ol.source.ServerVector.prototype.addFeaturesInternal = function(features) {
  /** @type {Array.<ol.Feature>} */
  var notLoadedFeatures = [];
  var i, ii;
  for (i = 0, ii = features.length; i < ii; ++i) {
    var feature = features[i];
    var featureId = feature.getId();
    if (!goog.isDef(featureId)) {
      notLoadedFeatures.push(feature);
    } else if (!(featureId in this.loadedFeatures_)) {
      notLoadedFeatures.push(feature);
      this.loadedFeatures_[featureId] = true;
    }
  }
  goog.base(this, 'addFeaturesInternal', notLoadedFeatures);
};

/**
 * @inheritDoc
 * @api stable
 */
ol.source.ServerVector.prototype.clear = function(opt_fast) {
  goog.object.clear(this.loadedFeatures_);
  this.loadedExtents_.clear();
  goog.base(this, 'clear', opt_fast);
};

/**
 * @inheritDoc
 */
ol.source.ServerVector.prototype.loadFeatures =
    function(extent, resolution, projection) {
  var loadedExtents = this.loadedExtents_;
  var extentsToLoad = this.strategy_(extent, resolution);
  var i, ii;
  for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
    var extentToLoad = extentsToLoad[i];
    var alreadyLoaded = loadedExtents.forEachInExtent(extentToLoad,
        /**
         * @param {{extent: ol.Extent}} object Object.
         * @return {boolean} Contains.
         */
        function(object) {
          return ol.extent.containsExtent(object.extent, extentToLoad);
        });
    if (!alreadyLoaded) {
      this.loader_.call(this, extentToLoad, resolution, projection);
      loadedExtents.insert(extentToLoad, {extent: extentToLoad.slice()});
    }
  }
};

/**
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @return {Array.<ol.Feature>} Features.
 * @api
 */
ol.source.ServerVector.prototype.readFeatures;
<|endoftext|>


<commit_before>/*global require, exports, console, setInterval, clearInterval*/

var logger = require('./../common/logger').logger;

// Logger
var log = logger.getLogger("RoomController");

exports.RoomController = function (spec) {
    "use strict";

    var that = {},
        // {id: array of subscribers}
        subscribers = {},
        // {id: erizoJS_id}
        publishers = {},
        // {erizoJS_id: {publishers: [ids], ka_count: count}}
        erizos = {},

        // {id: ExternalOutput}
        externalOutputs = {};

    var amqper = spec.amqper;
    var ecch = spec.ecch;

    var KEEPALIVE_INTERVAL = 5*1000;
    var TIMEOUT_LIMIT = 2;
    var MAX_ERIZOJS_RETRIES = 1;

    var eventListeners = [];

    var callbackFor = function(erizo_id) {

        return function(ok) {
            if (!erizos[erizo_id]) return;

            if (ok !== true) {
                erizos[erizo_id].ka_count ++;

                if (erizos[erizo_id].ka_count > TIMEOUT_LIMIT) {

                    log.error("Lost connection with ErizoJS", erizo_id,"will remove publishers", erizos[erizo_id].publishers);
                    for (var p in erizos[erizo_id].publishers) {
                        dispatchEvent("unpublish", erizos[erizo_id].publishers[p]);
                    }
                    ecch.deleteErizoJS(erizo_id);
                    delete erizos[erizo_id];
                }
            } else {
                erizos[erizo_id].ka_count = 0;
            }
        }
    };

    var sendKeepAlive = function() {
        for (var e in erizos) {
            amqper.callRpc("ErizoJS_" + e, "keepAlive", [], {callback: callbackFor(e)});
        }
    };

    var keepAliveLoop = setInterval(sendKeepAlive, KEEPALIVE_INTERVAL);

    var getErizoJS = function(callback) {
    	ecch.getErizoJS(function(erizo_id) {
            if (!erizos[erizo_id] && erizo_id !== 'timeout') {
                erizos[erizo_id] = {publishers: [], ka_count: 0};
            }
            callback(erizo_id);
        });
    };

    var getErizoQueue = function(publisher_id) {
        return "ErizoJS_" + publishers[publisher_id];
    };

    var dispatchEvent = function(type, event) {
        for (var event_id in eventListeners) {
            eventListeners[event_id](type, event);
        }

    };

    that.addEventListener = function(eventListener) {
        eventListeners.push(eventListener);
    };

    that.addExternalInput = function (publisher_id, url, callback) {

        if (publishers[publisher_id] === undefined) {

            log.info("Adding external input peer_id ", publisher_id);

            getErizoJS(function(erizo_id) {
                // then we call its addPublisher method.
    	        var args = [publisher_id, url];

                // Track publisher locally
                publishers[publisher_id] = erizo_id;
                subscribers[publisher_id] = [];

                amqper.callRpc(getErizoQueue(publisher_id), "addExternalInput", args, {callback: callback});

                erizos[erizo_id].publishers.push(publisher_id);

            });
        } else {
            log.info("Publisher already set for", publisher_id);
        }
    };

    that.addExternalOutput = function (publisher_id, url, callback) {
        if (publishers[publisher_id] !== undefined) {
            log.info("Adding ExternalOutput to " + publisher_id + " url " + url);

            var args = [publisher_id, url];

            amqper.callRpc(getErizoQueue(publisher_id), "addExternalOutput", args, undefined);

            // Track external outputs
            externalOutputs[url] = publisher_id;

            callback('success');
        } else {
            callback('error');
        }

    };

    that.removeExternalOutput = function (url, callback) {
        var publisher_id = externalOutputs[url];

        if (publisher_id !== undefined && publishers[publisher_id] != undefined) {
            log.info("Stopping ExternalOutput: url " + url);

            var args = [publisher_id, url];
            amqper.callRpc(getErizoQueue(publisher_id), "removeExternalOutput", args, undefined);

            // Remove track
            delete externalOutputs[url];
            callback(true);
        } else {
            callback(null, 'This stream is not being recorded');
        }
    };

    that.processSignaling = function (streamId, peerId, msg) {

        if (publishers[streamId] !== undefined) {

//            log.info("Sending signaling mess to erizoJS of st ", streamId, ' of peer ', peerId);

            var args = [streamId, peerId, msg];
            amqper.callRpc(getErizoQueue(streamId), "processSignaling", args, {});

        }
    };

    /*
     * Adds a publisher to the room. This creates a new OneToManyProcessor
     * and a new WebRtcConnection. This WebRtcConnection will be the publisher
     * of the OneToManyProcessor.
     */
    that.addPublisher = function (publisher_id, options, callback, retries) {
        if (retries === undefined)
            retries = 0;

        if (publishers[publisher_id] === undefined) {

            log.info("Adding publisher peer_id ", publisher_id, "options", options);;

            // We create a new ErizoJS with the publisher_id.
            getErizoJS(function(erizo_id) {

                if (erizo_id === 'timeout') {
                    log.error("Cannot contact ErizoAgent to add Publisher -- timeout");
                    callback('timeout-agent');
                    return;
                }
            	log.debug("ErizoJS id:", erizo_id, "assigned for publisher", publisher_id);
                // Track publisher locally
                // then we call its addPublisher method.
                var args = [publisher_id, options];
                publishers[publisher_id] = erizo_id;
                subscribers[publisher_id] = [];

                amqper.callRpc(getErizoQueue(publisher_id), "addPublisher", args, {callback: function (data){
                    if (data === 'timeout'){
                        if (retries < MAX_ERIZOJS_RETRIES){
                            log.warn("ErizoJS timed out, trying again to publish", getErizoQueue(publisher_id), "number of tries", retries);
                            publishers[publisher_id] = undefined;
                            retries++;
                            that.addPublisher(publisher_id, options,callback, retries);
                            return;
                        }
                        log.error("Can not contact ErizoJS", getErizoQueue(publisher_id) , " failed add Publisher -- timeout");
                        var index = erizos[publishers[publisher_id]].publishers.indexOf(publisher_id);
                        erizos[publishers[publisher_id]].publishers.splice(index, 1);
                        callback('timeout-erizojs');
                        return;
                    }else{
                        callback(data);
                    }
                }});

                erizos[erizo_id].publishers.push(publisher_id);
            });

        } else {
            log.warn("Publisher already set for", publisher_id);
        }
    };

    /*
     * Adds a subscriber to the room. This creates a new WebRtcConnection.
     * This WebRtcConnection will be added to the subscribers list of the
     * OneToManyProcessor.
     */
    that.addSubscriber = function (subscriber_id, publisher_id, options, callback, retries) {
        if (subscriber_id === null){
          callback("Error: null subscriber_id");
          return;
        }
        if (retries === undefined)
            retries = 0;

        if (publishers[publisher_id] !== undefined && subscribers[publisher_id].indexOf(subscriber_id) === -1) {
            log.info("Adding subscriber ", subscriber_id, ' to publisher ', publisher_id, "options", options);

            if (options.audio === undefined) options.audio = true;
            if (options.video === undefined) options.video = true;

            var args = [subscriber_id, publisher_id, options];
            //TODO: Potential problem here if erizoJS is not reachable
            amqper.callRpc(getErizoQueue(publisher_id, undefined), "addSubscriber", args, {callback: function (data){
                if (data === 'timeout'){
                    if (retries < MAX_ERIZOJS_RETRIES){
                        retries++;
                        log.warn("ErizoJS timed out, trying again to subscribe to ", publisher_id, "in", getErizoQueue(publisher_id), "from", subscriber_id, "number of tries", retries);
                        that.addSubscriber(subscriber_id, publisher_id, options, callback, retries);
                        return;
                    }
                    log.error("Can not contact to ErizoJS", getErizoQueue(publisher_id) , " failed add Subscriber", subscriber_id," -- timeout");
                    callback('timeout');
                    return;
                }else if (data === 'initializing'){
                    subscribers[publisher_id].push(subscriber_id);
                }
                callback(data);
            }});
        }
    };

    /*
     * Removes a publisher from the room. This also deletes the associated OneToManyProcessor.
     */
    that.removePublisher = function (publisher_id) {

        if (subscribers[publisher_id] !== undefined && publishers[publisher_id]!== undefined) {
            log.info("Removing publisher", publisher_id);

            var args = [publisher_id];
            amqper.callRpc(getErizoQueue(publisher_id), "removePublisher", args, undefined);

            // Remove tracks
            var index = erizos[publishers[publisher_id]].publishers.indexOf(publisher_id);
            erizos[publishers[publisher_id]].publishers.splice(index, 1);

            delete subscribers[publisher_id];
            delete publishers[publisher_id];
            log.info('Removed muxer', publisher_id, ' muxers left ', Object.keys(publishers).length );
        }
    };

    /*
     * Removes a subscriber from the room. This also removes it from the associated OneToManyProcessor.
     */
    that.removeSubscriber = function (subscriber_id, publisher_id) {

        var index = subscribers[publisher_id].indexOf(subscriber_id);
        if (index !== -1) {
            log.info('Removing subscriber ', subscriber_id, 'to muxer ', publisher_id);

            var args = [subscriber_id, publisher_id];
            amqper.callRpc(getErizoQueue(publisher_id), "removeSubscriber", args, undefined);

            subscribers[publisher_id].splice(index, 1);
        }
    };

    /*
     * Removes all the subscribers related with a client.
     */
    that.removeSubscriptions = function (subscriber_id) {

        var publisher_id, index;

        log.info('Removing subscriptions of ', subscriber_id);

        for (publisher_id in subscribers) {
            if (subscribers.hasOwnProperty(publisher_id)) {
                index = subscribers[publisher_id].indexOf(subscriber_id);
                if (index !== -1) {
                    log.debug('Removing subscriber ', subscriber_id, 'from muxer ', publisher_id);

                    var args = [subscriber_id, publisher_id];
            		amqper.callRpc(getErizoQueue(publisher_id), "removeSubscriber", args, undefined);

            		// Remove tracks
                    subscribers[publisher_id].splice(index, 1);
                }
            }
        }
    };

    return that;
};
<commit_msg>Fixed potential crash when erizoJS was removed because it did not respond<commit_after>/*global require, exports, console, setInterval, clearInterval*/

var logger = require('./../common/logger').logger;

// Logger
var log = logger.getLogger("RoomController");

exports.RoomController = function (spec) {
    "use strict";

    var that = {},
        // {id: array of subscribers}
        subscribers = {},
        // {id: erizoJS_id}
        publishers = {},
        // {erizoJS_id: {publishers: [ids], ka_count: count}}
        erizos = {},

        // {id: ExternalOutput}
        externalOutputs = {};

    var amqper = spec.amqper;
    var ecch = spec.ecch;

    var KEEPALIVE_INTERVAL = 5*1000;
    var TIMEOUT_LIMIT = 2;
    var MAX_ERIZOJS_RETRIES = 1;

    var eventListeners = [];

    var callbackFor = function(erizo_id) {

        return function(ok) {
            if (!erizos[erizo_id]) return;

            if (ok !== true) {
                erizos[erizo_id].ka_count ++;

                if (erizos[erizo_id].ka_count > TIMEOUT_LIMIT) {
                    if (erizos[erizo_id].publishers > 0){
                        log.error("Lost connection with ErizoJS", erizo_id,"will remove publishers", erizos[erizo_id].publishers);
                        for (var p in erizos[erizo_id].publishers) {
                            dispatchEvent("unpublish", erizos[erizo_id].publishers[p]);
                        }
                    } else {
                        log.warn("ErizoJS", erizo_id, "is empty and does not respond, removing");
                    }
                    ecch.deleteErizoJS(erizo_id);
                    delete erizos[erizo_id];
                }
            } else {
                erizos[erizo_id].ka_count = 0;
            }
        }
    };

    var sendKeepAlive = function() {
        for (var e in erizos) {
            amqper.callRpc("ErizoJS_" + e, "keepAlive", [], {callback: callbackFor(e)});
        }
    };

    var keepAliveLoop = setInterval(sendKeepAlive, KEEPALIVE_INTERVAL);

    var getErizoJS = function(callback) {
    	ecch.getErizoJS(function(erizo_id) {
            if (!erizos[erizo_id] && erizo_id !== 'timeout') {
                erizos[erizo_id] = {publishers: [], ka_count: 0};
            }
            callback(erizo_id);
        });
    };

    var getErizoQueue = function(publisher_id) {
        return "ErizoJS_" + publishers[publisher_id];
    };

    var dispatchEvent = function(type, event) {
        for (var event_id in eventListeners) {
            eventListeners[event_id](type, event);
        }

    };

    that.addEventListener = function(eventListener) {
        eventListeners.push(eventListener);
    };

    that.addExternalInput = function (publisher_id, url, callback) {

        if (publishers[publisher_id] === undefined) {

            log.info("Adding external input peer_id ", publisher_id);

            getErizoJS(function(erizo_id) {
                // then we call its addPublisher method.
    	        var args = [publisher_id, url];

                // Track publisher locally
                publishers[publisher_id] = erizo_id;
                subscribers[publisher_id] = [];

                amqper.callRpc(getErizoQueue(publisher_id), "addExternalInput", args, {callback: callback});

                erizos[erizo_id].publishers.push(publisher_id);

            });
        } else {
            log.info("Publisher already set for", publisher_id);
        }
    };

    that.addExternalOutput = function (publisher_id, url, callback) {
        if (publishers[publisher_id] !== undefined) {
            log.info("Adding ExternalOutput to " + publisher_id + " url " + url);

            var args = [publisher_id, url];

            amqper.callRpc(getErizoQueue(publisher_id), "addExternalOutput", args, undefined);

            // Track external outputs
            externalOutputs[url] = publisher_id;

            callback('success');
        } else {
            callback('error');
        }

    };

    that.removeExternalOutput = function (url, callback) {
        var publisher_id = externalOutputs[url];

        if (publisher_id !== undefined && publishers[publisher_id] != undefined) {
            log.info("Stopping ExternalOutput: url " + url);

            var args = [publisher_id, url];
            amqper.callRpc(getErizoQueue(publisher_id), "removeExternalOutput", args, undefined);

            // Remove track
            delete externalOutputs[url];
            callback(true);
        } else {
            callback(null, 'This stream is not being recorded');
        }
    };

    that.processSignaling = function (streamId, peerId, msg) {

        if (publishers[streamId] !== undefined) {

//            log.info("Sending signaling mess to erizoJS of st ", streamId, ' of peer ', peerId);

            var args = [streamId, peerId, msg];
            amqper.callRpc(getErizoQueue(streamId), "processSignaling", args, {});

        }
    };

    /*
     * Adds a publisher to the room. This creates a new OneToManyProcessor
     * and a new WebRtcConnection. This WebRtcConnection will be the publisher
     * of the OneToManyProcessor.
     */
    that.addPublisher = function (publisher_id, options, callback, retries) {
        if (retries === undefined)
            retries = 0;

        if (publishers[publisher_id] === undefined) {

            log.info("Adding publisher peer_id ", publisher_id, "options", options);;

            // We create a new ErizoJS with the publisher_id.
            getErizoJS(function(erizo_id) {

                if (erizo_id === 'timeout') {
                    log.error("Cannot contact ErizoAgent to add Publisher -- timeout");
                    callback('timeout-agent');
                    return;
                }
            	log.debug("ErizoJS id:", erizo_id, "assigned for publisher", publisher_id);
                // Track publisher locally
                // then we call its addPublisher method.
                var args = [publisher_id, options];
                publishers[publisher_id] = erizo_id;
                subscribers[publisher_id] = [];

                amqper.callRpc(getErizoQueue(publisher_id), "addPublisher", args, {callback: function (data){
                    if (data === 'timeout'){
                        if (retries < MAX_ERIZOJS_RETRIES){
                            log.warn("ErizoJS timed out, trying again to publish", getErizoQueue(publisher_id), "number of tries", retries);
                            publishers[publisher_id] = undefined;
                            retries++;
                            that.addPublisher(publisher_id, options,callback, retries);
                            return;
                        }
                        log.error("Can not contact ErizoJS", getErizoQueue(publisher_id) , " failed add Publisher -- timeout");
                        var index = erizos[publishers[publisher_id]].publishers.indexOf(publisher_id);
                        erizos[publishers[publisher_id]].publishers.splice(index, 1);
                        callback('timeout-erizojs');
                        return;
                    }else{
                        callback(data);
                    }
                }});

                erizos[erizo_id].publishers.push(publisher_id);
            });

        } else {
            log.warn("Publisher already set for", publisher_id);
        }
    };

    /*
     * Adds a subscriber to the room. This creates a new WebRtcConnection.
     * This WebRtcConnection will be added to the subscribers list of the
     * OneToManyProcessor.
     */
    that.addSubscriber = function (subscriber_id, publisher_id, options, callback, retries) {
        if (subscriber_id === null){
          callback("Error: null subscriber_id");
          return;
        }
        if (retries === undefined)
            retries = 0;

        if (publishers[publisher_id] !== undefined && subscribers[publisher_id].indexOf(subscriber_id) === -1) {
            log.info("Adding subscriber ", subscriber_id, ' to publisher ', publisher_id, "options", options);

            if (options.audio === undefined) options.audio = true;
            if (options.video === undefined) options.video = true;

            var args = [subscriber_id, publisher_id, options];
            //TODO: Potential problem here if erizoJS is not reachable
            amqper.callRpc(getErizoQueue(publisher_id, undefined), "addSubscriber", args, {callback: function (data){
                if (data === 'timeout'){
                    if (retries < MAX_ERIZOJS_RETRIES){
                        retries++;
                        log.warn("ErizoJS timed out, trying again to subscribe to ", publisher_id, "in", getErizoQueue(publisher_id), "from", subscriber_id, "number of tries", retries);
                        that.addSubscriber(subscriber_id, publisher_id, options, callback, retries);
                        return;
                    }
                    log.error("Can not contact to ErizoJS", getErizoQueue(publisher_id) , " failed add Subscriber", subscriber_id," -- timeout");
                    callback('timeout');
                    return;
                }else if (data === 'initializing'){
                    subscribers[publisher_id].push(subscriber_id);
                }
                callback(data);
            }});
        }
    };

    /*
     * Removes a publisher from the room. This also deletes the associated OneToManyProcessor.
     */
    that.removePublisher = function (publisher_id) {

        if (subscribers[publisher_id] !== undefined && publishers[publisher_id]!== undefined) {
            log.info("Removing publisher", publisher_id);

            var args = [publisher_id];
            amqper.callRpc(getErizoQueue(publisher_id), "removePublisher", args, undefined);

            if (erizos[publishers[publisher_id]]!== undefined){
                var index = erizos[publishers[publisher_id]].publishers.indexOf(publisher_id);
                erizos[publishers[publisher_id]].publishers.splice(index, 1);
            }else{
                L.Logger.warn("Trying to update erizoJS corresponding to ", publisher_id, "but was already removed");
            }

            delete subscribers[publisher_id];
            delete publishers[publisher_id];
            log.info('Removed muxer', publisher_id, ' muxers left ', Object.keys(publishers).length );
        }
    };

    /*
     * Removes a subscriber from the room. This also removes it from the associated OneToManyProcessor.
     */
    that.removeSubscriber = function (subscriber_id, publisher_id) {

        var index = subscribers[publisher_id].indexOf(subscriber_id);
        if (index !== -1) {
            log.info('Removing subscriber ', subscriber_id, 'to muxer ', publisher_id);

            var args = [subscriber_id, publisher_id];
            amqper.callRpc(getErizoQueue(publisher_id), "removeSubscriber", args, undefined);

            subscribers[publisher_id].splice(index, 1);
        }
    };

    /*
     * Removes all the subscribers related with a client.
     */
    that.removeSubscriptions = function (subscriber_id) {

        var publisher_id, index;

        log.info('Removing subscriptions of ', subscriber_id);

        for (publisher_id in subscribers) {
            if (subscribers.hasOwnProperty(publisher_id)) {
                index = subscribers[publisher_id].indexOf(subscriber_id);
                if (index !== -1) {
                    log.debug('Removing subscriber ', subscriber_id, 'from muxer ', publisher_id);

                    var args = [subscriber_id, publisher_id];
            		amqper.callRpc(getErizoQueue(publisher_id), "removeSubscriber", args, undefined);

            		// Remove tracks
                    subscribers[publisher_id].splice(index, 1);
                }
            }
        }
    };

    return that;
};
<|endoftext|>


<commit_before>const ProfileDialog = (title = __('Edit Profile'), action={}) => {
	const fields = [
		{
			fieldname: 'company_email',
			label: __('Email'),
			fieldtype: 'Read Only'
		},
		{
			fieldtype: 'Link',
			fieldname: 'company',
			label: __('Company'),
			options: 'Company',
			onchange: () => {
				const value = dialog.get_value('company');
				if (value) {
					frappe.db.get_doc('Company', value)
						.then(company => {
							console.log(company.company_logo);
							dialog.set_values({
								company_logo: company.company_logo,
								company_description: company.company_description
							});
						});
				}
			}
		},
		{
			label: __('About your company'),
			fieldname: 'company_description',
			fieldtype: 'Text'
		}
	];

	let dialog = new frappe.ui.Dialog({
		title: title,
		fields: fields,
		primary_action_label: action.label || __('Update'),
		primary_action: () => {
			const form_values = dialog.get_values();
			let values_filled = true;

			// TODO: Say "we notice that the company description and logo isn't set. Please set them in master."
			// Only then allow to register

			const mandatory_fields = ['company', 'company_description'];
			mandatory_fields.forEach(field => {
				const value = form_values[field];
				if (!value) {
					dialog.set_df_property(field, 'reqd', 1);
					values_filled = false;
				}
			});
			if (!values_filled) return;

			action.on_submit(form_values);
		}
	});

	// Post create
	const default_company = frappe.defaults.get_default('company');
	dialog.set_value('company', default_company);
	dialog.set_value('email', frappe.session.user);

	return dialog;
}

export {
	ProfileDialog
}
<commit_msg>fix: email in profile dialog<commit_after>const ProfileDialog = (title = __('Edit Profile'), action={}) => {
	const fields = [
		{
			fieldtype: 'Link',
			fieldname: 'company',
			label: __('Company'),
			options: 'Company'
		},
		{
			fieldtype: 'Read Only',
			fieldname: 'email',
			label: __('Email')
		},
		{
			label: __('About your company'),
			fieldname: 'company_description',
			fieldtype: 'Text'
		}
	];

	let dialog = new frappe.ui.Dialog({
		title: title,
		fields: fields,
		primary_action_label: action.label || __('Update'),
		primary_action: () => {
			const form_values = dialog.get_values();
			let values_filled = true;

			// TODO: Say "we notice that the company description and logo isn't set. Please set them in master."
			// Only then allow to register

			const mandatory_fields = ['company', 'company_description'];
			mandatory_fields.forEach(field => {
				const value = form_values[field];
				if (!value) {
					dialog.set_df_property(field, 'reqd', 1);
					values_filled = false;
				}
			});
			if (!values_filled) return;

			action.on_submit(form_values);
		}
	});

	// Post create
	const default_company = frappe.defaults.get_default('company');
	dialog.set_value('company', default_company);
	dialog.set_value('email', frappe.session.user);

	return dialog;
}

export {
	ProfileDialog
}
<|endoftext|>


<commit_before>var resultsArray = {
  'Martine':12,
  'Louise':25,
  'test':75
}

function circularDiagramm(resultsArray){
  if(typeof(resultsArray) == "object"){

    var coordonneesCenterDiagramm = [250, 250], rayonDiagramm = 250;

    var container = document.getElementById('DiagrammeCirculaire');

    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('width', 500);
    svg.setAttribute('height', 500);

    svg = generateSVGBody(resultsArray, svg, coordonneesCenterDiagramm, rayonDiagramm);

    container.appendChild(svg);
  }
  else{
    console.error("Les résultats ne sont pas sous forme d'objet");
  }
}

function generateSVGBody(resultsArray, svg, coordonneesCenterDiagramm, rayonDiagramm){
  var totalResults = 0;

  for(variableAvancement in resultsArray){
    totalResults += resultsArray[variableAvancement];
  }

  coordonneesPoints = calculateCoordonnesPoints(resultsArray, coordonneesCenterDiagramm, rayonDiagramm, totalResults);

  svg = createPaths(svg, coordonneesPoints, coordonneesCenterDiagramm, resultsArray, totalResults);

  return svg;
}

function calculateCoordonnesPoints(resultsArray, coordonneesCenterDiagramm, rayonDiagramm, totalResults){
  var pourcentAvancement = 0,coordonneesPoints = [];

  for(variableAvancement in resultsArray){
    coordonneesPoints[variableAvancement] = [];
    coordonneesPoints[variableAvancement][0] = Math.cos((pourcentAvancement * 3.6) * (Math.PI / 180)) * rayonDiagramm + coordonneesCenterDiagramm[0];
    coordonneesPoints[variableAvancement][1] = Math.abs(Math.sin((pourcentAvancement * 3.6) * (Math.PI / 180)) * rayonDiagramm - coordonneesCenterDiagramm[1]);

    pourcentAvancement += resultsArray[variableAvancement] * 100 / totalResults;
  }

  return coordonneesPoints;
}

function createPaths(svg, coordonneesPoints, coordonneesCenterDiagramm, resultsArray, totalResults){

  var center = "M" + coordonneesCenterDiagramm[0] + "," + coordonneesCenterDiagramm[1];
  var firstPoint, lastPoint, pathString = "";
  var taille = Object.keys(coordonneesPoints).length - 1;
  var keys = Object.keys(resultsArray);

  for(variable in coordonneesPoints){
    var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    var arc = "A" + coordonneesCenterDiagramm[0] + "," + coordonneesCenterDiagramm[1];

    var firstPoint = "L" + coordonneesPoints[variable][0] + "," + coordonneesPoints[variable][1];

    if(resultsArray[variable] > (totalResults / 2)){
      arc += " 0 1,0 "
    }

    else{
      arc += " 0 0,0 ";
    }

    if(keys.indexOf(variable) != taille){
      lastPoint = coordonneesPoints[keys[keys.indexOf(variable)+1]][0] + "," + coordonneesPoints[keys[keys.indexOf(variable)+1]][1];
    }
    else{
      lastPoint = coordonneesPoints[keys[0]][0] + "," + coordonneesPoints[keys[0]][1];
    }

    pathString = center + firstPoint + arc + lastPoint + " z";

    path.setAttribute('d', pathString);
    path.setAttribute('stroke', '#ffffff');
    path.setAttribute('stroke-width', 1);

    svg.appendChild(path);
  }
  return svg;
}
<commit_msg>Added Commentaries<commit_after>/*
* Fonction à appeler qui générera le diagramme circulaire de taille de 500 par 500
* La fonction nécessite au préalable une balise dite 'conteneur' avec
* l'id "DiagrammeCirculaire" qui contiendra le diagramme
*
* @param resultsArray{ Object }: Objet à deux dimensions qui contient l'intitulé
*             du résultat ainsi que son résultat
*             exemple: { 'argument1':55, 'argument2':612, 'argument3':15}
*/
function circularDiagramm(resultsArray){

  //On vérifie la validité de l'argument de resultsArray
  if(typeof(resultsArray) == "object"){

    //Initialisation des données utilisés à l'avenir
    var coordonneesCenterDiagramm = [250, 250], rayonDiagramm = 250;

    //Initialisation du conteneur du diagramme
    var container = document.getElementById('DiagrammeCirculaire');

    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('width', 500);
    svg.setAttribute('height', 500);

    svg = generateSVGBody(resultsArray, svg, coordonneesCenterDiagramm, rayonDiagramm);

    container.appendChild(svg);
  }
  else{
    console.error("Les résultats ne sont pas sous forme d'objet");
  }
}

/*
* Fonction qui générera le contenu de la balise "svg" qui est le diagramme
*
* @param resultsArray{ Object }: Contient les résultats du diagramme
* @param svg{ Object }: Balise HTML qui contiendra le diagramme
* @param coordonneesCenterDiagramm{ Array }: Contient les coordonnées du centre du diagramme
* @param rayonDiagramm{ Number }: Contient le rayon du diagramme
*
* @return svg{ Object }: Balise HTML qui contiendra le diagramme
*/
function generateSVGBody(resultsArray, svg, coordonneesCenterDiagramm, rayonDiagramm){
  var totalResults = 0;

  //Calcul du total des résultats
  for(variableAvancement in resultsArray){
    totalResults += resultsArray[variableAvancement];
  }

  coordonneesPoints = calculateCoordonnesPoints(resultsArray, coordonneesCenterDiagramm, rayonDiagramm, totalResults);

  svg = createPaths(svg, coordonneesPoints, coordonneesCenterDiagramm, resultsArray, totalResults);

  return svg;
}

/*
* Fonction qui calcule les coordonnées de tout les points utilisé à l'avenir
*
* @param resultsArray{ Object }: Contient les résultats du diagramme
* @param coordonneesCenterDiagramm{ Array }: Contient les coordonnées du centre du diagramme
* @param rayonDiagramm{ Number }: Contient le rayon du diagramme
* @param totalResults{ Number }: Contient le total du nombre de résultats
*
* @return coordonneesPoints{ Array }: Contient les coordonnées de tous les points utilisés à l'avenir
*/
function calculateCoordonnesPoints(resultsArray, coordonneesCenterDiagramm, rayonDiagramm, totalResults){
  var pourcentAvancement = 0,coordonneesPoints = [];

  //Calcul des coordonnées de chacun des points
  for(variableAvancement in resultsArray){

    //Initialisation du tableau des coordonnées
    coordonneesPoints[variableAvancement] = [];
    coordonneesPoints[variableAvancement][0] = Math.cos((pourcentAvancement * 3.6) * (Math.PI / 180)) * rayonDiagramm + coordonneesCenterDiagramm[0];
    coordonneesPoints[variableAvancement][1] = Math.abs(Math.sin((pourcentAvancement * 3.6) * (Math.PI / 180)) * rayonDiagramm - coordonneesCenterDiagramm[1]);

    pourcentAvancement += resultsArray[variableAvancement] * 100 / totalResults;
  }

  return coordonneesPoints;
}

/*
* Fonction qui génère les balises "paths", qui forment les parties du diagramme,
*   et qui sont ensuite ajouter à la balise "svg"
*
* @param svg{ Object }: Balise HTML qui contiendra le diagramme

* @param coordonneesCenterDiagramm{ Array }: Contient les coordonnées du centre du diagramme
* @param resultsArray{ Object }: Contient les résultats du diagramme
* @param totalResults{ Number }: Contient le total du nombre de résultats
*
* @return svg{ Object }: Balise HTML qui contiendra le diagramme
*/
function createPaths(svg, coordonneesPoints, coordonneesCenterDiagramm, resultsArray, totalResults){

  //Initialisation des constantes
  var center = "M" + coordonneesCenterDiagramm[0] + "," + coordonneesCenterDiagramm[1];
  var arc = "A" + coordonneesCenterDiagramm[0] + "," + coordonneesCenterDiagramm[1] + " 0 ";
  var firstPoint, lastPoint, pathString, arcSpecification;
  var taille = Object.keys(coordonneesPoints).length - 1;
  var keys = Object.keys(resultsArray);

  //Pour chacun des points, on créer le "path"
  for(variable in coordonneesPoints){

    //Initialise la balise "path"
    var path = document.createElementNS("http://www.w3.org/2000/svg", "path");

    //Création du premier point
    var firstPoint = "L" + coordonneesPoints[variable][0] + "," + coordonneesPoints[variable][1];

    //Modification de l'arc en fonction du résultat supérieur à 50% ou non
    if(resultsArray[variable] > (totalResults / 2)){
      arcSpecification = "1,0 ";
    }

    else{
      arcSpecification = "0,0 ";
    }

    //Création du dernier point
    if(keys.indexOf(variable) != taille){
      lastPoint = coordonneesPoints[keys[keys.indexOf(variable)+1]][0] + "," + coordonneesPoints[keys[keys.indexOf(variable)+1]][1];
    }
    else{
      lastPoint = coordonneesPoints[keys[0]][0] + "," + coordonneesPoints[keys[0]][1];
    }

    pathString = center + firstPoint + arc + arcSpecification + lastPoint + " z";

    path.setAttribute('d', pathString);
    path.setAttribute('stroke', '#ffffff');
    path.setAttribute('stroke-width', 1);

    svg.appendChild(path);
  }
  return svg;
}
<|endoftext|>


<commit_before>var dialogPolyfill = (function() {

  var supportCustomEvent = window.CustomEvent;
  if (!supportCustomEvent || typeof supportCustomEvent == 'object') {
    supportCustomEvent = function CustomEvent(event, x) {
      x = x || {};
      var ev = document.createEvent('CustomEvent');
      ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
      return ev;
    };
    supportCustomEvent.prototype = window.Event.prototype;
  }

  /**
   * Finds the nearest <dialog> from the passed element.
   *
   * @param {Element} el to search from
   * @return {HTMLDialogElement} dialog found
   */
  function findNearestDialog(el) {
    while (el) {
      if (el.nodeName == 'DIALOG') {
        return /** @type {HTMLDialogElement} */ (el);
      }
      el = el.parentElement;
    }
    return null;
  }

  var dialogPolyfill = {};

  dialogPolyfill.reposition = function(element) {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
    element.style.top = Math.max(0, topValue) + 'px';
    element.dialogPolyfillInfo.isTopOverridden = true;
  };

  dialogPolyfill.inNodeList = function(nodeList, node) {
    for (var i = 0; i < nodeList.length; ++i) {
      if (nodeList[i] == node)
        return true;
    }
    return false;
  };

  dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
    for (var i = 0; i < document.styleSheets.length; ++i) {
      var styleSheet = document.styleSheets[i];
      var cssRules = null;
      // Some browsers throw on cssRules.
      try {
        cssRules = styleSheet.cssRules;
      } catch (e) {}
      if (!cssRules)
        continue;
      for (var j = 0; j < cssRules.length; ++j) {
        var rule = cssRules[j];
        var selectedNodes = null;
        // Ignore errors on invalid selector texts.
        try {
          selectedNodes = document.querySelectorAll(rule.selectorText);
        } catch(e) {}
        if (!selectedNodes || !dialogPolyfill.inNodeList(selectedNodes, element))
          continue;
        var cssTop = rule.style.getPropertyValue('top');
        var cssBottom = rule.style.getPropertyValue('bottom');
        if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
          return true;
      }
    }
    return false;
  };

  dialogPolyfill.needsCentering = function(dialog) {
    var computedStyle = window.getComputedStyle(dialog);
    if (computedStyle.position != 'absolute') {
      return false;
    }

    // We must determine whether the top/bottom specified value is non-auto.  In
    // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
    // Firefox returns the used value. So we do this crazy thing instead: check
    // the inline style and then go through CSS rules.
    if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
        (dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
      return false;
    return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
  };

  dialogPolyfill.showDialog = function(isModal) {
    if (this.open) {
      throw 'InvalidStateError: showDialog called on open dialog';
    }
    this.open = true;  // TODO: should be a getter mapped to attribute
    this.setAttribute('open', 'open');

    if (isModal) {
      // Find element with `autofocus` attribute or first form control.
      var target = this.querySelector('[autofocus]:not([disabled])');
      if (!target) {
        var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
        var query = opts.map(function(el) {
          return el + ':not([disabled])';
        }).join(', ');
        target = this.querySelector(query);
      }
      target && target.focus();
    }

    if (dialogPolyfill.needsCentering(this))
      dialogPolyfill.reposition(this);
    if (isModal) {
      this.dialogPolyfillInfo.modal = true;
      dialogPolyfill.dm.pushDialog(this);
    }
  };

  dialogPolyfill.close = function(retval) {
    if (!this.open && !window.HTMLDialogElement) {
      // Native implementations will set .open to false, so ignore this error.
      throw 'InvalidStateError: close called on closed dialog';
    }
    this.open = false;
    this.removeAttribute('open');

    // Leave returnValue untouched in case it was set directly on the element
    if (typeof retval != 'undefined') {
      this.returnValue = retval;
    }

    // This won't match the native <dialog> exactly because if the user sets top
    // on a centered polyfill dialog, that top gets thrown away when the dialog is
    // closed. Not sure it's possible to polyfill this perfectly.
    if (this.dialogPolyfillInfo.isTopOverridden) {
      this.style.top = 'auto';
    }

    if (this.dialogPolyfillInfo.modal) {
      dialogPolyfill.dm.removeDialog(this);
    }

    // Triggering "close" event for any attached listeners on the <dialog>
    var closeEvent = new supportCustomEvent('close', {
      bubbles: true,
      cancelable: true
    });
    this.dispatchEvent(closeEvent);  // TODO: handle cancelling this event

    return this.returnValue;
  };

  dialogPolyfill.registerDialog = function(element) {
    if (element.show) {
      console.warn("This browser already supports <dialog>, the polyfill " +
          "may not work correctly.");
    }
    element.show = dialogPolyfill.showDialog.bind(element, false);
    element.showModal = dialogPolyfill.showDialog.bind(element, true);
    element.close = dialogPolyfill.close.bind(element);
    element.dialogPolyfillInfo = {};
    element.open = false;
  };

  // The overlay is used to simulate how a modal dialog blocks the document. The
  // blocking dialog is positioned on top of the overlay, and the rest of the
  // dialogs on the pending dialog stack are positioned below it. In the actual
  // implementation, the modal dialog stacking is controlled by the top layer,
  // where z-index has no effect.
  var TOP_LAYER_ZINDEX = 100000;
  var MAX_PENDING_DIALOGS = 100000;

  dialogPolyfill.DialogManager = function() {
    this.pendingDialogStack = [];
    this.overlay = document.createElement('div');
    this.overlay.className = '_dialog_overlay';

    this.overlay.addEventListener('click', function(e) {
      e.stopPropagation();
    });
  };

  dialogPolyfill.DialogManager.prototype.blockDocument = function() {
    if (!document.body.contains(this.overlay)) {
      document.body.appendChild(this.overlay);

      // On Safari/Mac (and possibly other browsers), the documentElement is
      // not focusable. This is required for modal dialogs as it is the first
      // element to be hit by a tab event, and further tabs are redirected to
      // the most visible dialog.
      if (this.needsDocumentElementFocus === undefined) {
        document.documentElement.focus();
        this.needsDocumentElementFocus =
            (document.activeElement != document.documentElement);
      }
      if (this.needsDocumentElementFocus) {
        document.documentElement.tabIndex = 1;
      }
    }
  };

  dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
    document.body.removeChild(this.overlay);
    if (this.needsDocumentElementFocus) {
      // TODO: Restore the previous tabIndex, rather than clearing it.
      document.documentElement.tabIndex = '';
    }
  };

  dialogPolyfill.DialogManager.prototype.updateStacking = function() {
    if (this.pendingDialogStack.length == 0) {
      this.unblockDocument();
      return;
    }
    this.blockDocument();

    var zIndex = TOP_LAYER_ZINDEX;
    for (var i = 0; i < this.pendingDialogStack.length; i++) {
      if (i == this.pendingDialogStack.length - 1)
        this.overlay.style.zIndex = zIndex++;
      var dialog = this.pendingDialogStack[i];
      dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++;
      dialog.style.zIndex = zIndex++;
    }
  };

  var tabDirection = 0;

  dialogPolyfill.DialogManager.prototype.handleKey = function(event) {
    var dialogCount = this.pendingDialogStack.length;
    if (dialogCount == 0) {
      return;
    }
    var dialog = this.pendingDialogStack[dialogCount - 1];

    tabDirection = 0;

    switch (event.keyCode) {
    case 9: /* tab */
      tabDirection = event.shiftKey ? +1 : -1;
      break;

    case 27: /* esc */
      event.preventDefault();
      event.stopPropagation();
      var cancelEvent = new supportCustomEvent('cancel', {
        bubbles: false,
        cancelable: true
      });
      if (dialog.dispatchEvent(cancelEvent)) {
        dialog.close();
      }
      break;

    }
  };

  dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) {
    if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) {
      throw "Too many modal dialogs";
    }

    var backdrop = document.createElement('div');
    backdrop.className = 'backdrop';
    var clickEventListener = function(e) {
      var redirectedEvent = document.createEvent('MouseEvents');
      redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
          e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
          e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
      dialog.dispatchEvent(redirectedEvent);
      e.stopPropagation();
    };
    backdrop.addEventListener('click', clickEventListener);
    dialog.parentNode.insertBefore(backdrop, dialog.nextSibling);
    dialog.dialogPolyfillInfo.backdrop = backdrop;
    dialog.dialogPolyfillInfo.clickEventListener = clickEventListener;
    this.pendingDialogStack.push(dialog);
    this.updateStacking();
  };

  dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) {
    var index = this.pendingDialogStack.indexOf(dialog);
    if (index == -1) {
      return;
    }
    this.pendingDialogStack.splice(index, 1);
    var backdrop = dialog.dialogPolyfillInfo.backdrop;
    var clickEventListener = dialog.dialogPolyfillInfo.clickEventListener;
    backdrop.removeEventListener('click', clickEventListener);
    backdrop.parentNode.removeChild(backdrop);
    dialog.dialogPolyfillInfo.backdrop = null;
    dialog.dialogPolyfillInfo.clickEventListener = null;
    this.updateStacking();
  };

  dialogPolyfill.dm = new dialogPolyfill.DialogManager();

  document.addEventListener('keydown',
      dialogPolyfill.dm.handleKey.bind(dialogPolyfill.dm));

  /**
   * Global form 'dialog' method handler. Closes a dialog correctly on submit
   * and possibly sets its return value.
   */
  document.addEventListener('submit', function(ev) {
    var method = ev.target.getAttribute('method').toLowerCase();
    if (method != 'dialog') { return; }
    ev.preventDefault();

    var dialog = findNearestDialog(/** @type {Element} */ (ev.target));
    if (!dialog) { return; }

    // FIXME: The original event doesn't contain the INPUT element used to
    // submit the form (if any). Look in some possible places.
    var returnValue;
    var cands = [document.activeElement, ev.explicitOriginalTarget];
    cands.some(function(cand) {
      if (cand && cand.nodeName == 'INPUT' && cand.form == ev.target) {
        returnValue = cand.value;
        return true;
      }
    });
    dialog.close(returnValue);
  }, true);

  return dialogPolyfill;
})();
<commit_msg>another TODO<commit_after>var dialogPolyfill = (function() {

  var supportCustomEvent = window.CustomEvent;
  if (!supportCustomEvent || typeof supportCustomEvent == 'object') {
    supportCustomEvent = function CustomEvent(event, x) {
      x = x || {};
      var ev = document.createEvent('CustomEvent');
      ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
      return ev;
    };
    supportCustomEvent.prototype = window.Event.prototype;
  }

  /**
   * Finds the nearest <dialog> from the passed element.
   *
   * @param {Element} el to search from
   * @return {HTMLDialogElement} dialog found
   */
  function findNearestDialog(el) {
    while (el) {
      if (el.nodeName == 'DIALOG') {
        return /** @type {HTMLDialogElement} */ (el);
      }
      el = el.parentElement;
    }
    return null;
  }

  var dialogPolyfill = {};

  dialogPolyfill.reposition = function(element) {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
    element.style.top = Math.max(0, topValue) + 'px';
    element.dialogPolyfillInfo.isTopOverridden = true;
  };

  dialogPolyfill.inNodeList = function(nodeList, node) {
    for (var i = 0; i < nodeList.length; ++i) {
      if (nodeList[i] == node)
        return true;
    }
    return false;
  };

  dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
    for (var i = 0; i < document.styleSheets.length; ++i) {
      var styleSheet = document.styleSheets[i];
      var cssRules = null;
      // Some browsers throw on cssRules.
      try {
        cssRules = styleSheet.cssRules;
      } catch (e) {}
      if (!cssRules)
        continue;
      for (var j = 0; j < cssRules.length; ++j) {
        var rule = cssRules[j];
        var selectedNodes = null;
        // Ignore errors on invalid selector texts.
        try {
          selectedNodes = document.querySelectorAll(rule.selectorText);
        } catch(e) {}
        if (!selectedNodes || !dialogPolyfill.inNodeList(selectedNodes, element))
          continue;
        var cssTop = rule.style.getPropertyValue('top');
        var cssBottom = rule.style.getPropertyValue('bottom');
        if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
          return true;
      }
    }
    return false;
  };

  dialogPolyfill.needsCentering = function(dialog) {
    var computedStyle = window.getComputedStyle(dialog);
    if (computedStyle.position != 'absolute') {
      return false;
    }

    // We must determine whether the top/bottom specified value is non-auto.  In
    // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
    // Firefox returns the used value. So we do this crazy thing instead: check
    // the inline style and then go through CSS rules.
    if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
        (dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
      return false;
    return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
  };

  dialogPolyfill.showDialog = function(isModal) {
    if (this.open) {
      throw 'InvalidStateError: showDialog called on open dialog';
    }
    this.open = true;  // TODO: should be a getter mapped to attribute
    this.setAttribute('open', 'open');

    if (isModal) {
      // Find element with `autofocus` attribute or first form control.
      var target = this.querySelector('[autofocus]:not([disabled])');
      if (!target) {
        // TODO: technically this is 'any focusable area'
        var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
        var query = opts.map(function(el) {
          return el + ':not([disabled])';
        }).join(', ');
        target = this.querySelector(query);
      }
      target && target.focus();
    }

    if (dialogPolyfill.needsCentering(this))
      dialogPolyfill.reposition(this);
    if (isModal) {
      this.dialogPolyfillInfo.modal = true;
      dialogPolyfill.dm.pushDialog(this);
    }
  };

  dialogPolyfill.close = function(retval) {
    if (!this.open && !window.HTMLDialogElement) {
      // Native implementations will set .open to false, so ignore this error.
      throw 'InvalidStateError: close called on closed dialog';
    }
    this.open = false;
    this.removeAttribute('open');

    // Leave returnValue untouched in case it was set directly on the element
    if (typeof retval != 'undefined') {
      this.returnValue = retval;
    }

    // This won't match the native <dialog> exactly because if the user sets top
    // on a centered polyfill dialog, that top gets thrown away when the dialog is
    // closed. Not sure it's possible to polyfill this perfectly.
    if (this.dialogPolyfillInfo.isTopOverridden) {
      this.style.top = 'auto';
    }

    if (this.dialogPolyfillInfo.modal) {
      dialogPolyfill.dm.removeDialog(this);
    }

    // Triggering "close" event for any attached listeners on the <dialog>
    var closeEvent = new supportCustomEvent('close', {
      bubbles: true,
      cancelable: true
    });
    this.dispatchEvent(closeEvent);  // TODO: handle cancelling this event

    return this.returnValue;
  };

  dialogPolyfill.registerDialog = function(element) {
    if (element.show) {
      console.warn("This browser already supports <dialog>, the polyfill " +
          "may not work correctly.");
    }
    element.show = dialogPolyfill.showDialog.bind(element, false);
    element.showModal = dialogPolyfill.showDialog.bind(element, true);
    element.close = dialogPolyfill.close.bind(element);
    element.dialogPolyfillInfo = {};
    element.open = false;
  };

  // The overlay is used to simulate how a modal dialog blocks the document. The
  // blocking dialog is positioned on top of the overlay, and the rest of the
  // dialogs on the pending dialog stack are positioned below it. In the actual
  // implementation, the modal dialog stacking is controlled by the top layer,
  // where z-index has no effect.
  var TOP_LAYER_ZINDEX = 100000;
  var MAX_PENDING_DIALOGS = 100000;

  dialogPolyfill.DialogManager = function() {
    this.pendingDialogStack = [];
    this.overlay = document.createElement('div');
    this.overlay.className = '_dialog_overlay';

    this.overlay.addEventListener('click', function(e) {
      e.stopPropagation();
    });
  };

  dialogPolyfill.DialogManager.prototype.blockDocument = function() {
    if (!document.body.contains(this.overlay)) {
      document.body.appendChild(this.overlay);

      // On Safari/Mac (and possibly other browsers), the documentElement is
      // not focusable. This is required for modal dialogs as it is the first
      // element to be hit by a tab event, and further tabs are redirected to
      // the most visible dialog.
      if (this.needsDocumentElementFocus === undefined) {
        document.documentElement.focus();
        this.needsDocumentElementFocus =
            (document.activeElement != document.documentElement);
      }
      if (this.needsDocumentElementFocus) {
        document.documentElement.tabIndex = 1;
      }
    }
  };

  dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
    document.body.removeChild(this.overlay);
    if (this.needsDocumentElementFocus) {
      // TODO: Restore the previous tabIndex, rather than clearing it.
      document.documentElement.tabIndex = '';
    }
  };

  dialogPolyfill.DialogManager.prototype.updateStacking = function() {
    if (this.pendingDialogStack.length == 0) {
      this.unblockDocument();
      return;
    }
    this.blockDocument();

    var zIndex = TOP_LAYER_ZINDEX;
    for (var i = 0; i < this.pendingDialogStack.length; i++) {
      if (i == this.pendingDialogStack.length - 1)
        this.overlay.style.zIndex = zIndex++;
      var dialog = this.pendingDialogStack[i];
      dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++;
      dialog.style.zIndex = zIndex++;
    }
  };

  var tabDirection = 0;

  dialogPolyfill.DialogManager.prototype.handleKey = function(event) {
    var dialogCount = this.pendingDialogStack.length;
    if (dialogCount == 0) {
      return;
    }
    var dialog = this.pendingDialogStack[dialogCount - 1];

    tabDirection = 0;

    switch (event.keyCode) {
    case 9: /* tab */
      tabDirection = event.shiftKey ? +1 : -1;
      break;

    case 27: /* esc */
      event.preventDefault();
      event.stopPropagation();
      var cancelEvent = new supportCustomEvent('cancel', {
        bubbles: false,
        cancelable: true
      });
      if (dialog.dispatchEvent(cancelEvent)) {
        dialog.close();
      }
      break;

    }
  };

  dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) {
    if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) {
      throw "Too many modal dialogs";
    }

    var backdrop = document.createElement('div');
    backdrop.className = 'backdrop';
    var clickEventListener = function(e) {
      var redirectedEvent = document.createEvent('MouseEvents');
      redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
          e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
          e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
      dialog.dispatchEvent(redirectedEvent);
      e.stopPropagation();
    };
    backdrop.addEventListener('click', clickEventListener);
    dialog.parentNode.insertBefore(backdrop, dialog.nextSibling);
    dialog.dialogPolyfillInfo.backdrop = backdrop;
    dialog.dialogPolyfillInfo.clickEventListener = clickEventListener;
    this.pendingDialogStack.push(dialog);
    this.updateStacking();
  };

  dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) {
    var index = this.pendingDialogStack.indexOf(dialog);
    if (index == -1) {
      return;
    }
    this.pendingDialogStack.splice(index, 1);
    var backdrop = dialog.dialogPolyfillInfo.backdrop;
    var clickEventListener = dialog.dialogPolyfillInfo.clickEventListener;
    backdrop.removeEventListener('click', clickEventListener);
    backdrop.parentNode.removeChild(backdrop);
    dialog.dialogPolyfillInfo.backdrop = null;
    dialog.dialogPolyfillInfo.clickEventListener = null;
    this.updateStacking();
  };

  dialogPolyfill.dm = new dialogPolyfill.DialogManager();

  document.addEventListener('keydown',
      dialogPolyfill.dm.handleKey.bind(dialogPolyfill.dm));

  /**
   * Global form 'dialog' method handler. Closes a dialog correctly on submit
   * and possibly sets its return value.
   */
  document.addEventListener('submit', function(ev) {
    var method = ev.target.getAttribute('method').toLowerCase();
    if (method != 'dialog') { return; }
    ev.preventDefault();

    var dialog = findNearestDialog(/** @type {Element} */ (ev.target));
    if (!dialog) { return; }

    // FIXME: The original event doesn't contain the INPUT element used to
    // submit the form (if any). Look in some possible places.
    var returnValue;
    var cands = [document.activeElement, ev.explicitOriginalTarget];
    cands.some(function(cand) {
      if (cand && cand.nodeName == 'INPUT' && cand.form == ev.target) {
        returnValue = cand.value;
        return true;
      }
    });
    dialog.close(returnValue);
  }, true);

  return dialogPolyfill;
})();
<|endoftext|>