<commit_before>(function() {
  // Setting some sweet global variables (note: Refactor to globalize, except for optiflowVector)
  var view = {
    size: {
      width: window.innerWidth,
      height: window.innerHeight
    }
  };

  var boids = [];
  var groupTogether = false;
  var optiflowVector = new PIXI.Vector(0, 0);
  var borderFactor = 10;
  var heartPath = [];

  // Create a path. This is for testing. Ideally we should detect an outline with an IR camera and using that data for the path.
  var steps = 30;
  for (var i = 0; i < steps; i++) {
    heartPath.push(new PIXI.Vector(
      (view.size.width / 2) + 300 * Math.cos(2 * Math.PI * i / steps),
      (view.size.height / 2) + 300 * Math.sin(2 * Math.PI * i / steps)
    ));
  }

  var Boid = function(position, maxSpeed, maxForce) {
      var strength = Math.random()*2 + 2;
      this.acceleration = new PIXI.Vector();
      this.vector = new PIXI.Vector(Math.random(), Math.random()).multiplyScalar(50).add(new PIXI.Vector(10,10));
      this.position = position.clone();
      this.radius = 30;
      this.maxSpeed = maxSpeed + strength;
      this.maxForce = maxForce + strength;
      this.amount = strength * 10 + 10;
      this.count = 0;
      this.flap = Math.random() * .5 + .5;
  };

  Boid.prototype.run = function(boids) {
    if (!groupTogether) {
      this.flock(boids);
    } else {
      this.align(boids);
    }
    this.update();
  };

  // We accumulate a new acceleration each time based on several rules
  Boid.prototype.flock = function(boids) {
    var separation = this.separate(boids).multiplyScalar(1);
    var alignment = this.align(boids).multiplyScalar(20);
    var cohesion = this.cohesion(boids).multiplyScalar(.5);
    var borders = this.borders().multiplyScalar(2);
    this.acceleration.add(separation).add(alignment).add(cohesion).add(borders);
  };

  Boid.prototype.opticalFlow = function() {
    if (window.boidData && window.boidData.u) {
        var u = window.boidData.u;
        var v = window.boidData.v;
        optiflowVector.x -= u;
        optiflowVector.y += v;
    }
    return vector;
  }

  Boid.prototype.update = function() {
    // Update velocity
    this.vector.add(this.acceleration);
    // Limit speed (vector#limit?)
    this.vector.setLength( Math.min(this.maxSpeed, this.vector.length()) );
    this.position.add(this.vector);
    // Reset acceleration to 0 each cycle
    // The closer to 0 this is, the more insect like the behavior
    // with sudden changes in direction
    this.acceleration.multiplyScalar(0);
  };

  Boid.prototype.seek = function(target) {
    this.acceleration.add(this.steer(target, false));
  };

  Boid.prototype.arrive = function(target) {
    this.acceleration.add(this.steer(target, true));
  };

  Boid.prototype.borders = function() {

    var vector = new PIXI.Vector(0, 0);
    var position = this.position;
    var radius = this.radius;

    if (position.x < -radius) {
      vector.x = borderFactor * (-radius - position.x);
    }
    if (position.y < -radius){
      vector.y = borderFactor * (-radius - position.y);
    }
    if (position.x > size.x + radius){
      vector.x = -borderFactor * (position.x - size.x - radius);
    }
    if (position.y > size.y + radius) {
      vector.y = -borderFactor * (position.y - size.y - radius);
    }

    return vector;
  };

  // A method that calculates a steering vector towards a target
  // Takes a second argument, if true, it slows down as it approaches
  // the target
  Boid.prototype.steer = function(target, slowdown) {
    var steer;
    var desired = target.clone().sub(this.position);
    var distance = desired.length();
    // Two options for desired vector magnitude
    // (1 -- based on distance, 2 -- maxSpeed)
    if (slowdown && distance < 100) {
      // This damping is somewhat arbitrary:
      desired.setLength( this.maxSpeed * (1 / distance ) );
    } else {
      // desired.setLength( this.maxSpeed );
    }
    steer = desired.clone().sub(this.vector);
    steer.setLength( Math.min(this.maxForce, steer.length()) );

    return steer;
  };

  Boid.prototype.separate = function(boids) {
    var desiredSeperation = Math.pow(100, 2);
    var steer = new PIXI.Vector(0, 0);
    var count = 0;
    // For every boid in the system, check if it's too close
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      // since this. and other. poisition are PIXI.Points, need to create a new Vector out of that point
      var vector = new PIXI.Vector(this.position.clone().sub(other.position));
      var distanceSq = vector.lengthSq();
      if (distanceSq > 0 && distanceSq < desiredSeperation) {
        // Calculate vector pointing away from neighbor
        steer.add( vector.multiplyScalar(1 / (distanceSq * distanceSq) ) );
        count++;
      }
    }
    // Average -- divide by how many
    if (count > 0)
      steer.divideScalar(count);
    if ( steer.length() !== 0 ) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer.setLength(this.maxSpeed);
      steer.sub(this.vector);
      steer.setLength( Math.min(steer.length(), this.maxForce) );
    }
    return steer;
  };

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  Boid.prototype.align = function(boids) {
    // Using square of distance to ease calculations
    var neighborDist = Math.pow(250, 2);
    var steer = new PIXI.Vector(0, 0);
    var count = 0;
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      // We just need to find the square of the distance — less calculation
      var distanceSq = this.position.distanceToSq(other.position);
      // debugger;
      if (distanceSq > 0 && distanceSq < neighborDist) {
        steer.add(other.vector);
        count++;
      }
    }

    if (count > 0)
      steer.divideScalar(count);
    if (! (steer.length() === 0) ) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer.setLength(this.maxSpeed);
      steer.sub(this.vector);
      steer.setLength( Math.min(steer.length(), this.maxForce) );
    }
    return steer;
  };

  // Cohesion
  // For the average location (i.e. center) of all nearby boids,
  // calculate steering vector towards that location
  Boid.prototype.cohesion = function(boids) {
    // Using square of distance to ease calculations
    var neighborDist = Math.pow(600, 2);
    var sum = new PIXI.Vector(0, 0);
    var count = 0;
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      var distanceSq = this.position.distanceToSq(other.position);
      if (distanceSq > 0 && distanceSq < neighborDist) {
        sum.add(other.position); // Add location
        count++;
      }
    }
    if (count > 0) {
      sum.divideScalar(count);
      // Steer towards the location
      return this.steer(sum, false);
    }
    return sum;
  };

    var size = {
      x: window.innerWidth,
      y: window.innerHeight
    };
    // create an new instance of a pixi _stage - and set it to be interactive
    var _stage = new PIXI.Stage(0xd9e2cc);
    _stage.setInteractive(true);

    // create a _renderer instance
    var _renderer = PIXI.autoDetectRenderer(size.x, size.y);

    _renderer.view.style.width = size.x + "px";
    _renderer.view.style.height = size.y + "px";

    _renderer.view.style.display = "block";

    requestAnimFrame(animate);
    var renderTexture = new PIXI.RenderTexture(size.x, size.y);

    var _target = new PIXI.DisplayObjectContainer();
    _stage.addChild(_target);
    var count = 0;

    // add render view to DOM
    document.body.appendChild(_renderer.view);

    //Main loop -
    init();
    _stage.setInteractive(true);
    _stage.mousedown = _stage.touchstart = onMouseDown;

    function init() {
      var boidContainers = [];

        // Add the boids:
        for (var i = 0; i < 200; i++) {
          var position = new PIXI.Point(Math.random() * size.x, Math.random() * size.y);
          var boid = new Boid(position, 4, 1);
          var boidContainer = new PIXI.DisplayObjectContainer();
          var boidGraphic = new PIXI.Graphics();
          boidGraphic.beginFill(0x002244);
          boidGraphic.drawCircle(0, 0, 4);
          boidGraphic.endFill();

          boidContainer.addChild(boidGraphic);
          boidContainer.alpha = boid.vector.length() / boid.maxSpeed;

          boid.container = boidContainer;
          boidContainers.push(boidContainer);

          boids.push(boid);

          _target.addChild(boidContainer);
          _stage.addChild(boidContainer);
        }
    }

    function onMouseDown() {
        groupTogether = !groupTogether;
    }

    function animate() {
        requestAnimFrame(animate);

        count += 1;
        _renderer.render(_stage);

        for (var i = 0, l = boids.length; i < l; i++) {
          var boid = boids[i];

          if (groupTogether) {
            // boid.seek(new PIXI.Vector(400,400));
            for (var i = 0, l = boids.length; i < l; i++) {
              var index = parseInt( (i + parseInt(i) / 30 % l ) / l * heartPath.length );
              var point = heartPath[index];
              console.log(point);
              if (point) {
                boid.arrive(point);
              }
            }
          }
          boid.container.position = boid.position;
          boid.vector.normalize();

          boid.count += 1;
          // boid.container.alpha = 1.1 - ( 1 / (boid.vector.lengthSq() + 1) ) ;
          // boid.container.rotation = boid.vector.rad() + .7707;

          // var flapFactor = boid.count * boid.vector.lengthSq() * .3;
          // boid.container.scale = new PIXI.Point(Math.sin(flapFactor) + .15, .6);

          boid.run(boids);
        }
    }
})();
<commit_msg>Path following finally works. Removed redundant nested for loop.<commit_after>(function() {
  // Setting some sweet global variables (note: Refactor to globalize, except for optiflowVector)
  var view = {
    size: {
      width: window.innerWidth,
      height: window.innerHeight
    }
  };

  var boids = [];
  var groupTogether = false;
  var optiflowVector = new PIXI.Vector(0, 0);
  var borderFactor = 10;
  var path = [];

  // Create a path. This is for testing. Ideally we should detect an outline with an IR camera and using that data for the path.
  var steps = 30;
  for (var i = 0; i < steps; i++) {
    path.push(new PIXI.Vector(
      (view.size.width / 2) + 300 * Math.cos(2 * Math.PI * i / steps),
      (view.size.height / 2) + 300 * Math.sin(2 * Math.PI * i / steps)
     ));
  }

  var Boid = function(position, maxSpeed, maxForce) {
    var strength = Math.random()*2 + 1;
    this.acceleration = new PIXI.Vector();
    this.vector = new PIXI.Vector(Math.random(), Math.random()).multiplyScalar(50).add(new PIXI.Vector(10,10));
    this.position = position.clone();
    this.radius = 30;
    this.maxSpeed = maxSpeed + strength;
    this.maxForce = maxForce + strength;
    this.amount = strength * 10 + 10;
    this.count = 0;
    this.flap = Math.random() * .5 + .5;
  };

  Boid.prototype.run = function(boids) {
    if (!groupTogether) {
      this.flock(boids);
    } else {
      this.align(boids);
    }
    this.update();
  };

  // We accumulate a new acceleration each time based on several rules
  Boid.prototype.flock = function(boids) {
    var separation = this.separate(boids).multiplyScalar(1);
    var alignment = this.align(boids).multiplyScalar(20);
    var cohesion = this.cohesion(boids).multiplyScalar(.5);
    var borders = this.borders().multiplyScalar(2);
    this.acceleration.add(separation).add(alignment).add(cohesion).add(borders);
  };

  Boid.prototype.opticalFlow = function() {
    if (window.boidData && window.boidData.u) {
      var u = window.boidData.u;
      var v = window.boidData.v;
      optiflowVector.x -= u;
      optiflowVector.y += v;
    }
    return vector;
  }

  Boid.prototype.update = function() {
    // Update velocity
    this.vector.add(this.acceleration);
    // Limit speed (vector#limit?)
    this.vector.setLength(Math.min(this.maxSpeed, this.vector.length()));
    this.position.add(this.vector);
    // Reset acceleration to 0 each cycle
    // The closer to 0 this is, the more insect like the behavior
    // with sudden changes in direction
    this.acceleration.multiplyScalar(.95);
  };

  Boid.prototype.seek = function(target) {
    this.acceleration.add(this.steer(target, false));
  };

  Boid.prototype.arrive = function(target) {
    this.acceleration.add(this.steer(target, true));
  };

  Boid.prototype.borders = function() {

    var vector = new PIXI.Vector(0, 0);
    var position = this.position;
    var radius = this.radius;

    if (position.x < -radius) {
      vector.x = borderFactor * (-radius - position.x);
    }
    if (position.y < -radius){
      vector.y = borderFactor * (-radius - position.y);
    }
    if (position.x > size.x + radius){
      vector.x = -borderFactor * (position.x - size.x - radius);
    }
    if (position.y > size.y + radius) {
      vector.y = -borderFactor * (position.y - size.y - radius);
    }

    return vector;
  };

  // A method that calculates a steering vector towards a target
  // Takes a second argument, if true, it slows down as it approaches
  // the target
  Boid.prototype.steer = function(target, slowdown) {
    var steer;
    var desired = target.clone().sub(this.position);
    var distance = desired.length();
    // Two options for desired vector magnitude
    // (1 -- based on distance, 2 -- maxSpeed)
    if (slowdown && distance < 20) {
      // This damping is somewhat arbitrary:
      desired.setLength(this.maxSpeed * (1 / distance));
    } else {
      // desired.setLength(this.maxSpeed);
    }
    steer = desired.clone().sub(this.vector);
    steer.setLength(Math.min(this.maxForce, steer.length()));

    return steer;
  };

  Boid.prototype.separate = function(boids) {
    var desiredSeperation = Math.pow(100, 2);
    var steer = new PIXI.Vector(0, 0);
    var count = 0;
    // For every boid in the system, check if it's too close
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      // since this. and other. poisition are PIXI.Points, need to create a new Vector out of that point
      var vector = new PIXI.Vector(this.position.clone().sub(other.position));
      var distanceSq = vector.lengthSq();
      if (distanceSq > 0 && distanceSq < desiredSeperation) {
        // Calculate vector pointing away from neighbor
        steer.add(vector.multiplyScalar(1 / (distanceSq * distanceSq)));
        count++;
      }
    }
    // Average -- divide by how many
    if (count > 0)
      steer.divideScalar(count);
    if (steer.length() !== 0) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer.setLength(this.maxSpeed);
      steer.sub(this.vector);
      steer.setLength(Math.min(steer.length(), this.maxForce));
    }
    return steer;
  };

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  Boid.prototype.align = function(boids) {
    // Using square of distance to ease calculations
    var neighborDist = Math.pow(250, 2);
    var steer = new PIXI.Vector(0, 0);
    var count = 0;
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      // We just need to find the square of the distance — less calculation
      var distanceSq = this.position.distanceToSq(other.position);
      // debugger;
      if (distanceSq > 0 && distanceSq < neighborDist) {
        steer.add(other.vector);
        count++;
      }
    }

    if (count > 0)
      steer.divideScalar(count);
    if (! (steer.length() === 0)) {
      // Implement Reynolds: Steering = Desired - Velocity
      steer.setLength(this.maxSpeed);
      steer.sub(this.vector);
      steer.setLength(Math.min(steer.length(), this.maxForce));
    }
    return steer;
  };

  // Cohesion
  // For the average location (i.e. center) of all nearby boids,
  // calculate steering vector towards that location
  Boid.prototype.cohesion = function(boids) {
    // Using square of distance to ease calculations
    var neighborDist = Math.pow(600, 2);
    var sum = new PIXI.Vector(0, 0);
    var count = 0;
    for (var i = 0, l = boids.length; i < l; i++) {
      var other = boids[i];
      var distanceSq = this.position.distanceToSq(other.position);
      if (distanceSq > 0 && distanceSq < neighborDist) {
        sum.add(other.position); // Add location
        count++;
      }
    }
    if (count > 0) {
      sum.divideScalar(count);
      // Steer towards the location
      return this.steer(sum, false);
    }
    return sum;
  };

  var size = {
    x: window.innerWidth,
    y: window.innerHeight
  };
  // create an new instance of a pixi _stage - and set it to be interactive
  var _stage = new PIXI.Stage(0xd9e2cc);
  _stage.setInteractive(true);

  // create a _renderer instance
  var _renderer = PIXI.autoDetectRenderer(size.x, size.y);

  _renderer.view.style.width = size.x + "px";
  _renderer.view.style.height = size.y + "px";

  _renderer.view.style.display = "block";

  requestAnimFrame(animate);
  var renderTexture = new PIXI.RenderTexture(size.x, size.y);

  var _target = new PIXI.DisplayObjectContainer();
  _stage.addChild(_target);
  var count = 0;

  // add render view to DOM
  document.body.appendChild(_renderer.view);

  //Main loop -
  init();
  _stage.setInteractive(true);
  _stage.mousedown = _stage.touchstart = onMouseDown;

  function init() {
    var boidContainers = [];

    // Add the boids:
    for (var i = 0; i < 200; i++) {
      var position = new PIXI.Point(Math.random() * size.x, Math.random() * size.y);
      var boid = new Boid(position, .1, 20);
      var boidContainer = new PIXI.DisplayObjectContainer();
      var boidGraphic = new PIXI.Graphics();
      boidGraphic.beginFill(0x002244);
      boidGraphic.drawCircle(0, 0, 4);
      boidGraphic.endFill();

      boidContainer.addChild(boidGraphic);
      boidContainer.alpha = boid.vector.length() / boid.maxSpeed;

      boid.container = boidContainer;
      boidContainers.push(boidContainer);

      boids.push(boid);

      _target.addChild(boidContainer);
      _stage.addChild(boidContainer);
    }
  }

  function onMouseDown() {
    groupTogether = !groupTogether;
  }

  function animate() {
    requestAnimFrame(animate);

    count += 1;
    _renderer.render(_stage);

    for (var i = 0, l = boids.length; i < l; i++) {
      var boid = boids[i];

      if (groupTogether) {
        var index = parseInt( ((i + boid.count / 6) % l) / l * path.length);
        var point = path[index];
        boid.seek(point);
      }
      boid.container.position = boid.position;
      boid.vector.normalize();

      boid.count += (1 + boid.flap);
      boid.container.alpha = 1.1 - (1 / (boid.vector.lengthSq() + 1)) ;
      boid.container.rotation = -boid.vector.rad();

      var flapFactor = boid.count * boid.vector.lengthSq() * .2;
      boid.container.scale = new PIXI.Point(Math.sin(flapFactor) + .45, .6);

      boid.run(boids);
    }
  }
})();
<|endoftext|>


