<commit_before>(function (window) {
  "use strict";

  var self;

  function ImageViewer(canvasId, imageUrl){
    self = this;

    // canvas
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");

    // dirty state
    this.dirty = true;

    // image scale
    this.scale = 1;
    this.scaleStep = 0.1;

    // image center (scroll offset)
    this.center = { x: 0, y: 0 };

    // image
    this.image = new Image();
    this.image.addEventListener('load', this._onImageLoad, false);
    this.image.src = imageUrl;

    // buttons
    this.buttons = [];

    // add buttons
    var padding = 10
      , radius = 20
      , x = this.canvas.width - radius - padding
      , y = this.canvas.height - radius - padding;

    var plusButton = new Button(x, y - 50, radius, drawPlusIcon);
    plusButton.onClick = function(){ self.zoomIn(); };
    this.buttons.push(plusButton);

    var minusButton = new Button(x, y, radius, drawMinusIcon);
    minusButton.onClick = function(){ self.zoomOut(); };
    this.buttons.push(minusButton);

    // render loop
    this.FPS = 1000/30;
    this.tickInterval = null;

    this.InputHandler = new InputHandler(this.canvas, this);
  }

  ImageViewer.prototype._onImageLoad = function(){
    // set scale to use as much space inside the canvas as possible
    if(((self.canvas.height / self.image.height) * self.image.width) <= self.canvas.width){
      self.scale = self.canvas.height / self.image.height;
    } else {
      self.scale = self.canvas.width / self.image.width;
    }

    // center at image center
    self.center.x = self.image.width / 2;
    self.center.y = self.image.height / 2;

    // image changed
    self.dirty = true;

    // stop old render loop (if existed)
    if(self.tickInterval) clearInterval(self.tickInterval);

    // start new render loop
    self.tickInterval = setInterval(function(){ self._render(); }, self.FPS);
  };

  ImageViewer.prototype._render = function(){
    // check if dirty
    if(!this.dirty) return;
    this.dirty = false;

    var ctx = this.context;
    // clear canvas
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    // draw image (transformed and scaled)
    ctx.save();
    var translateX = this.canvas.width / 2 - this.center.x * this.scale
      , translateY = this.canvas.height / 2 - this.center.y * this.scale;

    ctx.translate(translateX, translateY);
    ctx.scale(this.scale, this.scale);

    ctx.drawImage(this.image, 0,0);

    ctx.restore();

    // draw buttons
    this.buttons.forEach(function(button){
      button.draw(ctx);
    });
  };

  ImageViewer.prototype.zoomIn = function(){
    this.scale = this.scale * (1 + this.scaleStep);
    this.dirty = true;
  };

  ImageViewer.prototype.zoomOut = function(){
    this.scale = this.scale * (1 - this.scaleStep);
    this.dirty = true;
  };

  function Button(x, y, radius, icon){
    // centre coordinates
    this.x = x;
    this.y = y;

    // radius
    this.radius = radius;

    // transparency
    this.alpha = 0.5;

    // color
    this.color = '#000000';

    // icon drawing function
    // (ctx, x, y, radius, icon, color, alpha)
    this.icon = icon;

    // click action
    this.onClick = function(){ alert('no click action set!'); }
  }

  Button.prototype.isWithinBounds = function(x, y){
    var dx = Math.abs(this.x - x)
      , dy = Math.abs(this.y - y);
    return  dx * dx + dy * dy <= this.radius * this.radius;
  };

  Button.prototype.draw = function(ctx){
    // preserve context
    ctx.save();

    // drawing settings
    ctx.globalAlpha = this.alpha;
    ctx.fillStyle= this.color;
    ctx.lineWidth = 0;

    // draw circle
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    ctx.fill();

    // draw icon
    ctx.save();
    ctx.globalCompositeOperation = 'destination-out';
    this.icon(ctx, this.x, this.y, this.radius);
    ctx.restore();

    // restore context
    ctx.restore();
  };

  function drawMinusIcon(ctx, centerX, centerY, buttonRadius){
    var rectLength = buttonRadius
      , rectThickness = rectLength / 4
      , x = centerX - rectLength / 2
      , y = centerY - rectThickness / 2;

    ctx.fillRect(x, y, rectLength, rectThickness);
  }

  function drawPlusIcon(ctx, centerX, centerY, buttonRadius){
    /*
        11---10
        |     |
    01--12    09--08
    |              |
    02--03    06--07
        |     |
        04---05
    */
    var rectLength = buttonRadius
      , rectThickness = rectLength / 4;

    ctx.beginPath();
    // 1
    ctx.moveTo(centerX - rectLength / 2,
               centerY - rectThickness / 2);
    // 2
    ctx.lineTo(centerX - rectLength / 2,
               centerY + rectThickness / 2);
    // 3
    ctx.lineTo(centerX - rectThickness / 2,
               centerY + rectThickness / 2);
    // 4
    ctx.lineTo(centerX - rectThickness / 2,
               centerY + rectLength / 2);
    // 5
    ctx.lineTo(centerX + rectThickness / 2,
               centerY + rectLength / 2);
    // 6
    ctx.lineTo(centerX + rectThickness / 2,
               centerY + rectThickness / 2);
    // 7
    ctx.lineTo(centerX + rectLength / 2,
               centerY + rectThickness / 2);
    // 8
    ctx.lineTo(centerX + rectLength / 2,
               centerY - rectThickness / 2);
    // 9
    ctx.lineTo(centerX + rectThickness / 2,
               centerY - rectThickness / 2);
    // 10
    ctx.lineTo(centerX + rectThickness / 2,
               centerY - rectLength / 2);
    // 11
    ctx.lineTo(centerX - rectThickness / 2,
               centerY - rectLength / 2);
    // 12
    ctx.lineTo(centerX - rectThickness / 2,
               centerY - rectThickness / 2);

    ctx.closePath();
    ctx.fill();
  }

  function InputHandler(canvas, imageViewer) {
    this.canvas = canvas;
    this.imageViewer = imageViewer;

    // global
    this.leftMouseButtonDown = false;
    document.addEventListener('mousedown', this._onMouseDown);
    document.addEventListener('mouseup', this._onMouseUp);

    // zooming
    this.canvas.addEventListener('DOMMouseScroll', this._onMouseWheel);
    this.canvas.addEventListener('mousewheel', this._onMouseWheel);

    // moving
    this.mouseLastPos = null;
    this.canvas.addEventListener('mousemove', this._onMouseMove);
  }

  InputHandler.prototype._onMouseDown = function(evt){
    if(evt.button === 0){ // left/main button
      // check if a button was clicked
      var rect = self.canvas.getBoundingClientRect()
        , pos = {
            x: evt.clientX - rect.left,
            y: evt.clientY - rect.top
          };
      var clickedButtons = self.buttons.filter(function(button){ return button.isWithinBounds(pos.x, pos.y); });
      // button clicked
      if(clickedButtons.length > 0 ){
        clickedButtons[0].onClick();
      }
      // no button clicked
      else {
        // set flag for image moving
        self.InputHandler.leftMouseButtonDown = true;
      }
    }
  };

  InputHandler.prototype._onMouseUp = function(evt){
    if(evt.button === 0){ // left/main button
      self.InputHandler.leftMouseButtonDown = false;
    }
  };

  InputHandler.prototype._onMouseWheel = function(evt){
    if (!evt) evt = event;
    evt.preventDefault();
    if(evt.detail<0 || evt.wheelDelta>0){ // up -> smaller
      self.zoomOut();
    } else { // down -> larger
      self.zoomIn();
    }
  };

  InputHandler.prototype._onMouseMove = function(evt){
    var lastPos = self.InputHandler.mouseLastPos
      , rect = self.canvas.getBoundingClientRect()
      , newPos = {
          x: evt.clientX - rect.left,
          y: evt.clientY - rect.top
        };
    if(lastPos !== null && self.InputHandler.leftMouseButtonDown){
      var deltaX = newPos.x - lastPos.x
        , deltaY = newPos.y - lastPos.y;

      self.center.x -= deltaX / self.scale;
      self.center.y -= deltaY / self.scale;
      self.dirty = true;
    }
    self.InputHandler.mouseLastPos = newPos;
  };

  window.ImageViewer = ImageViewer;
}(window));
<commit_msg>linter error: added missing semicolon<commit_after>(function (window) {
  "use strict";

  var self;

  function ImageViewer(canvasId, imageUrl){
    self = this;

    // canvas
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");

    // dirty state
    this.dirty = true;

    // image scale
    this.scale = 1;
    this.scaleStep = 0.1;

    // image center (scroll offset)
    this.center = { x: 0, y: 0 };

    // image
    this.image = new Image();
    this.image.addEventListener('load', this._onImageLoad, false);
    this.image.src = imageUrl;

    // buttons
    this.buttons = [];

    // add buttons
    var padding = 10
      , radius = 20
      , x = this.canvas.width - radius - padding
      , y = this.canvas.height - radius - padding;

    var plusButton = new Button(x, y - 50, radius, drawPlusIcon);
    plusButton.onClick = function(){ self.zoomIn(); };
    this.buttons.push(plusButton);

    var minusButton = new Button(x, y, radius, drawMinusIcon);
    minusButton.onClick = function(){ self.zoomOut(); };
    this.buttons.push(minusButton);

    // render loop
    this.FPS = 1000/30;
    this.tickInterval = null;

    this.InputHandler = new InputHandler(this.canvas, this);
  }

  ImageViewer.prototype._onImageLoad = function(){
    // set scale to use as much space inside the canvas as possible
    if(((self.canvas.height / self.image.height) * self.image.width) <= self.canvas.width){
      self.scale = self.canvas.height / self.image.height;
    } else {
      self.scale = self.canvas.width / self.image.width;
    }

    // center at image center
    self.center.x = self.image.width / 2;
    self.center.y = self.image.height / 2;

    // image changed
    self.dirty = true;

    // stop old render loop (if existed)
    if(self.tickInterval) clearInterval(self.tickInterval);

    // start new render loop
    self.tickInterval = setInterval(function(){ self._render(); }, self.FPS);
  };

  ImageViewer.prototype._render = function(){
    // check if dirty
    if(!this.dirty) return;
    this.dirty = false;

    var ctx = this.context;
    // clear canvas
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    // draw image (transformed and scaled)
    ctx.save();
    var translateX = this.canvas.width / 2 - this.center.x * this.scale
      , translateY = this.canvas.height / 2 - this.center.y * this.scale;

    ctx.translate(translateX, translateY);
    ctx.scale(this.scale, this.scale);

    ctx.drawImage(this.image, 0,0);

    ctx.restore();

    // draw buttons
    this.buttons.forEach(function(button){
      button.draw(ctx);
    });
  };

  ImageViewer.prototype.zoomIn = function(){
    this.scale = this.scale * (1 + this.scaleStep);
    this.dirty = true;
  };

  ImageViewer.prototype.zoomOut = function(){
    this.scale = this.scale * (1 - this.scaleStep);
    this.dirty = true;
  };

  function Button(x, y, radius, icon){
    // centre coordinates
    this.x = x;
    this.y = y;

    // radius
    this.radius = radius;

    // transparency
    this.alpha = 0.5;

    // color
    this.color = '#000000';

    // icon drawing function
    // (ctx, x, y, radius, icon, color, alpha)
    this.icon = icon;

    // click action
    this.onClick = function(){ alert('no click action set!'); };
  }

  Button.prototype.isWithinBounds = function(x, y){
    var dx = Math.abs(this.x - x)
      , dy = Math.abs(this.y - y);
    return  dx * dx + dy * dy <= this.radius * this.radius;
  };

  Button.prototype.draw = function(ctx){
    // preserve context
    ctx.save();

    // drawing settings
    ctx.globalAlpha = this.alpha;
    ctx.fillStyle= this.color;
    ctx.lineWidth = 0;

    // draw circle
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    ctx.fill();

    // draw icon
    ctx.save();
    ctx.globalCompositeOperation = 'destination-out';
    this.icon(ctx, this.x, this.y, this.radius);
    ctx.restore();

    // restore context
    ctx.restore();
  };

  function drawMinusIcon(ctx, centerX, centerY, buttonRadius){
    var rectLength = buttonRadius
      , rectThickness = rectLength / 4
      , x = centerX - rectLength / 2
      , y = centerY - rectThickness / 2;

    ctx.fillRect(x, y, rectLength, rectThickness);
  }

  function drawPlusIcon(ctx, centerX, centerY, buttonRadius){
    /*
        11---10
        |     |
    01--12    09--08
    |              |
    02--03    06--07
        |     |
        04---05
    */
    var rectLength = buttonRadius
      , rectThickness = rectLength / 4;

    ctx.beginPath();
    // 1
    ctx.moveTo(centerX - rectLength / 2,
               centerY - rectThickness / 2);
    // 2
    ctx.lineTo(centerX - rectLength / 2,
               centerY + rectThickness / 2);
    // 3
    ctx.lineTo(centerX - rectThickness / 2,
               centerY + rectThickness / 2);
    // 4
    ctx.lineTo(centerX - rectThickness / 2,
               centerY + rectLength / 2);
    // 5
    ctx.lineTo(centerX + rectThickness / 2,
               centerY + rectLength / 2);
    // 6
    ctx.lineTo(centerX + rectThickness / 2,
               centerY + rectThickness / 2);
    // 7
    ctx.lineTo(centerX + rectLength / 2,
               centerY + rectThickness / 2);
    // 8
    ctx.lineTo(centerX + rectLength / 2,
               centerY - rectThickness / 2);
    // 9
    ctx.lineTo(centerX + rectThickness / 2,
               centerY - rectThickness / 2);
    // 10
    ctx.lineTo(centerX + rectThickness / 2,
               centerY - rectLength / 2);
    // 11
    ctx.lineTo(centerX - rectThickness / 2,
               centerY - rectLength / 2);
    // 12
    ctx.lineTo(centerX - rectThickness / 2,
               centerY - rectThickness / 2);

    ctx.closePath();
    ctx.fill();
  }

  function InputHandler(canvas, imageViewer) {
    this.canvas = canvas;
    this.imageViewer = imageViewer;

    // global
    this.leftMouseButtonDown = false;
    document.addEventListener('mousedown', this._onMouseDown);
    document.addEventListener('mouseup', this._onMouseUp);

    // zooming
    this.canvas.addEventListener('DOMMouseScroll', this._onMouseWheel);
    this.canvas.addEventListener('mousewheel', this._onMouseWheel);

    // moving
    this.mouseLastPos = null;
    this.canvas.addEventListener('mousemove', this._onMouseMove);
  }

  InputHandler.prototype._onMouseDown = function(evt){
    if(evt.button === 0){ // left/main button
      // check if a button was clicked
      var rect = self.canvas.getBoundingClientRect()
        , pos = {
            x: evt.clientX - rect.left,
            y: evt.clientY - rect.top
          };
      var clickedButtons = self.buttons.filter(function(button){ return button.isWithinBounds(pos.x, pos.y); });
      // button clicked
      if(clickedButtons.length > 0 ){
        clickedButtons[0].onClick();
      }
      // no button clicked
      else {
        // set flag for image moving
        self.InputHandler.leftMouseButtonDown = true;
      }
    }
  };

  InputHandler.prototype._onMouseUp = function(evt){
    if(evt.button === 0){ // left/main button
      self.InputHandler.leftMouseButtonDown = false;
    }
  };

  InputHandler.prototype._onMouseWheel = function(evt){
    if (!evt) evt = event;
    evt.preventDefault();
    if(evt.detail<0 || evt.wheelDelta>0){ // up -> smaller
      self.zoomOut();
    } else { // down -> larger
      self.zoomIn();
    }
  };

  InputHandler.prototype._onMouseMove = function(evt){
    var lastPos = self.InputHandler.mouseLastPos
      , rect = self.canvas.getBoundingClientRect()
      , newPos = {
          x: evt.clientX - rect.left,
          y: evt.clientY - rect.top
        };
    if(lastPos !== null && self.InputHandler.leftMouseButtonDown){
      var deltaX = newPos.x - lastPos.x
        , deltaY = newPos.y - lastPos.y;

      self.center.x -= deltaX / self.scale;
      self.center.y -= deltaY / self.scale;
      self.dirty = true;
    }
    self.InputHandler.mouseLastPos = newPos;
  };

  window.ImageViewer = ImageViewer;
}(window));
<|endoftext|>


<commit_before>
var polygraph = function(spec) {
    function requestAnimationFrame(f) {
      var fn = window.requestAnimationFrame;
      if (!fn) fn = window.webkitRequestAnimationFrame;
      if (!fn) fn = window.mozRequestAnimationFrame;
      return fn(f);
    }

    function drawEllipseByCenter(ctx, cx, cy, w, h) {
        drawEllipse(ctx, cx - w/2.0, cy - h/2.0, w, h);
    }

    function drawEllipse(ctx, x, y, w, h) {
        var kappa = .5522848,
            ox = (w / 2) * kappa, // control point offset horizontal
            oy = (h / 2) * kappa, // control point offset vertical
            xe = x + w,           // x-end
            ye = y + h,           // y-end
            xm = x + w / 2,       // x-middle
            ym = y + h / 2;       // y-middle

        ctx.beginPath();
        ctx.moveTo(x, ym);
        ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
        ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
        ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
        ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
        ctx.closePath();
        ctx.stroke();
    }

    function Node(ctx, name) {
        this.ctx = ctx
        this.name = name
        this.width = Math.max(ctx.measureText(name).width, 60)
        this.height = 60
        this.x = this.y = 0
    }

    Node.prototype.draw = function() {
        drawEllipseByCenter(this.ctx, this.x, this.y, this.width * 1.2, this.height)

        this.ctx.fillText(this.name, this.x, this.y, this.width)
    }

    Node.prototype.setPosition = function(x, y) {
        var cnv = this.ctx.canvas

        this.x = Math.min(Math.max(this.width, x), cnv.width - this.width)
        this.y = Math.min(Math.max(this.height, y), cnv.height - this.height)
    }

    Node.prototype.pointOnEdge = function(angle) {
        return {x: Math.cos(angle) * this.width * 0.6, y: Math.sin(angle) * this.height / 2}
    }

    function drawConnection(a, b, color) {
        var ctx = a.ctx

        ctx.strokeStyle = color
        ctx.beginPath()

        var angle = Math.atan2(b.y - a.y, b.x - a.x)
        var p1 = a.pointOnEdge(angle)
        var p2 = b.pointOnEdge(angle + Math.PI)

        ctx.moveTo(a.x + p1.x, a.y + p1.y)
        ctx.lineTo(b.x + p2.x, b.y + p2.y)
        ctx.stroke()
    }

    function capitalize(x) {
        return x[0].toUpperCase() + x.slice(1)
    }

    var ctx = document.getElementById('polygraph').getContext('2d')
    var style = window.getComputedStyle(ctx.canvas)

    var dims = ['width', 'height'].map(function(dim) {
        return ctx.canvas[dim] = window['inner' + capitalize(dim)] * 0.8
    })

    ctx.fillStyle = "rgb(50, 50, 50)"
    ctx.textAlign = "center"
    ctx.textBaseline = "middle"
    ctx.font = "1.5em Cookie"

    var NODES = spec.nodes.map(function(name, i) {
        var n = new Node(ctx, name)

        //n.setPosition(Math.random() * dims[0], Math.random() * dims[1])
        n.setPosition((dims[0] - 90) / 5 * (i % 5) + 90, (dims[1] - 50) / 5 * (i / 5) + 50)

        return n
    })

    var NODE_map = {}

    NODES.forEach(function(n) {
        NODE_map[n.name] = n })

    var EDGES = spec.connections.map(function(c) {
        var a = NODE_map[c[0]]
        var b = NODE_map[c[1]]
        var color = c[2]

        if (!a || !b) return null

        return [a, b, color]
    })

    function redraw() {
        ctx.clearRect(0, 0, dims[0], dims[1])
        ctx.strokeStyle = "rgb(50, 50, 50)"

        ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'
        ctx.fill()

        NODES.forEach(function(n) {
            n.draw()
        })

        EDGES.forEach(function(x) {
            drawConnection(x[0], x[1], x[2])
        })
    }

    var frame = 1
    // K is the optimal distance between vertices, 2.5 found experimentally
    var k = 2.5 * Math.sqrt(dims[0] * dims[1] / NODES.length);
    function recalc() {

        // from Thomas M. J. Fruchterman, Adward M. Reingold,
        //  Graph Drawing by Force-directed Placement
        // javascript implementation based on http://stevehanov.ca/blog/index.php?id=65

        // C limits the speed of the movement. Things become slower over time.
        var C = Math.log( frame + 1 ) * 100;
        frame++;

        // calculate repulsive forces
        NODES.forEach(function(v) {
            // Initialize velocity to none.
            v.vx = 0.0
            v.vy = 0.0
            // for each other node, calculate the repulsive force and adjust the velocity
            // in the direction of repulsion.
            NODES.forEach(function(u) {
                if (v == u) return

                // D is short hand for the difference vector between the positions
                // of the two vertices
                var Dx = v.x - u.x
                var Dy = v.y - u.y
                var len = Math.sqrt(Dx*Dx+Dy*Dy) // distance

                if ( len == 0 ) return // do not divide by zero

                var mul = k * k / (len*len * C)

                v.vx += Dx * mul
                v.vy += Dy * mul
            })
        })

        // calculate attractive forces
        EDGES.forEach(function(e) {

            // each edge is an ordered pair of vertices [0] and [1]
            // there is also a color, but that's only used when drawing
            var Dx = e[0].x - e[1].x
            var Dy = e[0].y - e[1].y
            var len = Math.sqrt(Dx * Dx + Dy * Dy) // distance.

            if ( len == 0 ) return // do not divide by zero

            var mul = len * len / k / C
            var Dxmul = Dx * mul
            var Dymul = Dy * mul

            // attract both nodes towards each other.
            e[0].vx -= Dxmul
            e[0].vy -= Dymul
            e[1].vx += Dxmul
            e[1].vy += Dymul

        })

        // Here we go through each node and actually move it in the given direction.
        NODES.forEach(function(v) {
            var len = v.vx * v.vx + v.vy * v.vy;
            var max = 10;

            if (frame > 20) max = 2.0;

            if ( len > max*max ) {
                len = Math.pow( len, 0.5 );
                v.vx *= max / len;
                v.vy *= max / len;
            }

            v.setPosition(v.x + v.vx, v.y + v.vy)

        })
    }
    redraw()
    requestAnimationFrame(function anim() {
        recalc()
        redraw()

        if (frame++ > 1e3) return

        requestAnimationFrame(anim)
    })
};
<commit_msg>More conventional prefix handling<commit_after>window.requestAnimationFrame = window.requestAnimationFrame
                            || window.webkitRequestAnimationFrame
                            || window.mozRequestAnimationFrame
                            || window.msRequestAnimationFrame;