<commit_before>DV.model.Annotations = function(viewer) {
  this.LEFT_MARGIN              = 25;
  this.PAGE_NOTE_FUDGE          = window.dc && dc.account && (dc.account.isOwner || dc.account.isReviewer) ? 46 : 26;
  this.viewer                   = viewer;
  this.offsetsAdjustments       = [];
  this.offsetAdjustmentSum      = 0;
  this.saveCallbacks            = [];
  this.deleteCallbacks          = [];
  this.byId                     = this.viewer.schema.data.annotationsById;
  this.byPage                   = this.viewer.schema.data.annotationsByPage;
  this.bySortOrder              = this.sortAnnotations();
};

DV.model.Annotations.prototype = {

  // Render an annotation model to HTML, calculating all of the dimenstions
  // and offsets, and running a template function.
  render: function(annotation){
    var documentModel             = this.viewer.models.document;
    var pageModel                 = this.viewer.models.pages;
    var zoom                      = pageModel.zoomFactor();
    var adata                     = annotation;
    var x1, x2, y1, y2;

    if(adata.type === 'page'){
      x1 = x2 = y1 = y2           = 0;
      adata.top                   = 0;
    }else{
      y1                          = Math.round(adata.y1 * zoom);
      y2                          = Math.round(adata.y2 * zoom);
      if (x1 < this.LEFT_MARGIN) x1 = this.LEFT_MARGIN;
      x1                          = Math.round(adata.x1 * zoom);
      x2                          = Math.round(adata.x2 * zoom);
      adata.top                   = y1 - 5;
    }

    adata.owns_note               = adata.owns_note || false;
    adata.width                   = pageModel.width;
    adata.pageNumber              = adata.page;
    adata.author                  = adata.author || "";
    adata.author_organization     = adata.author_organization || "";
    adata.bgWidth                 = adata.width;
    adata.bWidth                  = adata.width - 66;
    adata.excerptWidth            = (x2 - x1) - 8;
    adata.excerptMarginLeft       = x1 - 3;
    adata.excerptHeight           = y2 - y1;
    adata.index                   = adata.page - 1;
    adata.image                   = pageModel.imageURL(adata.index);
    adata.imageTop                = y1 + 1;
    adata.tabTop                  = (y1 < 35 ? 35 - y1 : 0) + 8;
    adata.imageWidth              = pageModel.width;
    adata.imageHeight             = Math.round(pageModel.height * zoom);
    adata.regionLeft              = x1;
    adata.regionWidth             = x2 - x1 ;
    adata.regionHeight            = y2 - y1;
    adata.excerptDSHeight         = adata.excerptHeight - 6;
    adata.DSOffset                = 3;

    if (adata.access == 'public')         adata.accessClass = 'DV-accessPublic';
    else if (adata.access =='exclusive')  adata.accessClass = 'DV-accessExclusive';
    else if (adata.access =='private')    adata.accessClass = 'DV-accessPrivate';

    adata.orderClass = '';
    adata.options = this.viewer.options;
    if (adata.position == 1) adata.orderClass += ' DV-firstAnnotation';
    if (adata.position == this.bySortOrder.length) adata.orderClass += ' DV-lastAnnotation';

    var template = (adata.type === 'page') ? 'pageAnnotation' : 'annotation';
    return JST[template](adata);
  },

  // Re-sort the list of annotations when its contents change. Annotations
  // are ordered by page primarily, and then their y position on the page.
  sortAnnotations : function() {
    return this.bySortOrder = _.sortBy(_.values(this.byId), function(anno) {
      return anno.page * 10000 + anno.y1;
    });
  },

  // Renders each annotation into it's HTML format.
  renderAnnotations: function(){
    for (var i=0; i<this.bySortOrder.length; i++) {
      var anno      = this.bySortOrder[i];
      anno.of       = _.indexOf(this.byPage[anno.page - 1], anno);
      anno.position = i + 1;
      anno.html     = this.render(anno);
    }
    this.renderAnnotationsByIndex();
  },

  // Renders each annotation for the "Annotation List" tab, in order.
  renderAnnotationsByIndex: function(){
    var rendered  = _.map(this.bySortOrder, function(anno){ return anno.html; });
    var html      = rendered.join('')
                    .replace(/src="/g, 'data-src="')
                    .replace(/id="DV-annotation-(\d+)"/g, function(match, id) {
      return 'id="DV-listAnnotation-' + id + '" rel="aid-' + id + '"';
    });

    this.viewer.$('div.DV-allAnnotations').html(html);

    this.renderAnnotationsByIndex.rendered  = true;
    this.renderAnnotationsByIndex.zoomLevel = this.zoomLevel;
    _.defer(_.bind(this.updateAnnotationOffsets, this));
  },

  // Refresh the annotation's title and content from the model, in both
  // The document and list views.
  refreshAnnotation : function(anno) {
    var viewer = this.viewer;
    anno.html = this.render(anno);
    DV.jQuery.$('#DV-annotation-' + anno.id).replaceWith(anno.html);
  },

  // Removes a given annotation from the Annotations model (and DOM).
  removeAnnotation : function(anno) {
    delete this.byId[anno.id];
    var i = anno.page - 1;
    this.byPage[i] = _.without(this.byPage[i], anno);
    this.sortAnnotations();
    DV.jQuery('#DV-annotation-' + anno.id + ', #DV-listAnnotation-' + anno.id).remove();
    this.viewer.api.redraw(true);
    if (_.isEmpty(this.byId)) this.viewer.open('ViewDocument');
  },

  // Offsets all document pages based on interleaved page annotations.
  updateAnnotationOffsets: function(){
    this.offsetsAdjustments   = [];
    this.offsetAdjustmentSum  = 0;
    var documentModel         = this.viewer.models.document;
    var annotationsContainer  = this.viewer.$('div.DV-allAnnotations');
    var pageAnnotationEls     = annotationsContainer.find('.DV-pageNote');
    var pageNoteHeights       = this.viewer.models.pages.pageNoteHeights;
    var me = this;

    if(this.viewer.$('div.DV-docViewer').hasClass('DV-viewAnnotations') == false){
      annotationsContainer.addClass('DV-getHeights');
    }

    // First, collect the list of page annotations, and associate them with
    // their DOM elements.
    var pageAnnos = [];
    _.each(_.select(this.bySortOrder, function(anno) {
      return anno.type == 'page';
    }), function(anno, i) {
      anno.el = pageAnnotationEls[i];
      pageAnnos[anno.pageNumber] = anno;
    });

    // Then, loop through the pages and store the cumulative offset due to
    // page annotations.
    for (var i = 0, len = documentModel.totalPages; i <= len; i++) {
      pageNoteHeights[i] = 0;
      if (pageAnnos[i]) {
        var height = (this.viewer.$(pageAnnos[i].el).height() + this.PAGE_NOTE_FUDGE);
        pageNoteHeights[i - 1] = height;
        this.offsetAdjustmentSum += height;
      }
      this.offsetsAdjustments[i] = this.offsetAdjustmentSum;
    }
    annotationsContainer.removeClass('DV-getHeights');
  },

  // When an annotation is successfully saved, fire any registered
  // save callbacks.
  fireSaveCallbacks : function(anno) {
    _.each(this.saveCallbacks, function(c){ c(anno); });
  },

  // When an annotation is successfully removed, fire any registered
  // delete callbacks.
  fireDeleteCallbacks : function(anno) {
    _.each(this.deleteCallbacks, function(c){ c(anno); });
  },

  // Returns the list of annotations on a given page.
  getAnnotations: function(_index){
    return this.byPage[_index];
  },

  getFirstAnnotation: function(){
    return _.first(this.bySortOrder);
  },

  getNextAnnotation: function(currentId) {
    var anno = this.byId[currentId];
    return this.bySortOrder[_.indexOf(this.bySortOrder, anno) + 1];
  },

  getPreviousAnnotation: function(currentId) {
    var anno = this.byId[currentId];
    return this.bySortOrder[_.indexOf(this.bySortOrder, anno) - 1];
  },

  // Get an annotation by id, with backwards compatibility for argument hashes.
  getAnnotation: function(identifier) {
    if (identifier.id) return this.byId[identifier.id];
    if (identifier.index && !identifier.id) throw new Error('looked up an annotation without an id');
    return this.byId[identifier];
  }

};
<commit_msg>grr. defer is never the answer.<commit_after>DV.model.Annotations = function(viewer) {
  this.LEFT_MARGIN              = 25;
  this.PAGE_NOTE_FUDGE          = window.dc && dc.account && (dc.account.isOwner || dc.account.isReviewer) ? 46 : 26;
  this.viewer                   = viewer;
  this.offsetsAdjustments       = [];
  this.offsetAdjustmentSum      = 0;
  this.saveCallbacks            = [];
  this.deleteCallbacks          = [];
  this.byId                     = this.viewer.schema.data.annotationsById;
  this.byPage                   = this.viewer.schema.data.annotationsByPage;
  this.bySortOrder              = this.sortAnnotations();
};

DV.model.Annotations.prototype = {

  // Render an annotation model to HTML, calculating all of the dimenstions
  // and offsets, and running a template function.
  render: function(annotation){
    var documentModel             = this.viewer.models.document;
    var pageModel                 = this.viewer.models.pages;
    var zoom                      = pageModel.zoomFactor();
    var adata                     = annotation;
    var x1, x2, y1, y2;

    if(adata.type === 'page'){
      x1 = x2 = y1 = y2           = 0;
      adata.top                   = 0;
    }else{
      y1                          = Math.round(adata.y1 * zoom);
      y2                          = Math.round(adata.y2 * zoom);
      if (x1 < this.LEFT_MARGIN) x1 = this.LEFT_MARGIN;
      x1                          = Math.round(adata.x1 * zoom);
      x2                          = Math.round(adata.x2 * zoom);
      adata.top                   = y1 - 5;
    }

    adata.owns_note               = adata.owns_note || false;
    adata.width                   = pageModel.width;
    adata.pageNumber              = adata.page;
    adata.author                  = adata.author || "";
    adata.author_organization     = adata.author_organization || "";
    adata.bgWidth                 = adata.width;
    adata.bWidth                  = adata.width - 66;
    adata.excerptWidth            = (x2 - x1) - 8;
    adata.excerptMarginLeft       = x1 - 3;
    adata.excerptHeight           = y2 - y1;
    adata.index                   = adata.page - 1;
    adata.image                   = pageModel.imageURL(adata.index);
    adata.imageTop                = y1 + 1;
    adata.tabTop                  = (y1 < 35 ? 35 - y1 : 0) + 8;
    adata.imageWidth              = pageModel.width;
    adata.imageHeight             = Math.round(pageModel.height * zoom);
    adata.regionLeft              = x1;
    adata.regionWidth             = x2 - x1 ;
    adata.regionHeight            = y2 - y1;
    adata.excerptDSHeight         = adata.excerptHeight - 6;
    adata.DSOffset                = 3;

    if (adata.access == 'public')         adata.accessClass = 'DV-accessPublic';
    else if (adata.access =='exclusive')  adata.accessClass = 'DV-accessExclusive';
    else if (adata.access =='private')    adata.accessClass = 'DV-accessPrivate';

    adata.orderClass = '';
    adata.options = this.viewer.options;
    if (adata.position == 1) adata.orderClass += ' DV-firstAnnotation';
    if (adata.position == this.bySortOrder.length) adata.orderClass += ' DV-lastAnnotation';

    var template = (adata.type === 'page') ? 'pageAnnotation' : 'annotation';
    return JST[template](adata);
  },

  // Re-sort the list of annotations when its contents change. Annotations
  // are ordered by page primarily, and then their y position on the page.
  sortAnnotations : function() {
    return this.bySortOrder = _.sortBy(_.values(this.byId), function(anno) {
      return anno.page * 10000 + anno.y1;
    });
  },

  // Renders each annotation into it's HTML format.
  renderAnnotations: function(){
    for (var i=0; i<this.bySortOrder.length; i++) {
      var anno      = this.bySortOrder[i];
      anno.of       = _.indexOf(this.byPage[anno.page - 1], anno);
      anno.position = i + 1;
      anno.html     = this.render(anno);
    }
    this.renderAnnotationsByIndex();
  },

  // Renders each annotation for the "Annotation List" tab, in order.
  renderAnnotationsByIndex: function(){
    var rendered  = _.map(this.bySortOrder, function(anno){ return anno.html; });
    var html      = rendered.join('')
                    .replace(/src="/g, 'data-src="')
                    .replace(/id="DV-annotation-(\d+)"/g, function(match, id) {
      return 'id="DV-listAnnotation-' + id + '" rel="aid-' + id + '"';
    });

    this.viewer.$('div.DV-allAnnotations').html(html);

    this.renderAnnotationsByIndex.rendered  = true;
    this.renderAnnotationsByIndex.zoomLevel = this.zoomLevel;

    // TODO: This is hacky, but seems to be necessary. When fixing, be sure to
    // test with both autozoom and page notes.
    this.updateAnnotationOffsets();
    _.defer(_.bind(this.updateAnnotationOffsets, this));
  },

  // Refresh the annotation's title and content from the model, in both
  // The document and list views.
  refreshAnnotation : function(anno) {
    var viewer = this.viewer;
    anno.html = this.render(anno);
    DV.jQuery.$('#DV-annotation-' + anno.id).replaceWith(anno.html);
  },

  // Removes a given annotation from the Annotations model (and DOM).
  removeAnnotation : function(anno) {
    delete this.byId[anno.id];
    var i = anno.page - 1;
    this.byPage[i] = _.without(this.byPage[i], anno);
    this.sortAnnotations();
    DV.jQuery('#DV-annotation-' + anno.id + ', #DV-listAnnotation-' + anno.id).remove();
    this.viewer.api.redraw(true);
    if (_.isEmpty(this.byId)) this.viewer.open('ViewDocument');
  },

  // Offsets all document pages based on interleaved page annotations.
  updateAnnotationOffsets: function(){
    this.offsetsAdjustments   = [];
    this.offsetAdjustmentSum  = 0;
    var documentModel         = this.viewer.models.document;
    var annotationsContainer  = this.viewer.$('div.DV-allAnnotations');
    var pageAnnotationEls     = annotationsContainer.find('.DV-pageNote');
    var pageNoteHeights       = this.viewer.models.pages.pageNoteHeights;
    var me = this;

    if(this.viewer.$('div.DV-docViewer').hasClass('DV-viewAnnotations') == false){
      annotationsContainer.addClass('DV-getHeights');
    }

    // First, collect the list of page annotations, and associate them with
    // their DOM elements.
    var pageAnnos = [];
    _.each(_.select(this.bySortOrder, function(anno) {
      return anno.type == 'page';
    }), function(anno, i) {
      anno.el = pageAnnotationEls[i];
      pageAnnos[anno.pageNumber] = anno;
    });

    // Then, loop through the pages and store the cumulative offset due to
    // page annotations.
    for (var i = 0, len = documentModel.totalPages; i <= len; i++) {
      pageNoteHeights[i] = 0;
      if (pageAnnos[i]) {
        var height = (this.viewer.$(pageAnnos[i].el).height() + this.PAGE_NOTE_FUDGE);
        pageNoteHeights[i - 1] = height;
        this.offsetAdjustmentSum += height;
      }
      this.offsetsAdjustments[i] = this.offsetAdjustmentSum;
    }
    annotationsContainer.removeClass('DV-getHeights');
  },

  // When an annotation is successfully saved, fire any registered
  // save callbacks.
  fireSaveCallbacks : function(anno) {
    _.each(this.saveCallbacks, function(c){ c(anno); });
  },

  // When an annotation is successfully removed, fire any registered
  // delete callbacks.
  fireDeleteCallbacks : function(anno) {
    _.each(this.deleteCallbacks, function(c){ c(anno); });
  },

  // Returns the list of annotations on a given page.
  getAnnotations: function(_index){
    return this.byPage[_index];
  },

  getFirstAnnotation: function(){
    return _.first(this.bySortOrder);
  },

  getNextAnnotation: function(currentId) {
    var anno = this.byId[currentId];
    return this.bySortOrder[_.indexOf(this.bySortOrder, anno) + 1];
  },

  getPreviousAnnotation: function(currentId) {
    var anno = this.byId[currentId];
    return this.bySortOrder[_.indexOf(this.bySortOrder, anno) - 1];
  },

  // Get an annotation by id, with backwards compatibility for argument hashes.
  getAnnotation: function(identifier) {
    if (identifier.id) return this.byId[identifier.id];
    if (identifier.index && !identifier.id) throw new Error('looked up an annotation without an id');
    return this.byId[identifier];
  }

};
<|endoftext|>


<commit_before>  var map;
  var bounds_ = new google.maps.LatLngBounds();

    $(document).ready( function() {
      $('#site_show_blog').click(function(){
        if (!$('#tumbler_id').is(':visible')) {
          $(this).parent().addClass('selected');
        } else {
          $(this).parent().removeClass('selected');
        }
        $('#tumbler_id').toggle();
      });

      $('a.manage_partners').click(function(ev){
        ev.preventDefault();
        ev.stopPropagation();
        var position = $('a.manage_partners').position();
        $('#manage_partners').css('top',position.top-360+'px');
        $('#manage_partners').css('left',position.left-145+'px');
        $('#manage_partners').fadeIn();

        $(window).resize(function() {
          var position = $('a.manage_partners').position();
          $('#manage_partners').css('top',position.top-360+'px');
          $('#manage_partners').css('left',position.left-145+'px');
        });
      });

      $('a.close').click(function(){
        $(window).unbind('resize');
        $('#manage_partners').fadeOut();
      });

      //change value tumblr input value
      $('#tumblr').focusin(function(ev){
        if ($(this).attr('value')=="Please write Tumblr ID") {
          $(this).attr('value','');
        }
      });
      $('#tumblr').focusout(function(ev){
        if ($(this).attr('value')=="") {
          $(this).attr('value','Please write Tumblr ID');
        }
      });

      $('ul.themes li').click(function(ev){
        if (!$(this).hasClass('selected')) {
          $('ul.themes li').removeClass('selected');
          $(this).addClass('selected');
          $('#site_theme_id').val($(this).attr('rel'));
        }
      });

      $('#zoomIn,#zoomOut').click(function(ev){
        ev.stopPropagation();
        ev.preventDefault();
      });

      // CHANGE EACH LIST
      $('ul.list_combo_content').find('li.element').click(function(ev){
        var id = $(this).attr('id');
        var name = $(this).children('p.project_name').text();

        $('input#project_primary_organization_id').val($(this).attr('id'));
        $('div.list_combo').find('a.organization').text(name);
        $('div.list_combo').find('ul.list_combo_content').css('display','none');
        $('div.list_combo').children('span.combo_large').attr('id','hidden');
        $('div.list_combo').children('span.combo_large').removeClass('displayed');
      });

      if ($('a.combo').length > 0){
          $('a.combo').click(function(ev){
             if (!$(this).hasClass('clicked')){
                 $('a.combo.clicked').removeClass('clicked');
                 $(this).addClass('clicked');
             }
             $('input#site_project_classification').val($(this).attr('id'));
          });
      }

      //Map customization
      if ($('#site_overview_map_bbox_minx').attr('value')!='') {
        bounds_.extend(new google.maps.LatLng($('#site_overview_map_bbox_miny').attr('value'),$('#site_overview_map_bbox_minx').attr('value')));
        bounds_.extend(new google.maps.LatLng($('#site_overview_map_bbox_maxy').attr('value'),$('#site_overview_map_bbox_maxx').attr('value')));
      } else {
        bounds_.extend(new google.maps.LatLng(0,0));
      }

     var myOptions = {
        scrollwheel: false,
        mapTypeControl: false,
        streetViewControl: false,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        disableDefaultUI: true,
        zoom: 1,
        center: bounds_.getCenter()
      }
      map = new google.maps.Map(document.getElementById("map"), myOptions);

      var marker = new google.maps.Marker({
              position: new google.maps.LatLng($('#site_overview_map_bbox_miny').attr('value'),$('#site_overview_map_bbox_minx').attr('value')),
              map: map,
              title:"Hello World!"
          });
      var marker = new google.maps.Marker({
              position: new google.maps.LatLng($('#site_overview_map_bbox_maxy').attr('value'),$('#site_overview_map_bbox_maxx').attr('value')),
              map: map,
              title:"Hello World!"
          });

      google.maps.event.addListener(map, "tilesloaded", function(ev) {
        map.fitBounds(bounds_);
        zoomIn();
        google.maps.event.clearListeners(map,"tilesloaded");
        google.maps.event.addListener(map, "bounds_changed", function(ev) {
          var map_bounds = map.getBounds();
          $('#site_overview_map_bbox_maxy').attr('value',map_bounds.getNorthEast().lat());
          $('#site_overview_map_bbox_minx').attr('value',map_bounds.getNorthEast().lng());
          $('#site_overview_map_bbox_miny').attr('value',map_bounds.getSouthWest().lat());
          $('#site_overview_map_bbox_maxx').attr('value',map_bounds.getSouthWest().lng());
        });
      });
    });

    function hideAllMapCombos(){
     $('div.list_combo').find('ul.list_combo_content').css('display','none');
     $('div.list_combo').children('span.combo_large').attr('id','hidden');
     $('div.list_combo').children('span.combo_large').removeClass('displayed');
    }

    function zoomIn() {
      map.setZoom(map.getZoom() + 1);
    }

    function zoomOut() {
      map.setZoom(map.getZoom() - 1);
    }<commit_msg>changes<commit_after>  var map;
  var bounds_ = new google.maps.LatLngBounds();

    $(document).ready( function() {
      $('#site_show_blog').click(function(){
        if (!$('#tumbler_id').is(':visible')) {
          $(this).parent().addClass('selected');
        } else {
          $(this).parent().removeClass('selected');
        }
        $('#tumbler_id').toggle();
      });

      $('a.manage_partners').click(function(ev){
        ev.preventDefault();
        ev.stopPropagation();
        var position = $('a.manage_partners').position();
        $('#manage_partners').css('top',position.top-360+'px');
        $('#manage_partners').css('left',position.left-145+'px');
        $('#manage_partners').fadeIn();

        $(window).resize(function() {
          var position = $('a.manage_partners').position();
          $('#manage_partners').css('top',position.top-360+'px');
          $('#manage_partners').css('left',position.left-145+'px');
        });
      });

      $('a.close').click(function(){
        $(window).unbind('resize');
        $('#manage_partners').fadeOut();
      });

      //change value tumblr input value
      $('#tumblr').focusin(function(ev){
        if ($(this).attr('value')=="Please write Tumblr ID") {
          $(this).attr('value','');
        }
      });
      $('#tumblr').focusout(function(ev){
        if ($(this).attr('value')=="") {
          $(this).attr('value','Please write Tumblr ID');
        }
      });

      $('ul.themes li').click(function(ev){
        if (!$(this).hasClass('selected')) {
          $('ul.themes li').removeClass('selected');
          $(this).addClass('selected');
          $('#site_theme_id').val($(this).attr('rel'));
        }
      });

      $('#zoomIn,#zoomOut').click(function(ev){
        ev.stopPropagation();
        ev.preventDefault();
      });

      // CHANGE EACH LIST
      $('ul.list_combo_content').find('li.element').click(function(ev){
        var id = $(this).attr('id');
        var name = $(this).children('p.project_name').text();

        $('input#project_primary_organization_id').val($(this).attr('id'));
        $('div.list_combo').find('a.organization').text(name);
        $('div.list_combo').find('ul.list_combo_content').css('display','none');
        $('div.list_combo').children('span.combo_large').attr('id','hidden');
        $('div.list_combo').children('span.combo_large').removeClass('displayed');
      });

      if ($('a.combo').length > 0){
          $('a.combo').click(function(ev){
             if (!$(this).hasClass('clicked')){
                 $('a.combo.clicked').removeClass('clicked');
                 $(this).addClass('clicked');
             }
             $('input#site_project_classification').val($(this).attr('id'));
          });
      }

      //Map customization
      if ($('#site_overview_map_bbox_minx').attr('value')!='') {
        bounds_ = new google.maps.LatLngBounds(
          new google.maps.LatLng($('#site_overview_map_bbox_miny').attr('value'),$('#site_overview_map_bbox_maxx').attr('value')),
          new google.maps.LatLng($('#site_overview_map_bbox_maxy').attr('value'),$('#site_overview_map_bbox_minx').attr('value'))
          );
      } else {
        bounds_.extend(new google.maps.LatLng(0,0));
      }

     var myOptions = {
        scrollwheel: false,
        mapTypeControl: false,
        streetViewControl: false,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        disableDefaultUI: true,
        zoom: 1,
        center: bounds_.getCenter()
      }
      map = new google.maps.Map(document.getElementById("map"), myOptions);

      google.maps.event.addListener(map, "tilesloaded", function(ev) {
        map.fitBounds(bounds_);
        zoomIn();
        google.maps.event.clearListeners(map,"tilesloaded");
        google.maps.event.addListener(map, "bounds_changed", function(ev) {
          var map_bounds = map.getBounds();
          $('#site_overview_map_bbox_maxy').attr('value',map_bounds.getNorthEast().lat());
          $('#site_overview_map_bbox_minx').attr('value',map_bounds.getNorthEast().lng());
          $('#site_overview_map_bbox_miny').attr('value',map_bounds.getSouthWest().lat());
          $('#site_overview_map_bbox_maxx').attr('value',map_bounds.getSouthWest().lng());
        });
      });
    });

    function hideAllMapCombos(){
     $('div.list_combo').find('ul.list_combo_content').css('display','none');
     $('div.list_combo').children('span.combo_large').attr('id','hidden');
     $('div.list_combo').children('span.combo_large').removeClass('displayed');
    }

    function zoomIn() {
      map.setZoom(map.getZoom() + 1);
    }

    function zoomOut() {
      map.setZoom(map.getZoom() - 1);
    }<|endoftext|>


<commit_before>'use strict';

/*
  Handle communication between the server and the map.
  Load observations, allow users to view them, and allow them to add new ones themselves.
*/
function MapController($scope, $log, $http, $location, $injector, appConstants, teams, levels, observationTypes, mageLib, FeatureService, LocationService) {
  /* Some map defaults */
  $scope.center = { lat: 39.8282, lng: -98.5795 };
  $scope.marker = { lat: 39.8282, lng: -98.5795 };
  $scope.zoom = 4;
  $scope.points = [];
  $scope.multiMarkers = {};
  $scope.externalLayer = "";
  $scope.observation = {};
  $scope.newFeature = null;

  /* Data models for the settings */
  $scope.layers = [];
  $scope.baseLayers = [{name: "Open Street Map"}];
  $scope.featureLayers = [];
  $scope.imageryLayers = [];

  /* Booleans for the ng-show attribues on the panels, toggling these will show and hide the map panels (i.e. layers, observation, export). */
  $scope.showSettings = false;
  $scope.showGoToAddress = false;
  $scope.showRefresh = false;
  $scope.showLocations = false;
  $scope.showExport = false;
  $scope.showObservation = false;

  /* Observation related variables and enums */
  $scope.observationTab = 1;
  $scope.teams = teams; //the next few fill in the selects on the observation form
  $scope.levels = levels;
  $scope.observationTypes = observationTypes;
  $scope.team = teams[0]; // form items
  $scope.level = levels[0];
  $scope.observationType = observationTypes[0];
  $scope.unit = "";
  $scope.description = "";
  $scope.files = []; // pre upload
  $scope.attachments = []; // images loaded from the server
  $scope.progressVisible = false; // progress bar stuff
  $scope.progressVisible = 0;

  $scope.observation.attributes = {};
  $scope.observation.attributes.TEAM = teams[0];
  $scope.observation.attributes.LEVEL = levels[0];
  $scope.observation.attributes.TYPE = observationTypes[0];
  $scope.observation.attributes.UNIT = "";
  $scope.observation.attributes.DESCRIPTION = "";

  /* Uncomment the following block to enable the Tomnod layers */
  /*$scope.imageryLayers = [{
    id: 1,
    name: "Digital Globe St Louis Map",
    enabled: false,
    external: true,
    type: "imagery",
    url: "http://sip.tomnod.com/sip/c6754f6173d059ac82729b6243148a08/256/{z}/{x}/{y}.png",
    tms: false
  },{
    id: 2,
    name: "Digital Globe Oklahoma Map",
    enabled: false,
    external: true,
    type: "imagery",
    url: "http://s3.amazonaws.com/explorationlab/oktornado1/{z}/{x}/{y}.png",
    tms: true
  }];
  $scope.featureLayers = [{
    id: 3,
    name: "St Louis Crowd Rank",
    enabled: false,
    external: true,
    url: "https://mapperdev.tomnod.com/nod/api/stlouistornado/sage"
  },{
    id: 4,
    name: "Oklahoma Crowd Rank",
    enabled: false,
    external: true,
    url: "https://mapperdev.tomnod.com/nod/api/oktornado2/sage"
  }];*/

  $scope.currentLayerId = 0;
  $scope.baseLayer = $scope.baseLayers[0];

  // Should move the call to navigator out to mageLib, hand back the location then $scope.apply the results
  $scope.geolocate = function () {
    console.log("in geolocate");
    if ($window.navigator.geolocation) {
      $window.navigator.geolocation.getCurrentPosition(function(position) {
        console.log("lat " + position.coords.latitude + " lon " + position.coords.longitude);
        $scope.$apply(function() {
          $scope.zoom = 12;
          $scope.center = { lat: position.coords.latitude, lng: position.coords.longitude };
        });
      });
    }
  }

  // call back for the geolocation ***REMOVED***
  $scope.setCurrentLocation = function (position) {
    $scope.$apply(function() {
      $scope.zoom = 12;
      $scope.center = { lat: position.coords.latitude, lng: position.coords.longitude };
    });
  }

  $scope.getGeolocation = function () {
    mageLib.geolocate($scope.setCurrentLocation, mageLib.geoError, {});
  }

  $scope.$watch("observation", function (oldValue, newValue) {
    console.log("Observation changed " + JSON.stringify($scope.observation));
    if ($scope.observation.feature && $scope.observation.feature.properties.OBJECTID > 0) { //open existing observation
      FeatureService.getFeature($scope.observation.layer.id, $scope.observation.feature.properties.OBJECTID).
        success(function (data) {
          $scope.observation = data;
          /* Since the angular select directive works off of reference, set the TEAM, LEVEL, and TYPE attributes to theie
              corresponsing values from their respective arrays */
          $scope.observation.attributes.TEAM = _.find($scope.teams, function (t) {
                if(t.name == $scope.observation.attributes.TEAM) {
                  return t;
                }
              });
          $scope.observation.attributes.LEVEL = _.find($scope.levels, function (l) {
                if (l.color == $scope.observation.attributes.EVENTLEVEL){
                  return l;
                }
              });
          $scope.observation.attributes.TYPE = _.find($scope.observationTypes, function (o) {
                if (o.title == $scope.observation.attributes.TYPE){
                  return o;
                }
              });
          $scope.showObservation = true;
        }).
        error(function (data, status, headers, config) {
          // if the user does not have permissions to get layers, re-route them to the signin page
          if (status > 399 && status < 500) {
            $location.path('/');
          }
        });
    } else {

    }
  }, true);

  $scope.getFeatureLayers = function () {
    $http.get(appConstants.rootUrl + '/FeatureServer/', {params: mageLib.getTokenParams()}).
      success(function (data, status, headers, config) {
          console.log('got layers');
          $scope.layers = data.layers;

          _.each(data.layers, function(layer) {
            $scope.featureLayers.unshift({
              id: layer.id,
              name: layer.name,
              url: appConstants.rootUrl + "/FeatureServer/" + layer.id + "/features/?properties=OBJECTID&access_token=" + mageLib.getLocalItem('token'),
              enabled: false
            });
          });

          $scope.showSettings = true;
      }).
      error(function (data, status, headers, config) {
          $log.log("Error getting layers: " + status);
          if (status > 399) {
            $location.path('/');
          }
      });
  }

  $scope.onFeatureLayer = function(layer) {
    if (!layer.checked) {
      $scope.layer = layer;
      return;
    };

    var options = {
      method: "GET",
      url: layer.url,
      headers: {
        "Accepts": "application/json",
        "Content-Type": "application/json"
      }
    }

    if (layer.external) {
      options.method = "JSONP",
      options.params = {
         "callback": "JSON_CALLBACK"
      }
    }

    $http(options)
      .success(function(data, status, headers, config) {
        console.log('got points');
        layer.featureCollection = data;
        $scope.layer = layer;
      })
      .error(function(data, status, headers, config) {
        console.log("Error getting features for layer 'layer.name' : " + status);
      });
  }

  $scope.onImageryLayer = function(layer) {
    $scope.layer = layer;
  }

  /* Settings aka layer panel funcitons */
  $scope.openSettings = function () {
    console.log("in open settings");
    $scope.showSettings = true;
  }

  $scope.closeSettings = function () {
    console.log("closing settings");
    $scope.showSettings = false;
  }

  $scope.saveSettings = function () {
    console.log("saving settings");
    $scope.showSettings = false;
  }

  /*
    The observation functions are a mix of copy pasta from the observation directive, hopefully cleaned up a bit
    and using the FeatureService. May need to be cleaned up after PDC.
  */
  $scope.newObservation = function () {
    console.log("in new observation");
    $scope.showObservation = true;
    $scope.observation = {feature: { properties: {OBJECTID: -1}}};
  }

  $scope.cancelObservation = function () {
    $scope.showObservation = false;
    $scope.observation = {feature: { properties: {OBJECTID: -1}}};
  }

  $scope.saveObservation = function () {
    // Build the observation object
    var operation = "";

    $scope.observation.attributes.EVENTDATE = new Date().getTime();
    $scope.observation.geometry = {
        "x": $scope.marker.lng,
        "y": $scope.marker.lat
      };

    /* check to see if this is this an update or a new observation, if its an update, set the location and ID */
    if ($scope.observation.feature.properties.OBJECTID > 0) {
      operation = "updateFeatures";
      obs.attributes.OBJECTID = $scope.observation.feature.properties.OBJECTID;
      // may not need to do this
      obs.geometry.x = $scope.observation.geometry.coordinates[0];
      obs.geometry.x = $scope.observation.geometry.coordinates[1];
    } else {
      operation = "addFeatures";
    }

    // make a call to the FeatureService
    FeatureService.saveFeature($scope.currentLayerId, $scope.observation, operation)
      .success(function (data) {
        console.log('observation created');
        var objectId = data.addResults ? data.addResults[0].objectId : data.updateResults[0].objectId;
        $scope.showObservation = false;

        if (operation == "addFeatures") {
          $scope.newFeature = {
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: [$scope.marker.lng, $scope.marker.lat]
            },
            properties: {
              OBJECTID: objectId
            }
          }
        }

        if ($scope.files.length > 0) {
            $scope.fileUploadUrl = appConstants.rootUrl + '/FeatureServer/' + $scope.currentLayerId + '/' + objectId + '/addAttachment&access_token=' + mageLib.getLocalItem('token');
            $scope.uploadFile();
          }
      });
  }

  /* Attachment upload functions, some of these make more sense in the FeatureService...more copy pasta */
  $scope.setFiles = function (element) {
    $scope.$apply(function(scope) {
      console.log('files:', element.files);
      // Turn the FileList object into an Array
      $scope.files = []
      for (var i = 0; i < element.files.length; i++) {
        $scope.files.push(element.files[i])
      }
      $scope.progressVisible = false
    });
  },

  $scope.uploadFile = function() {
    var fd = new FormData()
    for (var i in $scope.files) {
      fd.append("attachment", $scope.files[i])
    }
    var xhr = new XMLHttpRequest()
    xhr.upload.addEventListener("progress", uploadProgress, false)
    xhr.addEventListener("load", uploadComplete, false)
    xhr.addEventListener("error", uploadFailed, false)
    xhr.addEventListener("abort", uploadCanceled, false)
    xhr.open("POST", $scope.fileUploadUrl)
    $scope.progressVisible = true
    xhr.send(fd)
  }

  function uploadProgress(evt) {
    $scope.$apply(function(){
      if (evt.lengthComputable) {
        $scope.progress = Math.round(evt.loaded * 100 / evt.total)
      } else {
        $scope.progress = 'unable to compute'
      }
    });
  }

  function uploadComplete(evt) {
    $scope.files = [];
    $scope.progressVisible = false
  }

  function uploadFailed(evt) {
    alert("There was an error attempting to upload the file.")
  }

  function uploadCanceled(evt) {
    $scope.$apply(function(){
        $scope.progressVisible = false
    })
    alert("The upload has been canceled by the user or the browser dropped the connection.")
  }

  /* Goto address, need to implement some geocoding like the android app does, otherwise pull it out for PDC. */
  $scope.openGotoAddress = function () {
    console.log("in goto address");
    $scope.showGoToAddress = true;
  }

  $scope.dismissGotoAddress = function () {
    console.log("in goto address");
    $scope.showGoToAddress = false;
  }

  /* Need to sort out how this works with the GeoJSON layers... */
  $scope.refreshPoints = function () {
    // TODO refresh point for selected layers
  //   console.log("in refresh points");
  //   $('#refresh-panel').removeCl***REMOVED***('hide');
  //   $scope.multiMarkers = {};

  //   $http.get(appConstants.rootUrl + '/FeatureServer/' + $scope.currentLayerId + '/query?outFields=OBJECTID').
  //       success(function (data, status, headers, config) {
  //           console.log('got points');
  //           $scope.points = data.features;
  //           var markers = {};
  //           for (var i = 0; i <  $scope.points.length; i++) {
  //             console.log($scope.points[i].geometry.x + ", " + $scope.points[i].geometry.y);
  //             markers[$scope.points[i].attributes.OBJECTID] = {lat: $scope.points[i].geometry.y, lng: $scope.points[i].geometry.x,draggable: false, id: $scope.points[i].attributes.OBJECTID};
  //           }
  //           $scope.multiMarkers = markers;
  //       }).
  //       error(function (data, status, headers, config) {
  //           $log.log("Error getting layers: " + status);
  //       });

  //   $('#refresh-panel').addCl***REMOVED***('hide');
  }

  $scope.dismissRefresh = function () {
    console.log("in refresh points");
    $scope.showRefresh = false;
  }

  /* Locations == FFT */
  $scope.openLocations = function() {
    console.log("in showLocations");
    $scope.showLocations = true;
  }

  $scope.dismissLocations = function() {
    console.log("in dismissLocations");
    $scope.showLocations = false;
  }

  /* Open and close the export dialog, and handle making the call to get the KML file. */
  $scope.openExport = function () {
    console.log("opening export");
    $scope.showExport = true;
  }

  $scope.closeExport = function () {
    console.log("closing export panel");
    $scope.showExport = false;
  }

  $scope.export = function () {
    console.log("exporting features to KML 1");
    window.location.href = appConstants.rootUrl + "/api/locations/export?&access_token=" + mageLib.getLocalItem('token');
    console.log("exporting features to KML 2");
    $scope.closeExport();
  }

  // Calls to make when the page is loaded
  $scope.getFeatureLayers();
  //$scope.geolocate(); // this makes angular upset because there are 2 scope.applys running at the same time...

}
<commit_msg>derp, accidentally overwrote the if for file upload...forreals<commit_after>'use strict';

/*
  Handle communication between the server and the map.
  Load observations, allow users to view them, and allow them to add new ones themselves.
*/
function MapController($scope, $log, $http, $location, $injector, appConstants, teams, levels, observationTypes, mageLib, FeatureService, LocationService) {
  /* Some map defaults */
  $scope.center = { lat: 39.8282, lng: -98.5795 };
  $scope.marker = { lat: 39.8282, lng: -98.5795 };
  $scope.zoom = 4;
  $scope.points = [];
  $scope.multiMarkers = {};
  $scope.externalLayer = "";
  $scope.observation = {};
  $scope.newFeature = null;

  /* Data models for the settings */
  $scope.layers = [];
  $scope.baseLayers = [{name: "Open Street Map"}];
  $scope.featureLayers = [];
  $scope.imageryLayers = [];

  /* Booleans for the ng-show attribues on the panels, toggling these will show and hide the map panels (i.e. layers, observation, export). */
  $scope.showSettings = false;
  $scope.showGoToAddress = false;
  $scope.showRefresh = false;
  $scope.showLocations = false;
  $scope.showExport = false;
  $scope.showObservation = false;

  /* Observation related variables and enums */
  $scope.observationTab = 1;
  $scope.teams = teams; //the next few fill in the selects on the observation form
  $scope.levels = levels;
  $scope.observationTypes = observationTypes;
  $scope.team = teams[0]; // form items
  $scope.level = levels[0];
  $scope.observationType = observationTypes[0];
  $scope.unit = "";
  $scope.description = "";
  $scope.files = []; // pre upload
  $scope.attachments = []; // images loaded from the server
  $scope.progressVisible = false; // progress bar stuff
  $scope.progressVisible = 0;

  $scope.observation.attributes = {};
  $scope.observation.attributes.TEAM = teams[0];
  $scope.observation.attributes.LEVEL = levels[0];
  $scope.observation.attributes.TYPE = observationTypes[0];
  $scope.observation.attributes.UNIT = "";
  $scope.observation.attributes.DESCRIPTION = "";

  /* Uncomment the following block to enable the Tomnod layers */
  /*$scope.imageryLayers = [{
    id: 1,
    name: "Digital Globe St Louis Map",
    enabled: false,
    external: true,
    type: "imagery",
    url: "http://sip.tomnod.com/sip/c6754f6173d059ac82729b6243148a08/256/{z}/{x}/{y}.png",
    tms: false
  },{
    id: 2,
    name: "Digital Globe Oklahoma Map",
    enabled: false,
    external: true,
    type: "imagery",
    url: "http://s3.amazonaws.com/explorationlab/oktornado1/{z}/{x}/{y}.png",
    tms: true
  }];
  $scope.featureLayers = [{
    id: 3,
    name: "St Louis Crowd Rank",
    enabled: false,
    external: true,
    url: "https://mapperdev.tomnod.com/nod/api/stlouistornado/sage"
  },{
    id: 4,
    name: "Oklahoma Crowd Rank",
    enabled: false,
    external: true,
    url: "https://mapperdev.tomnod.com/nod/api/oktornado2/sage"
  }];*/

  $scope.currentLayerId = 0;
  $scope.baseLayer = $scope.baseLayers[0];

  // Should move the call to navigator out to mageLib, hand back the location then $scope.apply the results
  $scope.geolocate = function () {
    console.log("in geolocate");
    if ($window.navigator.geolocation) {
      $window.navigator.geolocation.getCurrentPosition(function(position) {
        console.log("lat " + position.coords.latitude + " lon " + position.coords.longitude);
        $scope.$apply(function() {
          $scope.zoom = 12;
          $scope.center = { lat: position.coords.latitude, lng: position.coords.longitude };
        });
      });
    }
  }

  // call back for the geolocation ***REMOVED***
  $scope.setCurrentLocation = function (position) {
    $scope.$apply(function() {
      $scope.zoom = 12;
      $scope.center = { lat: position.coords.latitude, lng: position.coords.longitude };
    });
  }

  $scope.getGeolocation = function () {
    mageLib.geolocate($scope.setCurrentLocation, mageLib.geoError, {});
  }

  $scope.$watch("observation", function (oldValue, newValue) {
    console.log("Observation changed " + JSON.stringify($scope.observation));
    if ($scope.observation.feature && $scope.observation.feature.properties.OBJECTID > 0) { //open existing observation
      FeatureService.getFeature($scope.observation.layer.id, $scope.observation.feature.properties.OBJECTID).
        success(function (data) {
          $scope.observation = data;
          /* Since the angular select directive works off of reference, set the TEAM, LEVEL, and TYPE attributes to theie
              corresponsing values from their respective arrays */
          $scope.observation.attributes.TEAM = _.find($scope.teams, function (t) {
                if(t.name == $scope.observation.attributes.TEAM) {
                  return t;
                }
              });
          $scope.observation.attributes.LEVEL = _.find($scope.levels, function (l) {
                if (l.color == $scope.observation.attributes.EVENTLEVEL){
                  return l;
                }
              });
          $scope.observation.attributes.TYPE = _.find($scope.observationTypes, function (o) {
                if (o.title == $scope.observation.attributes.TYPE){
                  return o;
                }
              });
          $scope.showObservation = true;
        }).
        error(function (data, status, headers, config) {
          // if the user does not have permissions to get layers, re-route them to the signin page
          if (status > 399 && status < 500) {
            $location.path('/');
          }
        });
    } else {

    }
  }, true);

  $scope.getFeatureLayers = function () {
    $http.get(appConstants.rootUrl + '/FeatureServer/', {params: mageLib.getTokenParams()}).
      success(function (data, status, headers, config) {
          console.log('got layers');
          $scope.layers = data.layers;

          _.each(data.layers, function(layer) {
            $scope.featureLayers.unshift({
              id: layer.id,
              name: layer.name,
              url: appConstants.rootUrl + "/FeatureServer/" + layer.id + "/features/?properties=OBJECTID&access_token=" + mageLib.getLocalItem('token'),
              enabled: false
            });
          });

          $scope.showSettings = true;
      }).
      error(function (data, status, headers, config) {
          $log.log("Error getting layers: " + status);
          if (status > 399) {
            $location.path('/');
          }
      });
  }

  $scope.onFeatureLayer = function(layer) {
    if (!layer.checked) {
      $scope.layer = layer;
      return;
    };

    var options = {
      method: "GET",
      url: layer.url,
      headers: {
        "Accepts": "application/json",
        "Content-Type": "application/json"
      }
    }

    if (layer.external) {
      options.method = "JSONP",
      options.params = {
         "callback": "JSON_CALLBACK"
      }
    }

    $http(options)
      .success(function(data, status, headers, config) {
        console.log('got points');
        layer.featureCollection = data;
        $scope.layer = layer;
      })
      .error(function(data, status, headers, config) {
        console.log("Error getting features for layer 'layer.name' : " + status);
      });
  }

  $scope.onImageryLayer = function(layer) {
    $scope.layer = layer;
  }

  /* Settings aka layer panel funcitons */
  $scope.openSettings = function () {
    console.log("in open settings");
    $scope.showSettings = true;
  }

  $scope.closeSettings = function () {
    console.log("closing settings");
    $scope.showSettings = false;
  }

  $scope.saveSettings = function () {
    console.log("saving settings");
    $scope.showSettings = false;
  }

  /*
    The observation functions are a mix of copy pasta from the observation directive, hopefully cleaned up a bit
    and using the FeatureService. May need to be cleaned up after PDC.
  */
  $scope.newObservation = function () {
    console.log("in new observation");
    $scope.showObservation = true;
    $scope.observation = {feature: { properties: {OBJECTID: -1}}};
  }

  $scope.cancelObservation = function () {
    $scope.showObservation = false;
    $scope.observation = {feature: { properties: {OBJECTID: -1}}};
  }

  $scope.saveObservation = function () {
    // Build the observation object
    var operation = "";

    $scope.observation.attributes.EVENTDATE = new Date().getTime();
    $scope.observation.geometry = {
        "x": $scope.marker.lng,
        "y": $scope.marker.lat
      };

    /* check to see if this is this an update or a new observation, if its an update, set the location and ID */
    if ($scope.observation.feature.properties.OBJECTID > 0) {
      operation = "updateFeatures";
      obs.attributes.OBJECTID = $scope.observation.feature.properties.OBJECTID;
      // may not need to do this
      obs.geometry.x = $scope.observation.geometry.coordinates[0];
      obs.geometry.x = $scope.observation.geometry.coordinates[1];
    } else {
      operation = "addFeatures";
    }

    // make a call to the FeatureService
    FeatureService.saveFeature($scope.currentLayerId, $scope.observation, operation)
      .success(function (data) {
        console.log('observation created');
        var objectId = data.addResults ? data.addResults[0].objectId : data.updateResults[0].objectId;
        $scope.showObservation = false;

        if (operation == "addFeatures") {
          $scope.newFeature = {
            type: "Feature",
            geometry: {
              type: "Point",
              coordinates: [$scope.marker.lng, $scope.marker.lat]
            },
            properties: {
              OBJECTID: objectId
            }
          }
        }

        if ($scope.files.length > 0) {
            $scope.fileUploadUrl = appConstants.rootUrl + '/FeatureServer/' + $scope.currentLayerId + '/' + objectId + '/addAttachment?access_token=' + mageLib.getLocalItem('token');
            $scope.uploadFile();
          }
      });
  }

  /* Attachment upload functions, some of these make more sense in the FeatureService...more copy pasta */
  $scope.setFiles = function (element) {
    $scope.$apply(function(scope) {
      console.log('files:', element.files);
      // Turn the FileList object into an Array
      $scope.files = []
      for (var i = 0; i < element.files.length; i++) {
        $scope.files.push(element.files[i])
      }
      $scope.progressVisible = false
    });
  },

  $scope.uploadFile = function() {
    var fd = new FormData()
    for (var i in $scope.files) {
      fd.append("attachment", $scope.files[i])
    }
    var xhr = new XMLHttpRequest()
    xhr.upload.addEventListener("progress", uploadProgress, false)
    xhr.addEventListener("load", uploadComplete, false)
    xhr.addEventListener("error", uploadFailed, false)
    xhr.addEventListener("abort", uploadCanceled, false)
    xhr.open("POST", $scope.fileUploadUrl)
    $scope.progressVisible = true
    xhr.send(fd)
  }

  function uploadProgress(evt) {
    $scope.$apply(function(){
      if (evt.lengthComputable) {
        $scope.progress = Math.round(evt.loaded * 100 / evt.total)
      } else {
        $scope.progress = 'unable to compute'
      }
    });
  }

  function uploadComplete(evt) {
    $scope.files = [];
    $scope.progressVisible = false
  }

  function uploadFailed(evt) {
    alert("There was an error attempting to upload the file.")
  }

  function uploadCanceled(evt) {
    $scope.$apply(function(){
        $scope.progressVisible = false
    })
    alert("The upload has been canceled by the user or the browser dropped the connection.")
  }

  /* Goto address, need to implement some geocoding like the android app does, otherwise pull it out for PDC. */
  $scope.openGotoAddress = function () {
    console.log("in goto address");
    $scope.showGoToAddress = true;
  }

  $scope.dismissGotoAddress = function () {
    console.log("in goto address");
    $scope.showGoToAddress = false;
  }

  /* Need to sort out how this works with the GeoJSON layers... */
  $scope.refreshPoints = function () {
    // TODO refresh point for selected layers
  //   console.log("in refresh points");
  //   $('#refresh-panel').removeCl***REMOVED***('hide');
  //   $scope.multiMarkers = {};

  //   $http.get(appConstants.rootUrl + '/FeatureServer/' + $scope.currentLayerId + '/query?outFields=OBJECTID').
  //       success(function (data, status, headers, config) {
  //           console.log('got points');
  //           $scope.points = data.features;
  //           var markers = {};
  //           for (var i = 0; i <  $scope.points.length; i++) {
  //             console.log($scope.points[i].geometry.x + ", " + $scope.points[i].geometry.y);
  //             markers[$scope.points[i].attributes.OBJECTID] = {lat: $scope.points[i].geometry.y, lng: $scope.points[i].geometry.x,draggable: false, id: $scope.points[i].attributes.OBJECTID};
  //           }
  //           $scope.multiMarkers = markers;
  //       }).
  //       error(function (data, status, headers, config) {
  //           $log.log("Error getting layers: " + status);
  //       });

  //   $('#refresh-panel').addCl***REMOVED***('hide');
  }

  $scope.dismissRefresh = function () {
    console.log("in refresh points");
    $scope.showRefresh = false;
  }

  /* Locations == FFT */
  $scope.openLocations = function() {
    console.log("in showLocations");
    $scope.showLocations = true;
  }

  $scope.dismissLocations = function() {
    console.log("in dismissLocations");
    $scope.showLocations = false;
  }

  /* Open and close the export dialog, and handle making the call to get the KML file. */
  $scope.openExport = function () {
    console.log("opening export");
    $scope.showExport = true;
  }

  $scope.closeExport = function () {
    console.log("closing export panel");
    $scope.showExport = false;
  }

  $scope.export = function () {
    console.log("exporting features to KML 1");
    window.location.href = appConstants.rootUrl + "/api/locations/export?&access_token=" + mageLib.getLocalItem('token');
    console.log("exporting features to KML 2");
    $scope.closeExport();
  }

  // Calls to make when the page is loaded
  $scope.getFeatureLayers();
  //$scope.geolocate(); // this makes angular upset because there are 2 scope.applys running at the same time...

}
<|endoftext|>


<commit_before>chorus.views.UserShowSidebar = chorus.views.Sidebar.extend({
    className:"user_show_sidebar",
    entityType:"user",
    subviews:{
        '.activities':'activityList'
    },

    setup:function () {
        this.collection = this.model.activities();
        this.collection.fetch();
        this.collection.bind("changed", this.render, this);
        this.activityList = new chorus.views.ActivityList({ collection:this.collection, headingText:t("user.activity"), additionalClass:"sidebar" });

        this.config = chorus.models.Config.instance();
        this.requiredResources.push(this.config);
    },

    additionalContext:function () {
        var userIsLoggedIn = this.model.get("userName") == chorus.session.user().get("userName");
        var userIsAdmin = chorus.session.user().get("admin");
        return {
            permission: userIsLoggedIn || userIsAdmin,
            changePasswordAvailable: userIsLoggedIn && !this.config.isExternalAuth()
        }
    }
});

chorus.alerts.UserDelete = chorus.alerts.Base.extend({
})<commit_msg>remove spurious double definition of User delete alert in user_show_sidebar.js<commit_after>chorus.views.UserShowSidebar = chorus.views.Sidebar.extend({
    className:"user_show_sidebar",
    entityType:"user",
    subviews:{
        '.activities':'activityList'
    },

    setup:function () {
        this.collection = this.model.activities();
        this.collection.fetch();
        this.collection.bind("changed", this.render, this);
        this.activityList = new chorus.views.ActivityList({ collection:this.collection, headingText:t("user.activity"), additionalClass:"sidebar" });

        this.config = chorus.models.Config.instance();
        this.requiredResources.push(this.config);
    },

    additionalContext:function () {
        var userIsLoggedIn = this.model.get("userName") == chorus.session.user().get("userName");
        var userIsAdmin = chorus.session.user().get("admin");
        return {
            permission: userIsLoggedIn || userIsAdmin,
            changePasswordAvailable: userIsLoggedIn && !this.config.isExternalAuth()
        }
    }
});
<|endoftext|>


<commit_before>(function () {
    var cohortVisualization = angular.module('redash.visualization');

    cohortVisualization.config(['VisualizationProvider', function(VisualizationProvider) {

      var editTemplate = '<cohort-editor></cohort-editor>';
        var defaultOptions = {
          'timeInterval': 'daily'
        };

        VisualizationProvider.registerVisualization({
            type: 'COHORT',
            name: 'Cohort',
            renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>',
            editorTemplate: editTemplate,
            defaultOptions: defaultOptions
        });
    }]);

    cohortVisualization.directive('cohortRenderer', function() {
        return {
            restrict: 'E',
            scope: {
                queryResult: '='
            },
            template: "",
            replace: false,
            link: function($scope, element, attrs) {
                $scope.$watch('[queryResult && queryResult.getData(), visualization.options.timeInterval ]', function () {
                    if ($scope.queryResult.getData() == null) {

                    } else {
                        var sortedData = _.sortBy($scope.queryResult.getData(),function(r) {
                          return r['date'] + r['day_number']  ;
                        });

                        var grouped = _.groupBy(sortedData, "date");
                        var maxColumns = _.reduce(grouped, function(memo, data){
                            return (data.length > memo)? data.length : memo;
                        }, 0);
                        var data = _.map(grouped, function(values, date) {
                           var row = [values[0].total];
                            _.each(values, function(value) { row.push(value.value); });
                            _.each(_.range(values.length, maxColumns), function() { row.push(null); });
                            return row;
                        });

                        var initialDate = moment(sortedData[0].date).toDate(),
                            container = angular.element(element)[0];

                        $scope.visualization.options.timeInterval = $scope.visualization.options.timeInterval || 'daily';

                        var timeLabels = {'daily': 'Day', 'weekly': 'Week', 'monthly': 'Month'};
                        var timeLabel = timeLabels[$scope.visualization.options.timeInterval];

                        Cornelius.draw({
                            initialDate: initialDate,
                            container: container,
                            cohort: data,
                            title: null,
                            timeInterval: $scope.visualization.options.timeInterval,
                            labels: {
                                time: 'Activation ' + timeLabel,
                                people: 'Users'
                            },
                            formatHeaderLabel: function (i) {
                                return  timeLabel + (i - 1);
                            }
                        });
                    }
                });
            }
        }
    });
    cohortVisualization.directive('cohortEditor', function() {
        return {
          restrict: 'E',
          templateUrl: '/views/visualizations/cohort_editor.html'
        }
    });

}());
<commit_msg>Fix: cohort visulization had infinte digest loop<commit_after>(function () {
  var cohortVisualization = angular.module('redash.visualization');

  cohortVisualization.config(['VisualizationProvider', function (VisualizationProvider) {

    var editTemplate = '<cohort-editor></cohort-editor>';
    var defaultOptions = {
      'timeInterval': 'daily'
    };

    VisualizationProvider.registerVisualization({
      type: 'COHORT',
      name: 'Cohort',
      renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>',
      editorTemplate: editTemplate,
      defaultOptions: defaultOptions
    });
  }]);

  cohortVisualization.directive('cohortRenderer', function () {
    return {
      restrict: 'E',
      scope: {
        queryResult: '=',
        options: '='
      },
      template: "",
      replace: false,
      link: function ($scope, element, attrs) {
        $scope.options.timeInterval = $scope.options.timeInterval || 'daily';

        var updateCohort = function () {
          if ($scope.queryResult.getData() === null) {
            return;
          }

          var sortedData = _.sortBy($scope.queryResult.getData(), function (r) {
            return r['date'] + r['day_number'];
          });

          var grouped = _.groupBy(sortedData, "date");

          var maxColumns = _.reduce(grouped, function (memo, data) {
            return (data.length > memo) ? data.length : memo;
          }, 0);

          var data = _.map(grouped, function (values, date) {
            var row = [values[0].total];
            _.each(values, function (value) {
              row.push(value.value);
            });
            _.each(_.range(values.length, maxColumns), function () {
              row.push(null);
            });
            return row;
          });

          var initialDate = moment(sortedData[0].date).toDate(),
            container = angular.element(element)[0];

          Cornelius.draw({
            initialDate: initialDate,
            container: container,
            cohort: data,
            title: null,
            timeInterval: $scope.options.timeInterval,
            labels: {
              time: 'Time',
              people: 'Users',
              weekOf: 'Week of'
            }
          });
        }

        $scope.$watch('queryResult && queryResult.getData()', updateCohort);
        $scope.$watch('options.timeInterval', updateCohort);
      }
    }
  });
  cohortVisualization.directive('cohortEditor', function () {
    return {
      restrict: 'E',
      templateUrl: '/views/visualizations/cohort_editor.html'
    }
  });

}());
<|endoftext|>


<commit_before>/* @flow */

import Bourne from '@hapi/bourne';

import { reportError } from './helpers';

/**
 * A list if keys to ignore when parsing.
 * @type {string[]}
 */
const blacklist = [ '__proto__', 'constructor', 'prototype' ];

/**
 * Parses the query/search or fragment/hash parameters out of a specific URL and
 * returns them as a JS object.
 *
 * @param {URL} url - The URL to parse.
 * @param {boolean} dontParse - If falsy, some transformations (for parsing the
 * value as JSON) will be executed.
 * @param {string} source - If {@code 'search'}, the parameters will parsed out
 * of {@code url.search}; otherwise, out of {@code url.hash}.
 * @returns {Object}
 */
export function parseURLParams(
        url: URL,
        dontParse: boolean = false,
        source: string = 'hash'): Object {
    const paramStr = source === 'search' ? url.search : url.hash;
    const params = {};
    const paramParts = (paramStr && paramStr.substr(1).split('&')) || [];

    // Detect and ignore hash params for hash routers.
    if (source === 'hash' && paramParts.length === 1) {
        const firstParam = paramParts[0];

        if (firstParam.startsWith('/') && firstParam.split('&').length === 1) {
            return params;
        }
    }

    paramParts.forEach(part => {
        const param = part.split('=');
        const key = param[0];

        if (!key || blacklist.includes(key.split('.')[0])) {
            return;
        }

        let value;

        try {
            value = param[1];

            if (!dontParse) {
                const decoded = decodeURIComponent(value).replace(/\\&/, '&');

                value = decoded === 'undefined' ? undefined : Bourne.parse(decoded);
            }
        } catch (e) {
            reportError(
                e, `Failed to parse URL parameter value: ${String(value)}`);

            return;
        }
        params[key] = value;
    });

    return params;
}
<commit_msg>fix: Checks all parts while parsing config params.<commit_after>/* @flow */

import Bourne from '@hapi/bourne';

import { reportError } from './helpers';

/**
 * A list if keys to ignore when parsing.
 * @type {string[]}
 */
const blacklist = [ '__proto__', 'constructor', 'prototype' ];

/**
 * Parses the query/search or fragment/hash parameters out of a specific URL and
 * returns them as a JS object.
 *
 * @param {URL} url - The URL to parse.
 * @param {boolean} dontParse - If falsy, some transformations (for parsing the
 * value as JSON) will be executed.
 * @param {string} source - If {@code 'search'}, the parameters will parsed out
 * of {@code url.search}; otherwise, out of {@code url.hash}.
 * @returns {Object}
 */
export function parseURLParams(
        url: URL,
        dontParse: boolean = false,
        source: string = 'hash'): Object {
    const paramStr = source === 'search' ? url.search : url.hash;
    const params = {};
    const paramParts = (paramStr && paramStr.substr(1).split('&')) || [];

    // Detect and ignore hash params for hash routers.
    if (source === 'hash' && paramParts.length === 1) {
        const firstParam = paramParts[0];

        if (firstParam.startsWith('/') && firstParam.split('&').length === 1) {
            return params;
        }
    }

    paramParts.forEach(part => {
        const param = part.split('=');
        const key = param[0];

        if (!key || key.split('.').some(k => blacklist.includes(k))) {
            return;
        }

        let value;

        try {
            value = param[1];

            if (!dontParse) {
                const decoded = decodeURIComponent(value).replace(/\\&/, '&');

                value = decoded === 'undefined' ? undefined : Bourne.parse(decoded);
            }
        } catch (e) {
            reportError(
                e, `Failed to parse URL parameter value: ${String(value)}`);

            return;
        }
        params[key] = value;
    });

    return params;
}
<|endoftext|>


<commit_before>/* globals APP, interfaceConfig */

import { openDialog } from '../base/dialog';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { API_ID } from '../../../modules/API/constants';
import {
    setAudioInputDevice,
    setAudioOutputDevice,
    setVideoInputDevice
} from '../base/devices';
import { i18next } from '../base/i18n';
import {
    PostMessageTransportBackend,
    Transport
} from '../../../modules/transport';

import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { DeviceSelectionDialog } from './components';

/**
 * Open DeviceSelectionDialog with a configuration based on the environment's
 * supported abilities.
 *
 * @returns {Function}
 */
export function openDeviceSelectionDialog() {
    return dispatch => {
        if (interfaceConfig.filmStripOnly) {
            dispatch(_openDeviceSelectionDialogInPopup());
        } else {
            dispatch(_openDeviceSelectionDialogHere());
        }
    };
}

/**
 * Opens the DeviceSelectionDialog in the same window.
 *
 * @returns {Function}
 */
function _openDeviceSelectionDialogHere() {
    return dispatch =>
        JitsiMeetJS.mediaDevices.isDeviceListAvailable()
            .then(isDeviceListAvailable => {
                dispatch(openDialog(DeviceSelectionDialog, {
                    currentAudioInputId: APP.settings.getMicDeviceId(),
                    currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
                    currentVideoInputId: APP.settings.getCameraDeviceId(),
                    disableAudioInputChange:
                        !JitsiMeetJS.isMultipleAudioInputSupported(),
                    disableDeviceChange: !isDeviceListAvailable
                        || !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
                    hasAudioPermission: JitsiMeetJS.mediaDevices
                        .isDevicePermissionGranted('audio'),
                    hasVideoPermission: JitsiMeetJS.mediaDevices
                        .isDevicePermissionGranted('video'),
                    hideAudioInputPreview:
                        !JitsiMeetJS.isCollectingLocalStats(),
                    hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
                        .isDeviceChangeAvailable('output')
                }));
            });
}

/**
 * Opens a popup window with the device selection dialog in it.
 *
 * @returns {Function}
 */
function _openDeviceSelectionDialogInPopup() {
    return (dispatch, getState) => {
        const { popupDialogData } = getState()['features/device-selection'];

        if (popupDialogData) {
            popupDialogData.popup.focus();

            return;
        }

        // API_ID will always be defined because the iframe api is enabled
        const scope = `dialog_${API_ID}`;
        const url = `static/deviceSelectionPopup.html#scope=${
            encodeURIComponent(JSON.stringify(scope))}`;
        const popup
            = window.open(
                url,
                'device-selection-popup',
                'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');

        popup.addEventListener('DOMContentLoaded', () => {
            popup.init(i18next);
        });

        const transport = new Transport({
            backend: new PostMessageTransportBackend({
                postisOptions: {
                    scope,
                    window: popup
                }
            })
        });

        transport.on('request',
            _processRequest.bind(undefined, dispatch, getState));
        transport.on('event', event => {
            if (event.type === 'devices-dialog' && event.name === 'close') {
                popup.close();
                transport.dispose();
                dispatch(_setDeviceSelectionPopupData());

                return true;
            }

            return false;
        });

        dispatch(_setDeviceSelectionPopupData({
            popup,
            transport
        }));
    };
}

/**
 * Processes device requests from external applications.
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} request - The request to be processed.
 * @param {Function} responseCallback - The callback that will send the
 * response.
 * @returns {boolean}
 */ // eslint-disable-next-line max-params
function _processRequest(dispatch, getState, request, responseCallback) {
    if (request.type === 'devices') {
        switch (request.name) {
        case 'isDeviceListAvailable':
            JitsiMeetJS.mediaDevices.isDeviceListAvailable()
                .then(isDeviceListAvailable =>
                    responseCallback(isDeviceListAvailable))
                .catch(e => responseCallback(null, e));
            break;
        case 'isDeviceChangeAvailable':
            responseCallback(
                JitsiMeetJS.mediaDevices.isDeviceChangeAvailable());
            break;
        case 'isMultipleAudioInputSupported':
            responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
            break;
        case 'getCurrentDevices':
            responseCallback({
                audioInput: APP.settings.getMicDeviceId(),
                audioOutput: APP.settings.getAudioOutputDeviceId(),
                videoInput: APP.settings.getCameraDeviceId()
            });
            break;
        case 'getAvailableDevices':
            responseCallback(getState()['features/base/devices']);
            break;
        case 'setDevice': {
            let action;
            const { device } = request;

            switch (device.kind) {
            case 'audioinput':
                action = setAudioInputDevice;
                break;
            case 'audiooutput':
                action = setAudioOutputDevice;
                break;
            case 'videoinput':
                action = setVideoInputDevice;
                break;
            default:

            }
            dispatch(action(device.id));
            responseCallback(true);
            break;
        }
        default:

            return false;
        }

        return true;
    }

    return false;
}

/**
 * Sets information about device selection popup in the store.
 *
 * @param {Object} popupDialogData - Information about the popup.
 * @param {Object} popupDialog.popup - The popup object returned from
 * window.open.
 * @param {Object} popupDialogData.transport - The transport instance used for
 * communication with the popup window.
 * @returns {{
 *     type: SET_DEVICE_SELECTION_POPUP_DATA,
 *     popupDialogData: Object
 * }}
 */
function _setDeviceSelectionPopupData(popupDialogData) {
    return {
        type: SET_DEVICE_SELECTION_POPUP_DATA,
        popupDialogData
    };
}
<commit_msg>fix(device_selection_popup): URL<commit_after>/* globals APP, interfaceConfig */

import { openDialog } from '../base/dialog';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { API_ID } from '../../../modules/API/constants';
import {
    setAudioInputDevice,
    setAudioOutputDevice,
    setVideoInputDevice
} from '../base/devices';
import { i18next } from '../base/i18n';
import {
    PostMessageTransportBackend,
    Transport
} from '../../../modules/transport';

import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { DeviceSelectionDialog } from './components';

/**
 * Open DeviceSelectionDialog with a configuration based on the environment's
 * supported abilities.
 *
 * @returns {Function}
 */
export function openDeviceSelectionDialog() {
    return dispatch => {
        if (interfaceConfig.filmStripOnly) {
            dispatch(_openDeviceSelectionDialogInPopup());
        } else {
            dispatch(_openDeviceSelectionDialogHere());
        }
    };
}

/**
 * Opens the DeviceSelectionDialog in the same window.
 *
 * @returns {Function}
 */
function _openDeviceSelectionDialogHere() {
    return dispatch =>
        JitsiMeetJS.mediaDevices.isDeviceListAvailable()
            .then(isDeviceListAvailable => {
                dispatch(openDialog(DeviceSelectionDialog, {
                    currentAudioInputId: APP.settings.getMicDeviceId(),
                    currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
                    currentVideoInputId: APP.settings.getCameraDeviceId(),
                    disableAudioInputChange:
                        !JitsiMeetJS.isMultipleAudioInputSupported(),
                    disableDeviceChange: !isDeviceListAvailable
                        || !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
                    hasAudioPermission: JitsiMeetJS.mediaDevices
                        .isDevicePermissionGranted('audio'),
                    hasVideoPermission: JitsiMeetJS.mediaDevices
                        .isDevicePermissionGranted('video'),
                    hideAudioInputPreview:
                        !JitsiMeetJS.isCollectingLocalStats(),
                    hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
                        .isDeviceChangeAvailable('output')
                }));
            });
}

/**
 * Opens a popup window with the device selection dialog in it.
 *
 * @returns {Function}
 */
function _openDeviceSelectionDialogInPopup() {
    return (dispatch, getState) => {
        const { popupDialogData } = getState()['features/device-selection'];

        if (popupDialogData) {
            popupDialogData.popup.focus();

            return;
        }

        // API_ID will always be defined because the iframe api is enabled
        const scope = `dialog_${API_ID}`;
        const url = `${
            window.location.origin}/static/deviceSelectionPopup.html#scope=${
                encodeURIComponent(JSON.stringify(scope))}`;
        const popup
            = window.open(
                url,
                'device-selection-popup',
                'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');

        popup.addEventListener('DOMContentLoaded', () => {
            popup.init(i18next);
        });

        const transport = new Transport({
            backend: new PostMessageTransportBackend({
                postisOptions: {
                    scope,
                    window: popup
                }
            })
        });

        transport.on('request',
            _processRequest.bind(undefined, dispatch, getState));
        transport.on('event', event => {
            if (event.type === 'devices-dialog' && event.name === 'close') {
                popup.close();
                transport.dispose();
                dispatch(_setDeviceSelectionPopupData());

                return true;
            }

            return false;
        });

        dispatch(_setDeviceSelectionPopupData({
            popup,
            transport
        }));
    };
}

/**
 * Processes device requests from external applications.
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} request - The request to be processed.
 * @param {Function} responseCallback - The callback that will send the
 * response.
 * @returns {boolean}
 */ // eslint-disable-next-line max-params
function _processRequest(dispatch, getState, request, responseCallback) {
    if (request.type === 'devices') {
        switch (request.name) {
        case 'isDeviceListAvailable':
            JitsiMeetJS.mediaDevices.isDeviceListAvailable()
                .then(isDeviceListAvailable =>
                    responseCallback(isDeviceListAvailable))
                .catch(e => responseCallback(null, e));
            break;
        case 'isDeviceChangeAvailable':
            responseCallback(
                JitsiMeetJS.mediaDevices.isDeviceChangeAvailable());
            break;
        case 'isMultipleAudioInputSupported':
            responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
            break;
        case 'getCurrentDevices':
            responseCallback({
                audioInput: APP.settings.getMicDeviceId(),
                audioOutput: APP.settings.getAudioOutputDeviceId(),
                videoInput: APP.settings.getCameraDeviceId()
            });
            break;
        case 'getAvailableDevices':
            responseCallback(getState()['features/base/devices']);
            break;
        case 'setDevice': {
            let action;
            const { device } = request;

            switch (device.kind) {
            case 'audioinput':
                action = setAudioInputDevice;
                break;
            case 'audiooutput':
                action = setAudioOutputDevice;
                break;
            case 'videoinput':
                action = setVideoInputDevice;
                break;
            default:

            }
            dispatch(action(device.id));
            responseCallback(true);
            break;
        }
        default:

            return false;
        }

        return true;
    }

    return false;
}

/**
 * Sets information about device selection popup in the store.
 *
 * @param {Object} popupDialogData - Information about the popup.
 * @param {Object} popupDialog.popup - The popup object returned from
 * window.open.
 * @param {Object} popupDialogData.transport - The transport instance used for
 * communication with the popup window.
 * @returns {{
 *     type: SET_DEVICE_SELECTION_POPUP_DATA,
 *     popupDialogData: Object
 * }}
 */
function _setDeviceSelectionPopupData(popupDialogData) {
    return {
        type: SET_DEVICE_SELECTION_POPUP_DATA,
        popupDialogData
    };
}
<|endoftext|>