var polygraph = function(spec) {
    function drawEllipseByCenter(ctx, cx, cy, w, h) {
        drawEllipse(ctx, cx - w/2.0, cy - h/2.0, w, h);
    }

    function drawEllipse(ctx, x, y, w, h) {
        var kappa = .5522848,
            ox = (w / 2) * kappa, // control point offset horizontal
            oy = (h / 2) * kappa, // control point offset vertical
            xe = x + w,           // x-end
            ye = y + h,           // y-end
            xm = x + w / 2,       // x-middle
            ym = y + h / 2;       // y-middle

        ctx.beginPath();
        ctx.moveTo(x, ym);
        ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
        ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
        ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
        ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
        ctx.closePath();
        ctx.stroke();
    }

    function Node(ctx, name) {
        this.ctx = ctx
        this.name = name
        this.width = Math.max(ctx.measureText(name).width, 60)
        this.height = 60
        this.x = this.y = 0
    }

    Node.prototype.draw = function() {
        drawEllipseByCenter(this.ctx, this.x, this.y, this.width * 1.2, this.height)

        this.ctx.fillText(this.name, this.x, this.y, this.width)
    }

    Node.prototype.setPosition = function(x, y) {
        var cnv = this.ctx.canvas

        this.x = Math.min(Math.max(this.width, x), cnv.width - this.width)
        this.y = Math.min(Math.max(this.height, y), cnv.height - this.height)
    }

    Node.prototype.pointOnEdge = function(angle) {
        return {x: Math.cos(angle) * this.width * 0.6, y: Math.sin(angle) * this.height / 2}
    }

    function drawConnection(a, b, color) {
        var ctx = a.ctx

        ctx.strokeStyle = color
        ctx.beginPath()

        var angle = Math.atan2(b.y - a.y, b.x - a.x)
        var p1 = a.pointOnEdge(angle)
        var p2 = b.pointOnEdge(angle + Math.PI)

        ctx.moveTo(a.x + p1.x, a.y + p1.y)
        ctx.lineTo(b.x + p2.x, b.y + p2.y)
        ctx.stroke()
    }

    function capitalize(x) {
        return x[0].toUpperCase() + x.slice(1)
    }

    var ctx = document.getElementById('polygraph').getContext('2d')
    var style = window.getComputedStyle(ctx.canvas)

    var dims = ['width', 'height'].map(function(dim) {
        return ctx.canvas[dim] = window['inner' + capitalize(dim)] * 0.8
    })

    ctx.fillStyle = "rgb(50, 50, 50)"
    ctx.textAlign = "center"
    ctx.textBaseline = "middle"
    ctx.font = "1.5em Cookie"

    var NODES = spec.nodes.map(function(name, i) {
        var n = new Node(ctx, name)

        //n.setPosition(Math.random() * dims[0], Math.random() * dims[1])
        n.setPosition((dims[0] - 90) / 5 * (i % 5) + 90, (dims[1] - 50) / 5 * (i / 5) + 50)

        return n
    })

    var NODE_map = {}

    NODES.forEach(function(n) {
        NODE_map[n.name] = n })

    var EDGES = spec.connections.map(function(c) {
        var a = NODE_map[c[0]]
        var b = NODE_map[c[1]]
        var color = c[2]

        if (!a || !b) return null

        return [a, b, color]
    })

    function redraw() {
        ctx.clearRect(0, 0, dims[0], dims[1])
        ctx.strokeStyle = "rgb(50, 50, 50)"

        ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'
        ctx.fill()

        NODES.forEach(function(n) {
            n.draw()
        })

        EDGES.forEach(function(x) {
            drawConnection(x[0], x[1], x[2])
        })
    }

    var frame = 1
    // K is the optimal distance between vertices, 2.5 found experimentally
    var k = 2.5 * Math.sqrt(dims[0] * dims[1] / NODES.length);
    function recalc() {

        // from Thomas M. J. Fruchterman, Adward M. Reingold,
        //  Graph Drawing by Force-directed Placement
        // javascript implementation based on http://stevehanov.ca/blog/index.php?id=65

        // C limits the speed of the movement. Things become slower over time.
        var C = Math.log( frame + 1 ) * 100;
        frame++;

        // calculate repulsive forces
        NODES.forEach(function(v) {
            // Initialize velocity to none.
            v.vx = 0.0
            v.vy = 0.0
            // for each other node, calculate the repulsive force and adjust the velocity
            // in the direction of repulsion.
            NODES.forEach(function(u) {
                if (v == u) return

                // D is short hand for the difference vector between the positions
                // of the two vertices
                var Dx = v.x - u.x
                var Dy = v.y - u.y
                var len = Math.sqrt(Dx*Dx+Dy*Dy) // distance

                if ( len == 0 ) return // do not divide by zero

                var mul = k * k / (len*len * C)

                v.vx += Dx * mul
                v.vy += Dy * mul
            })
        })

        // calculate attractive forces
        EDGES.forEach(function(e) {

            // each edge is an ordered pair of vertices [0] and [1]
            // there is also a color, but that's only used when drawing
            var Dx = e[0].x - e[1].x
            var Dy = e[0].y - e[1].y
            var len = Math.sqrt(Dx * Dx + Dy * Dy) // distance.

            if ( len == 0 ) return // do not divide by zero

            var mul = len * len / k / C
            var Dxmul = Dx * mul
            var Dymul = Dy * mul

            // attract both nodes towards each other.
            e[0].vx -= Dxmul
            e[0].vy -= Dymul
            e[1].vx += Dxmul
            e[1].vy += Dymul

        })

        // Here we go through each node and actually move it in the given direction.
        NODES.forEach(function(v) {
            var len = v.vx * v.vx + v.vy * v.vy;
            var max = 10;

            if (frame > 20) max = 2.0;

            if ( len > max*max ) {
                len = Math.pow( len, 0.5 );
                v.vx *= max / len;
                v.vy *= max / len;
            }

            v.setPosition(v.x + v.vx, v.y + v.vy)

        })
    }
    redraw()
    requestAnimationFrame(function anim() {
        recalc()
        redraw()

        if (frame++ > 1e3) return

        requestAnimationFrame(anim)
    })
};
<|endoftext|>


<commit_before>var WebSocket = require('ws');

var DEFAULT_BACKOFF = 1000;
var MAX_BACKOFF = 30000;
var BACKOFF_INCR = 2000;

var EVENT = 1;
var REPLY = 2;
var PING = 3;
var PONG = 4;

var NO_ACK = 0;

// isObject returns true if the provided value is an object. (From lodash.)
function isObject(value) {
  return !!value && (typeof value === 'object' || typeof value === 'function');
}

// isFunction returns true if the provided value is a function.
function isFunction(value) {
  return isObject(value) && Object.prototype.toString.call(value) === '[object Function]';
}

// WebRPC implements the WebRPC client.
function WebRPC(url, codec) {
  if (!(this instanceof WebRPC)) {
    throw new Error('Please use the \'new\' operator.');
  }

  var self = this;
  var backoff = DEFAULT_BACKOFF;

  function connect() {
    var ws = new WebSocket(url);
    ws.addEventListener('open', openEvent);
    ws.addEventListener('close', closeEvent);
    ws.addEventListener('message', messageEvent);

    return ws;
  }

  function send(type, name, data, ack) {
    var event = self.codec.encode({
      type: type,
      name: name,
      data: data,
      ack: ack
    });

    if (self.ws.readyState === 0) {
      self.sendq.push(event);
    } else {
      self.ws.send(event);
    }
  }

  function openEvent(e) {
    // Send queued events.
    for (var i = 0, len = self.sendq.length; i < len; i++) {
      self.ws.send(self.sendq[i]);
    }
    self.sendq = [];

    // Run onconnect handler.
    if (self.onconnect) {
      self.onconnect(e);
    }

    // Reset the reconnect backoff.
    backoff = DEFAULT_BACKOFF;
  }

  function closeEvent(e) {
    // Run ondisconnect handler.
    if (self.ondisconnect) {
      self.ondisconnect(e);
    }

    // Try reconnecting.
    setTimeout(backoff, function tryConnect() {
      self.ws = connect();
    });

    // Increase the reconnect backoff.
    backoff += BACKOFF_INCR;
    if (backoff > MAX_BACKOFF) {
      backoff = MAX_BACKOFF;
    }
  }

  function messageEvent(e) {
    // Decode message.
    var message = self.codec.decode(e.data);

    switch (message.type) {
    case PING:
      send(PONG);
      break;

    case REPLY:
      var resolve = self.dispatch[event.ack];
      if (resolve) {
        resolve.apply(self, event.data);
        delete self.dispatch[event.ack];
      }
      break;

    case EVENT:
      var listeners = self.listenermap[event.name];
      if (listeners) {
        for (var i = 0, len = listeners.length; i < len; i++) {
          listeners[i].apply(self, event.data);
        }
      }
      break;

    default:
      break;
    }
  }

  // Connection state.
  this.ws = connect();
  this.codec = codec || new JSONCodec();
  this.ack = 0;

  // RPC data.
  this.sendq = [];      // List of queued messages.
  this.dispatch = {};   // Map of ack ID => dispatch function.
  this.listenermap = {};  // Map of event name => array of listener functions.

  // Callbacks.
  this.onconnect = null;
  this.ondisconnect = null;

  // Expose direct send.
  this._send = send;
}

WebRPC.prototype.on = function on(name, listener) {
  var listeners = self.listenermap[event.name];

  if (!listeners) {
    listeners = [];
  }

  listeners.push(listener);

  self.listenermap[event.name] = listeners;
};

WebRPC.prototype.emit = function emit() {
  var name = arguments[0];
  var params = Array.prototype.splice.call(arguments, 1);

  if (isFunction(params[params.length - 1])) {
    var callback = params.pop();
    this._send(EVENT, name, params, ++self.ack);
    this.dispatch[self.ack] = callback;
  } else {
    this._send(EVENT, name, params, NO_ACK);
  }
};

// JSONCodec implements a JSON codec for WebRPC (the default.)
function JSONCodec() {
  if (!(this instanceof JSONCodec)) {
    throw new Error('Please use the \'new\' operator.');
  }
}

JSONCodec.prototype.encode = function jsonEncode(msg) {
  return JSON.stringify(msg);
};

JSONCodec.prototype.decode = function jsonDecode(data) {
  return JSON.parse(data);
};

module.exports = WebRPC;
module.exports.JSONCodec = JSONCodec;
<commit_msg>Minor bugfixes.<commit_after>var WebSocket = require('ws');

var DEFAULT_BACKOFF = 1000;
var MAX_BACKOFF = 30000;
var BACKOFF_INCR = 2000;

var EVENT = 1;
var REPLY = 2;
var PING = 3;
var PONG = 4;

var NO_ACK = 0;

// isObject returns true if the provided value is an object. (From lodash.)
function isObject(value) {
  return !!value && (typeof value === 'object' || typeof value === 'function');
}

// isFunction returns true if the provided value is a function.
function isFunction(value) {
  return isObject(value) && Object.prototype.toString.call(value) === '[object Function]';
}

// WebRPC implements the WebRPC client.
function WebRPC(url, codec) {
  if (!(this instanceof WebRPC)) {
    throw new Error('Please use the \'new\' operator.');
  }

  var self = this;
  var backoff = DEFAULT_BACKOFF;

  function connect() {
    var ws = new WebSocket(url);
    ws.addEventListener('open', openEvent);
    ws.addEventListener('close', closeEvent);
    ws.addEventListener('message', messageEvent);

    return ws;
  }

  function send(type, name, data, ack) {
    var event = self.codec.encode({
      type: type,
      name: name,
      data: data,
      ack: ack
    });

    if (self.ws.readyState === 0) {
      self.sendq.push(event);
    } else {
      self.ws.send(event);
    }
  }

  function openEvent(e) {
    // Send queued events.
    for (var i = 0, len = self.sendq.length; i < len; i++) {
      self.ws.send(self.sendq[i]);
    }
    self.sendq = [];

    // Run onconnect handler.
    if (self.onconnect) {
      self.onconnect(e);
    }

    // Reset the reconnect backoff.
    backoff = DEFAULT_BACKOFF;
  }

  function closeEvent(e) {
    // Run ondisconnect handler.
    if (self.ondisconnect) {
      self.ondisconnect(e);
    }

    // Try reconnecting.
    setTimeout(backoff, function tryConnect() {
      self.ws = connect();
    });

    // Increase the reconnect backoff.
    backoff += BACKOFF_INCR;
    if (backoff > MAX_BACKOFF) {
      backoff = MAX_BACKOFF;
    }
  }

  function messageEvent(e) {
    // Decode message.
    var message = self.codec.decode(e.data);

    switch (message.type) {
    case PING:
      send(PONG);
      break;

    case REPLY:
      var resolve = self.dispatch[message.ack];
      if (resolve) {
        resolve.apply(self, message.data);
        delete self.dispatch[message.ack];
      }
      break;

    case EVENT:
      var listeners = self.listenermap[message.name];
      if (listeners) {
        for (var i = 0, len = listeners.length; i < len; i++) {
          listeners[i].apply(self, message.data);
        }
      }
      break;

    default:
      break;
    }
  }

  // Connection state.
  this.ws = connect();
  this.codec = codec || new JSONCodec();
  this.ack = 0;

  // RPC data.
  this.sendq = [];      // List of queued messages.
  this.dispatch = {};   // Map of ack ID => dispatch function.
  this.listenermap = {};  // Map of event name => array of listener functions.

  // Callbacks.
  this.onconnect = null;
  this.ondisconnect = null;

  // Expose direct send.
  this._send = send;
}

WebRPC.prototype.on = function on(name, listener) {
  var listeners = this.listenermap[name];

  if (!listeners) {
    listeners = [];
  }

  listeners.push(listener);

  this.listenermap[name] = listeners;
};

WebRPC.prototype.emit = function emit() {
  var name = arguments[0];
  var params = Array.prototype.splice.call(arguments, 1);

  if (isFunction(params[params.length - 1])) {
    var callback = params.pop();
    this._send(EVENT, name, params, ++self.ack);
    this.dispatch[self.ack] = callback;
  } else {
    this._send(EVENT, name, params, NO_ACK);
  }
};

// JSONCodec implements a JSON codec for WebRPC (the default.)
function JSONCodec() {
  if (!(this instanceof JSONCodec)) {
    throw new Error('Please use the \'new\' operator.');
  }
}

JSONCodec.prototype.encode = function jsonEncode(msg) {
  return JSON.stringify(msg);
};

JSONCodec.prototype.decode = function jsonDecode(data) {
  return JSON.parse(data);
};

module.exports = WebRPC;
module.exports.JSONCodec = JSONCodec;
<|endoftext|>


<commit_before>const _ = require('lodash')
const async = require('async')
const crypto = require('crypto')
const entities = require('html-entities').AllHtmlEntities
const fs = require('fs')
const http = require('http')
const ical = require('ical')
const mime = require('mime-types')
const op = require('object-path')
const parseString = require('xml2js').parseString
const path = require('path')
const request = require('request')

const interval = process.env.HARVEST_INTERVAL || 1000 * 60 * 20

const newsFile = path.join(__dirname, 'static', 'news.json')
const eventsFile = path.join(__dirname, 'static', 'events.json')
var newsMd5
var eventsMd5

var getNews = (news_url, callback) => {
    request.get({url: news_url, strictSSL: true, timeout: 10000}, (err, response, body) => {
        if(err) { return callback(err) }

        parseString(body, { trim: true, normalizeTags: true, ignoreAttrs: true }, (err, result) => {
            if(err) { return callback(err) }

            const rss = op.get(result, 'rss.channel.0.item')

            let news = []
            for (let i = 0; i < rss.length; i++) {
                news.push({
                    title: op.get(rss, [i, 'title', 0]),
                    date: new Date(op.get(rss, [i, 'pubdate', 0])),
                    text: op.get(rss, [i, 'description', 0]).replace('[…]', '...'),
                    url: op.get(rss, [i, 'link', 0])
                })
            }

            callback(null, news)
        })
    })
}

var getRooms = (rooms_url, callback) => {
    request.get({url: rooms_url, strictSSL: true, json: true, timeout: 10000}, (err, response, body) => {
        if(err) { return callback(err) }

        let rooms = []
        for (let i = 0; i < body.length; i++) {
            if(!op.get(body, [i, 'properties', 'calendar', 'values', 0, 'value'])) { continue }

            rooms.push({
                title: op.get(body, [i, 'displayname']),
                info: op.get(body, [i, 'displayinfo']),
                ical: op.get(body, [i, 'properties', 'calendar', 'values', 0, 'value']),
            })
        }

        callback(null, rooms)
    })
}

var getMd5 = (str) => {
    return crypto.createHash('md5').update(str, 'utf8').digest('hex')
}

var getFileMd5 = (path) => {
    if (!fs.existsSync(path)) { return }

    return getMd5(fs.readFileSync(path, 'utf8'))
}

var doHarvest = () => {
    getNews('http://www.artun.ee/?feed=newsticker', (err, news) => {
        if (err) { return console.error(err) }

        var sortedNews = JSON.stringify(_.sortBy(news, ['date']))

        if (getMd5(sortedNews) === (newsMd5 || getFileMd5(newsFile))) {
            // console.log((new Date()).toISOString(), `News: not changed`)
        } else {
            fs.writeFile(newsFile, sortedNews, 'utf8', () => {
                newsMd5 = getFileMd5(newsFile)
                console.log((new Date()).toISOString(), `News: ${news.length}`)
            })
        }
    })

    getRooms('https://eka.entu.ee/api/get_entity_list?only_public=true&full_info=true&entity_definition_keyname=room', (err, rooms) => {
        if (err) { return console.error(err) }

        var events = []
        async.each(rooms, (room, callback) => {
            ical.fromURL(room.ical, {}, function(err, data) {
                if (err) {
                    console.error(room.ical)
                    console.error(err)
                    return callback(null)
                }

                for (let k in data) {
                    if (!data.hasOwnProperty(k)) { continue }
                    if (op.get(data, [k, 'type']) !== 'VEVENT') { continue }

                    events.push({
                        title: room.title,
                        info: room.info,
                        start: op.get(data, [k, 'start']),
                        end: op.get(data, [k, 'end']),
                        summary: entities.decode(op.get(data, [k, 'summary'])),
                        description: entities.decode(op.get(data, [k, 'description']))
                    })
                }

                callback(null)
            })
        }, err => {
            if (err) { return console.error(err) }

            var sortedEvents = JSON.stringify(_.sortBy(events, ['start', 'end', 'title']))

            if (getMd5(sortedEvents) === (eventsMd5 || getFileMd5(eventsFile))) {
                // console.log((new Date()).toISOString(), `Events: not changed`)
            } else {
                fs.writeFile(eventsFile, sortedEvents, 'utf8', () => {
                    eventsMd5 = getFileMd5(eventsFile)
                    console.log((new Date()).toISOString(), `Events: ${events.length}`)
                })
            }
        })
    })
}

var server = http.createServer((request, response) => {
    var filePath = path.join(__dirname, 'static', request.url.split('?')[0])
    console.log(filePath)

    var contentType = mime.lookup(path.extname(filePath)) || 'application/octet-stream'

    fs.readFile(filePath, (err, content) => {
        if (err) {
            response.writeHead(404, { 'Content-Type': 'text/plain' })
            response.end('404\n')
        } else {
            response.writeHead(200, { 'Content-Type': contentType })
            response.end(content, 'utf-8')
        }
    })
})

server.listen(process.env.PORT)

doHarvest()

setInterval(doHarvest, interval)
<commit_msg>Serve index.html<commit_after>const _ = require('lodash')
const async = require('async')
const crypto = require('crypto')
const entities = require('html-entities').AllHtmlEntities
const fs = require('fs')
const http = require('http')
const ical = require('ical')
const mime = require('mime-types')
const op = require('object-path')
const parseString = require('xml2js').parseString
const path = require('path')
const request = require('request')

const interval = process.env.HARVEST_INTERVAL || 1000 * 60 * 20

const newsFile = path.join(__dirname, 'static', 'news.json')
const eventsFile = path.join(__dirname, 'static', 'events.json')
var newsMd5
var eventsMd5

var getNews = (news_url, callback) => {
    request.get({url: news_url, strictSSL: true, timeout: 10000}, (err, response, body) => {
        if(err) { return callback(err) }

        parseString(body, { trim: true, normalizeTags: true, ignoreAttrs: true }, (err, result) => {
            if(err) { return callback(err) }

            const rss = op.get(result, 'rss.channel.0.item')

            let news = []
            for (let i = 0; i < rss.length; i++) {
                news.push({
                    title: op.get(rss, [i, 'title', 0]),
                    date: new Date(op.get(rss, [i, 'pubdate', 0])),
                    text: op.get(rss, [i, 'description', 0]).replace('[…]', '...'),
                    url: op.get(rss, [i, 'link', 0])
                })
            }

            callback(null, news)
        })
    })
}

var getRooms = (rooms_url, callback) => {
    request.get({url: rooms_url, strictSSL: true, json: true, timeout: 10000}, (err, response, body) => {
        if(err) { return callback(err) }

        let rooms = []
        for (let i = 0; i < body.length; i++) {
            if(!op.get(body, [i, 'properties', 'calendar', 'values', 0, 'value'])) { continue }

            rooms.push({
                title: op.get(body, [i, 'displayname']),
                info: op.get(body, [i, 'displayinfo']),
                ical: op.get(body, [i, 'properties', 'calendar', 'values', 0, 'value']),
            })
        }

        callback(null, rooms)
    })
}

var getMd5 = (str) => {
    return crypto.createHash('md5').update(str, 'utf8').digest('hex')
}

var getFileMd5 = (path) => {
    if (!fs.existsSync(path)) { return }

    return getMd5(fs.readFileSync(path, 'utf8'))
}

var doHarvest = () => {
    getNews('http://www.artun.ee/?feed=newsticker', (err, news) => {
        if (err) { return console.error(err) }

        var sortedNews = JSON.stringify(_.sortBy(news, ['date']))

        if (getMd5(sortedNews) === (newsMd5 || getFileMd5(newsFile))) {
            // console.log((new Date()).toISOString(), `News: not changed`)
        } else {
            fs.writeFile(newsFile, sortedNews, 'utf8', () => {
                newsMd5 = getFileMd5(newsFile)
                console.log((new Date()).toISOString(), `News: ${news.length}`)
            })
        }
    })

    getRooms('https://eka.entu.ee/api/get_entity_list?only_public=true&full_info=true&entity_definition_keyname=room', (err, rooms) => {
        if (err) { return console.error(err) }

        var events = []
        async.each(rooms, (room, callback) => {
            ical.fromURL(room.ical, {}, function(err, data) {
                if (err) {
                    console.error(room.ical)
                    console.error(err)
                    return callback(null)
                }

                for (let k in data) {
                    if (!data.hasOwnProperty(k)) { continue }
                    if (op.get(data, [k, 'type']) !== 'VEVENT') { continue }

                    events.push({
                        title: room.title,
                        info: room.info,
                        start: op.get(data, [k, 'start']),
                        end: op.get(data, [k, 'end']),
                        summary: entities.decode(op.get(data, [k, 'summary'])),
                        description: entities.decode(op.get(data, [k, 'description']))
                    })
                }

                callback(null)
            })
        }, err => {
            if (err) { return console.error(err) }

            var sortedEvents = JSON.stringify(_.sortBy(events, ['start', 'end', 'title']))

            if (getMd5(sortedEvents) === (eventsMd5 || getFileMd5(eventsFile))) {
                // console.log((new Date()).toISOString(), `Events: not changed`)
            } else {
                fs.writeFile(eventsFile, sortedEvents, 'utf8', () => {
                    eventsMd5 = getFileMd5(eventsFile)
                    console.log((new Date()).toISOString(), `Events: ${events.length}`)
                })
            }
        })
    })
}

var server = http.createServer((request, response) => {
    var filePath = path.join(__dirname, 'static', request.url)

    if (filePath.indexOf('.') === -1) {
        filePath = path.join(filePath, 'index.html')
    }

    var contentType = mime.lookup(path.extname(filePath)) || 'application/octet-stream'

    fs.readFile(filePath, (err, content) => {
        if (err) {
            response.writeHead(404, { 'Content-Type': 'text/plain' })
            response.end('404\n')
        } else {
            response.writeHead(200, { 'Content-Type': contentType })
            response.end(content, 'utf-8')
        }
    })
})

server.listen(process.env.PORT)

doHarvest()

setInterval(doHarvest, interval)
<|endoftext|>


<commit_before>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (sauceAccessKey === undefined
    || sauceUsername === undefined) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  ctx.striderMessage("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      clearInterval(intervalId)
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': SAUCE_TEST_TIMEOUT / 1000
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<commit_msg>use '!' instead of '=== undefined' to determine whether sauce is configured or not.<commit_after>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (!sauceAccessKey
    || !sauceUsername) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  ctx.striderMessage("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      clearInterval(intervalId)
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': SAUCE_TEST_TIMEOUT / 1000
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<|endoftext|>


<commit_before>function checkRenameUri(collectionId, data, renameUri, onSave) {
  if (renameUri) {
    swal({
      title: "Warning",
      text: "You have changed the title or edition and this could change the uri. Are you sure you want to proceed?",
      type: "warning",
      showCancelButton: true,
      confirmButtonText: "Change url",
      cancelButtonText: "Cancel",
      closeOnConfirm: true
    }, function (result) {
      if (result === true) {
        // Does it have edition?
        if (data.description.edition) {
          //Special case dataset editions. They have edition but not title
          if (data.description.title) {
            var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          }
          var editionNoSpace = data.description.edition.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          if (data.type === 'dataset') {
            tmpNewUri.splice([tmpNewUri.length - 1], 1, editionNoSpace);
          } else {
            tmpNewUri.splice([tmpNewUri.length - 2], 2, titleNoSpace, editionNoSpace);
          }
          var newUri = tmpNewUri.join("/");
          //is it a compendium? Rename children array
          if (data.type === 'compendium_landing_page') {
            if (data.chapters) {
              data.chapters = renameCompendiumChildren(data.chapters, titleNoSpace, editionNoSpace);
            }
            if (data.datasets) {
              data.datasets = renameCompendiumChildren(data.datasets, titleNoSpace, editionNoSpace);
            }
          }
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            }
          );
          console.log(newUri);
          // is it an adHoc?
        } else if (data.type === 'static_adhoc') {
          var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var referenceNoSpace = data.description.reference.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          tmpNewUri.splice([tmpNewUri.length - 1], 1, referenceNoSpace + titleNoSpace);
          var newUri = tmpNewUri.join("/");
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            }
          );
          console.log(newUri);
        } else {
          var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          //Articles with no edition. Add date as edition
          if (data.type === 'article' || data.type === 'article_download') {
            var editionDate = $.datepicker.formatDate('yy-mm-dd', new Date());
            tmpNewUri.splice([tmpNewUri.length - 2], 2, titleNoSpace, editionDate);
          } else {
            tmpNewUri.splice([tmpNewUri.length - 1], 1, titleNoSpace);
          }
          var newUri = tmpNewUri.join("/");
          //if it is a dataset rename children array
          if (data.type === 'dataset_landing_page') {
            if (data.datasets) {
              data.datasets = renameDatasetChildren(data.datasets, titleNoSpace);
            }
          }
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            }
          );
          console.log(newUri);
        }
      } else {
        refreshPreview(data.uri);
        loadPageDataIntoEditor(data.uri, collectionId);
      }
    });
  } else {
    onSave(collectionId, data.uri, JSON.stringify(data));
  }
}

<commit_msg>Change title/edition on a published item does not change the url<commit_after>function checkRenameUri(collectionId, data, renameUri, onSave) {
  if (renameUri) {
    swal({
      title: "Warning",
      text: "You have changed the title or edition and this could change the uri. Are you sure you want to proceed?",
      type: "warning",
      showCancelButton: true,
      confirmButtonText: "Change url",
      cancelButtonText: "Cancel",
      closeOnConfirm: true
    }, function (result) {
      if (result === true) {
        // Does it have edition?
        if (data.description.edition) {
          //Special case dataset editions. They have edition but not title
          if (data.description.title) {
            var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          }
          var editionNoSpace = data.description.edition.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          if (data.type === 'dataset') {
            tmpNewUri.splice([tmpNewUri.length - 1], 1, editionNoSpace);
          } else {
            tmpNewUri.splice([tmpNewUri.length - 2], 2, titleNoSpace, editionNoSpace);
          }
          var newUri = tmpNewUri.join("/");
          //is it a compendium? Rename children array
          if (data.type === 'compendium_landing_page') {
            if (data.chapters) {
              data.chapters = renameCompendiumChildren(data.chapters, titleNoSpace, editionNoSpace);
            }
            if (data.datasets) {
              data.datasets = renameCompendiumChildren(data.datasets, titleNoSpace, editionNoSpace);
            }
          }
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            },
            onError = function () {
              onSave(collectionId, data.uri, JSON.stringify(data));
            }
          );
          // is it an adHoc?
        } else if (data.type === 'static_adhoc') {
          var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var referenceNoSpace = data.description.reference.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          tmpNewUri.splice([tmpNewUri.length - 1], 1, referenceNoSpace + titleNoSpace);
          var newUri = tmpNewUri.join("/");
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            },
            onError = function () {
              onSave(collectionId, data.uri, JSON.stringify(data));
            }
          );
        } else {
          var titleNoSpace = data.description.title.replace(/[^A-Z0-9]+/ig, "").toLowerCase();
          var tmpNewUri = data.uri.split("/");
          //Articles with no edition. Add date as edition
          if (data.type === 'article' || data.type === 'article_download') {
            var editionDate = $.datepicker.formatDate('yy-mm-dd', new Date());
            tmpNewUri.splice([tmpNewUri.length - 2], 2, titleNoSpace, editionDate);
          } else {
            tmpNewUri.splice([tmpNewUri.length - 1], 1, titleNoSpace);
          }
          var newUri = tmpNewUri.join("/");
          //if it is a dataset rename children array
          if (data.type === 'dataset_landing_page') {
            if (data.datasets) {
              data.datasets = renameDatasetChildren(data.datasets, titleNoSpace);
            }
          }
          moveContent(collectionId, data.uri, newUri,
            onSuccess = function () {
              Florence.globalVars.pagePath = newUri;
              onSave(collectionId, newUri, JSON.stringify(data));
            },
            onError = function () {
              onSave(collectionId, data.uri, JSON.stringify(data));
            }
          );
        }
      } else {
        refreshPreview(data.uri);
        loadPageDataIntoEditor(data.uri, collectionId);
      }
    });
  } else {
    onSave(collectionId, data.uri, JSON.stringify(data));
  }
}

<|endoftext|>


<commit_before>/**
 * Generic error handler method for ajax responses.
 * Apply your specific requirements for an error response and then call this method to take care of the rest.
 * @param response
 */
function handleApiError(response) {

    if (!response || response.status === 200)
        return;

    if (response.status === 403 || response.status === 401) {
        //sweetAlert('You are not logged in', 'Please refresh Florence and log back in.');
        logout();
    } else if (response.status === 504) {
        sweetAlert('This task is taking longer than expected', "It will continue to run in the background.", "info");
    } else {
        var message = 'An error has occurred, please contact an administrator.';

        if (response.responseJSON) {
            message = response.responseJSON.message;
        }

        console.log(message);
        sweetAlert("Error", message, "error");
    }
}

/* Unique error handling for the login screen */
function handleLoginApiError(response) {

    if (!response || response.status === 200)
        return;

    if (response.status === 401) {
        sweetAlert('Incorrect login details', 'These login credentials were not recognised. Please try again.', 'error');
    } else {
        var message = 'An error has occurred, please contact an administrator.';

        if (response.responseJSON) {
            message = response.responseJSON.message;
        }

        console.log(message);
        sweetAlert("Error", message, "error");
    }
}
<commit_msg>Call logout to clear authentication data when an unauthorised response is returned.<commit_after>/**
 * Generic error handler method for ajax responses.
 * Apply your specific requirements for an error response and then call this method to take care of the rest.
 * @param response
 */
function handleApiError(response) {

    if (!response || response.status === 200)
        return;

    if (response.status === 403 || response.status === 401) {
        //sweetAlert('You are not logged in', 'Please refresh Florence and log back in.');
        logout();
    } else if (response.status === 504) {
        sweetAlert('This task is taking longer than expected', "It will continue to run in the background.", "info");
    } else {
        var message = 'An error has occurred, please contact an administrator.';

        if (response.responseJSON) {
            message = response.responseJSON.message;
        }

        console.log(message);
        sweetAlert("Error", message, "error");
    }
}

/* Unique error handling for the login screen */
function handleLoginApiError(response) {

    if (!response || response.status === 200)
        return;

    if (response.status === 403 || response.status === 401) {
        sweetAlert('Incorrect login details', 'These login credentials were not recognised. Please try again.', 'error');
        logout();
    } else {
        var message = 'An error has occurred, please contact an administrator.';

        if (response.responseJSON) {
            message = response.responseJSON.message;
        }

        console.log(message);
        sweetAlert("Error", message, "error");
    }
}
<|endoftext|>


<commit_before>/**
 * Created by Serhii Maksymchuk from Ubrainians for imCode
 * 05.09.17
 */
define("imcms-menus-rest-api", ["imcms-rest-api"], function (rest) {
    let url = '/menus';
    let api = new rest.API(url);

    api.getSortTypes = nested => rest.ajax.call({url: `${url}/sort-types`, type: 'GET', json: false}, nested);
    api.getSortedItems = menuData => rest.ajax.call({url: `${url}/sorting`, type: 'PUT', json: true}, menuData);

    return api;
});
<commit_msg>Issue IMCMS-484: Improve the menu-administration - Remove api types sort<commit_after>/**
 * Created by Serhii Maksymchuk from Ubrainians for imCode
 * 05.09.17
 */
define("imcms-menus-rest-api", ["imcms-rest-api"], function (rest) {
    let url = '/menus';
    let api = new rest.API(url);

    api.getSortedItems = menuData => rest.ajax.call({url: `${url}/sorting`, type: 'PUT', json: true}, menuData);

    return api;
});
<|endoftext|>


<commit_before>import { fileToUrl } from 'uni-platform/helpers/file'
/**
 * 下载任务
 */
class DownloadTask {
  _xhr
  _callbacks = []
  constructor (xhr) {
    this._xhr = xhr
  }
  /**
   * 监听下载进度
   * @param {Function} callback 回调
   */
  onProgressUpdate (callback) {
    if (typeof callback !== 'function') {
      return
    }
    this._callbacks.push(callback)
  }
  /**
   * 停止任务
   */
  abort () {
    if (this._xhr) {
      this._xhr.abort()
      delete this._xhr
    }
  }
}
/**
 * 下载文件
 * @param {*} param0
 * @param {string} callbackId
 * @return {DownloadTask}
 */
export function downloadFile ({
  url,
  header
}, callbackId) {
  var timeout = (__uniConfig.networkTimeout && __uniConfig.networkTimeout.downloadFile) || 60 * 1000
  const {
    invokeCallbackHandler: invoke
  } = UniServiceJSBridge
  var timer
  var xhr = new XMLHttpRequest()
  var downloadTask = new DownloadTask(xhr)
  xhr.open('GET', url, true)
  Object.keys(header).forEach(key => {
    xhr.setRequestHeader(key, header[key])
  })
  xhr.responseType = 'blob'
  xhr.onload = function () {
    clearTimeout(timer)
    let statusCode = xhr.status
    let blob = this.response
    invoke(callbackId, {
      errMsg: 'downloadFile:ok',
      statusCode,
      tempFilePath: fileToUrl(blob)
    })
  }
  xhr.onabort = function () {
    clearTimeout(timer)
    invoke(callbackId, {
      errMsg: 'downloadFile:fail abort'
    })
  }
  xhr.onerror = function () {
    clearTimeout(timer)
    invoke(callbackId, {
      errMsg: 'downloadFile:fail'
    })
  }
  xhr.onprogress = function (event) {
    downloadTask._callbacks.forEach(callback => {
      var totalBytesWritten = event.loaded
      var totalBytesExpectedToWrite = event.total
      var progress = Math.round(totalBytesWritten / totalBytesExpectedToWrite * 100)
      callback({
        progress,
        totalBytesWritten,
        totalBytesExpectedToWrite
      })
    })
  }
  xhr.send()
  timer = setTimeout(function () {
    xhr.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null
    downloadTask.abort()
    invoke(callbackId, {
      errMsg: 'downloadFile:fail timeout'
    })
  }, timeout)
  return downloadTask
}
<commit_msg>fix: H5 DownloadTask 支持 offProgressUpdate<commit_after>import { fileToUrl } from 'uni-platform/helpers/file'
/**
 * 下载任务
 */
class DownloadTask {
  _xhr
  _callbacks = []
  constructor (xhr) {
    this._xhr = xhr
  }
  /**
   * 监听下载进度
   * @param {Function} callback 回调
   */
  onProgressUpdate (callback) {
    if (typeof callback !== 'function') {
      return
    }
    this._callbacks.push(callback)
  }
  offProgressUpdate (callback) {
    const index = this._callbacks.indexOf(callback)
    if (index >= 0) {
      this._callbacks.splice(index, 1)
    }
  }
  /**
   * 停止任务
   */
  abort () {
    if (this._xhr) {
      this._xhr.abort()
      delete this._xhr
    }
  }
}
/**
 * 下载文件
 * @param {*} param0
 * @param {string} callbackId
 * @return {DownloadTask}
 */
export function downloadFile ({
  url,
  header
}, callbackId) {
  var timeout = (__uniConfig.networkTimeout && __uniConfig.networkTimeout.downloadFile) || 60 * 1000
  const {
    invokeCallbackHandler: invoke
  } = UniServiceJSBridge
  var timer
  var xhr = new XMLHttpRequest()
  var downloadTask = new DownloadTask(xhr)
  xhr.open('GET', url, true)
  Object.keys(header).forEach(key => {
    xhr.setRequestHeader(key, header[key])
  })
  xhr.responseType = 'blob'
  xhr.onload = function () {
    clearTimeout(timer)
    let statusCode = xhr.status
    let blob = this.response
    invoke(callbackId, {
      errMsg: 'downloadFile:ok',
      statusCode,
      tempFilePath: fileToUrl(blob)
    })
  }
  xhr.onabort = function () {
    clearTimeout(timer)
    invoke(callbackId, {
      errMsg: 'downloadFile:fail abort'
    })
  }
  xhr.onerror = function () {
    clearTimeout(timer)
    invoke(callbackId, {
      errMsg: 'downloadFile:fail'
    })
  }
  xhr.onprogress = function (event) {
    downloadTask._callbacks.forEach(callback => {
      var totalBytesWritten = event.loaded
      var totalBytesExpectedToWrite = event.total
      var progress = Math.round(totalBytesWritten / totalBytesExpectedToWrite * 100)
      callback({
        progress,
        totalBytesWritten,
        totalBytesExpectedToWrite
      })
    })
  }
  xhr.send()
  timer = setTimeout(function () {
    xhr.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null
    downloadTask.abort()
    invoke(callbackId, {
      errMsg: 'downloadFile:fail timeout'
    })
  }, timeout)
  return downloadTask
}
<|endoftext|>