<commit_before>import { mount } from 'enzyme';
import React from 'react';
import Modal from 'react-modal';
import Lightbox from '../index';

// Mock the loadStyles static function to avoid
// issues with a lack of styles._insertCss
Lightbox.loadStyles = jest.fn();

const commonProps = {
  mainSrc: '/fake/image/src.jpg',
  onCloseRequest: () => {},
};

const extendedCommonProps = {
  ...commonProps,
  prevSrc: '/fake/image/src1.jpg',
  nextSrc: '/fake/image/src2.jpg',
};

describe('Lightbox structure', () => {
  const wrapper = mount(<Lightbox {...commonProps} />);

  it('contains react-modal', () => {
    expect(wrapper.find(Modal).length).toEqual(1);
  });

  it('does not contain prev button when no prevSrc supplied', () => {
    expect(wrapper.find('.ril-prev-button').length).toEqual(0);
  });

  it('contains prev button when prevSrc supplied', () => {
    wrapper.setProps({ prevSrc: '/my/prev/src' });
    expect(wrapper.find('.ril-prev-button').length).toEqual(1);
  });

  it('does not contain next button when no nextSrc supplied', () => {
    expect(wrapper.find('.ril-next-button').length).toEqual(0);
  });

  it('contains next button when nextSrc supplied', () => {
    wrapper.setProps({ nextSrc: '/my/next/src' });
    expect(wrapper.find('.ril-next-button').length).toEqual(1);
  });

  it('contains zoom buttons when enableZoom is true (default)', () => {
    expect(wrapper.find('.ril-zoom-out').length).toEqual(1);
    expect(wrapper.find('.ril-zoom-in').length).toEqual(1);
  });

  it('does not contain zoom buttons when enableZoom is false', () => {
    wrapper.setProps({ enableZoom: false });
    expect(wrapper.find('.ril-zoom-out').length).toEqual(0);
    expect(wrapper.find('.ril-zoom-in').length).toEqual(0);
  });

  it('does not contain a caption bar when no caption is supplied', () => {
    expect(wrapper.find('.ril-caption').length).toEqual(0);
  });

  it('contains a caption bar when a caption is supplied', () => {
    wrapper.setProps({ caption: 'My Caption' });
    expect(wrapper.find('.ril-caption').length).toEqual(0);
  });

  it('contains custom toolbar buttons when supplied', () => {
    wrapper.setProps({
      toolbarButtons: [<button className="my-test-button" />],
    });
    expect(wrapper.find('.ril-toolbar__item .my-test-button').length).toEqual(
      1
    );
  });

  it('contains image title when supplied', () => {
    wrapper.setProps({
      imageTitle:
,
    });

    expect(
      wrapper.find(
        '.ril-toolbar-left .ril-toolbar__item__child .my-image-title'
      ).length
    ).toEqual(1);
  });
});

describe('Events', () => {
  const mockFns = {
    onAfterOpen: jest.fn(),
    onCloseRequest: jest.fn(),
    onMovePrevRequest: jest.fn(),
    onMoveNextRequest: jest.fn(),
  };

  const wrapper = mount(
    <Lightbox {...extendedCommonProps} {...mockFns} animationDisabled />
  );

  it('Calls onAfterOpen when mounted', () => {
    expect(mockFns.onAfterOpen).toHaveBeenCalledTimes(1);
    expect(mockFns.onAfterOpen).toHaveBeenCalledWith();
  });

  it('Calls onMovePrevRequest when left button clicked', () => {
    expect(mockFns.onMovePrevRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-prev-button').simulate('click');
    expect(mockFns.onMovePrevRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onMovePrevRequest).not.toHaveBeenCalledWith();
  });

  it('Calls onMoveNextRequest when right button clicked', () => {
    expect(mockFns.onMoveNextRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-next-button').simulate('click');
    expect(mockFns.onMoveNextRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onMoveNextRequest).not.toHaveBeenCalledWith();
  });

  it('Calls onCloseRequest when close button clicked', () => {
    expect(mockFns.onCloseRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-close').simulate('click');
    expect(mockFns.onCloseRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onCloseRequest).not.toHaveBeenCalledWith();
  });
});

describe('Key bindings', () => {
  const mockCloseRequest = jest.fn();
  const mockMovePrevRequest = jest.fn();
  const mockMoveNextRequest = jest.fn();

  const wrapper = mount(
    <Lightbox
      {...commonProps}
      onCloseRequest={mockCloseRequest}
      onMovePrevRequest={mockMovePrevRequest}
      onMoveNextRequest={mockMoveNextRequest}
    />
  );

  const simulateKey = keyCode => {
    // Avoid interference by key throttling
    wrapper.instance().lastKeyDownTime = new Date('1970-01-01').getTime();

    wrapper.find('.ril-outer').simulate('keyDown', { keyCode });
  };

  it('Responds to close key binding', () => {
    expect(mockCloseRequest).toHaveBeenCalledTimes(0);
    // Simulate ESC key press
    simulateKey(27);
    expect(mockCloseRequest).toHaveBeenCalledTimes(1);
  });

  it('Doesn\'t respond to "move to next" key binding when no next image available', () => {
    // Simulate right arrow key press
    simulateKey(39);
    expect(mockMoveNextRequest).toHaveBeenCalledTimes(0);
  });

  it('Responds to "move to next" key binding when next image available', () => {
    wrapper.setProps({ nextSrc: '/my/next/src' });

    // Simulate right arrow key press
    simulateKey(39);
    expect(mockMoveNextRequest).toHaveBeenCalledTimes(1);
  });

  it('Doesn\'t respond to "move to prev" key binding when no prev image available', () => {
    // Simulate left arrow key press
    simulateKey(37);
    expect(mockMovePrevRequest).toHaveBeenCalledTimes(0);
  });

  it('Responds to "move to prev" key binding', () => {
    wrapper.setProps({ prevSrc: '/my/prev/src' });

    // Simulate left arrow key press
    simulateKey(37);
    expect(mockMovePrevRequest).toHaveBeenCalledTimes(1);
  });
});

describe('Snapshot Testing', () => {
  it('Lightbox renders properly"', () => {
    const wrapper = mount(
      <Lightbox
        {...commonProps}
        reactModalProps={{ appElement: global.document.createElement('div') }}
      />
    );
    expect(wrapper).toMatchSnapshot();
  });
});

describe('Error Testing', () => {
  it('Should render the default error message', () => {
    const wrapper = mount(<Lightbox {...commonProps} />);
    wrapper.setState({
      loadErrorStatus: { mainSrc: true },
    });
    wrapper.update();
    expect(wrapper.find('div.errorContainer')).toHaveText(
      'This image failed to load'
    );
  });
  it('Should render the specified error message', () => {
    const wrapper = mount(<Lightbox {...commonProps} />);
    const imageLoadErrorMessage =
Specified Error Message
;
    wrapper.setState({
      loadErrorStatus: { mainSrc: true },
    });
    wrapper.setProps({
      imageLoadErrorMessage,
    });
    wrapper.update();
    expect(wrapper.find('div.errorContainer')).toContainReact(
      imageLoadErrorMessage
    );
  });
});
<commit_msg>test: add tests for load and load error callbacks<commit_after>import { mount } from 'enzyme';
import React from 'react';
import Modal from 'react-modal';
import Lightbox from '../index';

// Mock the loadStyles static function to avoid
// issues with a lack of styles._insertCss
Lightbox.loadStyles = jest.fn();

const commonProps = {
  mainSrc: '/fake/image/src.jpg',
  onCloseRequest: () => {},
};

const extendedCommonProps = {
  ...commonProps,
  prevSrc: '/fake/image/src1.jpg',
  nextSrc: '/fake/image/src2.jpg',
};

describe('Lightbox structure', () => {
  const wrapper = mount(<Lightbox {...commonProps} />);

  it('contains react-modal', () => {
    expect(wrapper.find(Modal).length).toEqual(1);
  });

  it('does not contain prev button when no prevSrc supplied', () => {
    expect(wrapper.find('.ril-prev-button').length).toEqual(0);
  });

  it('contains prev button when prevSrc supplied', () => {
    wrapper.setProps({ prevSrc: '/my/prev/src' });
    expect(wrapper.find('.ril-prev-button').length).toEqual(1);
  });

  it('does not contain next button when no nextSrc supplied', () => {
    expect(wrapper.find('.ril-next-button').length).toEqual(0);
  });

  it('contains next button when nextSrc supplied', () => {
    wrapper.setProps({ nextSrc: '/my/next/src' });
    expect(wrapper.find('.ril-next-button').length).toEqual(1);
  });

  it('contains zoom buttons when enableZoom is true (default)', () => {
    expect(wrapper.find('.ril-zoom-out').length).toEqual(1);
    expect(wrapper.find('.ril-zoom-in').length).toEqual(1);
  });

  it('does not contain zoom buttons when enableZoom is false', () => {
    wrapper.setProps({ enableZoom: false });
    expect(wrapper.find('.ril-zoom-out').length).toEqual(0);
    expect(wrapper.find('.ril-zoom-in').length).toEqual(0);
  });

  it('does not contain a caption bar when no caption is supplied', () => {
    expect(wrapper.find('.ril-caption').length).toEqual(0);
  });

  it('contains a caption bar when a caption is supplied', () => {
    wrapper.setProps({ caption: 'My Caption' });
    expect(wrapper.find('.ril-caption').length).toEqual(0);
  });

  it('contains custom toolbar buttons when supplied', () => {
    wrapper.setProps({
      toolbarButtons: [<button className="my-test-button" />],
    });
    expect(wrapper.find('.ril-toolbar__item .my-test-button').length).toEqual(
      1
    );
  });

  it('contains image title when supplied', () => {
    wrapper.setProps({
      imageTitle:
,
    });

    expect(
      wrapper.find(
        '.ril-toolbar-left .ril-toolbar__item__child .my-image-title'
      ).length
    ).toEqual(1);
  });
});

describe('Events', () => {
  const LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC';
  const LOAD_SUCCESS_SRC = 'LOAD_SUCCESS_SRC';
  let originalImageSrcProto;
  beforeAll(() => {
    originalImageSrcProto = Object.getOwnPropertyDescriptor(
      global.Image.prototype,
      'src'
    );

    Object.defineProperty(global.Image.prototype, 'src', {
      set(src) {
        if (src === LOAD_FAILURE_SRC) {
          setTimeout(() => this.onerror(new Error('mock error')));
        } else if (src === LOAD_SUCCESS_SRC) {
          setTimeout(this.onload);
        }
      },
    });
  });

  afterAll(() => {
    Object.defineProperty(global.Image.prototype, 'src', originalImageSrcProto);
  });

  const mockFns = {
    onAfterOpen: jest.fn(),
    onCloseRequest: jest.fn(),
    onMovePrevRequest: jest.fn(),
    onMoveNextRequest: jest.fn(),
    onImageLoad: jest.fn(),
    onImageLoadError: jest.fn(),
  };

  const wrapper = mount(
    <Lightbox {...extendedCommonProps} {...mockFns} animationDisabled />
  );

  it('Calls onAfterOpen when mounted', () => {
    expect(mockFns.onAfterOpen).toHaveBeenCalledTimes(1);
    expect(mockFns.onAfterOpen).toHaveBeenCalledWith();
  });

  it('Calls onMovePrevRequest when left button clicked', () => {
    expect(mockFns.onMovePrevRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-prev-button').simulate('click');
    expect(mockFns.onMovePrevRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onMovePrevRequest).not.toHaveBeenCalledWith();
  });

  it('Calls onMoveNextRequest when right button clicked', () => {
    expect(mockFns.onMoveNextRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-next-button').simulate('click');
    expect(mockFns.onMoveNextRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onMoveNextRequest).not.toHaveBeenCalledWith();
  });

  it('Calls onCloseRequest when close button clicked', () => {
    expect(mockFns.onCloseRequest).toHaveBeenCalledTimes(0);
    wrapper.find('.ril-close').simulate('click');
    expect(mockFns.onCloseRequest).toHaveBeenCalledTimes(1);
    expect(mockFns.onCloseRequest).not.toHaveBeenCalledWith();
  });

  it('Calls onImageLoad when image loaded', done => {
    mockFns.onImageLoad.mockImplementationOnce((imageSrc, srcType, image) => {
      expect(imageSrc).toEqual(LOAD_SUCCESS_SRC);
      expect(srcType).toEqual('mainSrc');
      expect(image).toBeInstanceOf(global.Image);
      done();
    });

    expect(mockFns.onImageLoad).toHaveBeenCalledTimes(0);
    wrapper.setProps({ mainSrc: LOAD_SUCCESS_SRC });
  });

  it('Calls onImageLoadError when image loaded', done => {
    mockFns.onImageLoadError.mockImplementationOnce(
      (imageSrc, srcType, image) => {
        expect(imageSrc).toEqual(LOAD_FAILURE_SRC);
        expect(srcType).toEqual('mainSrc');
        expect(image).toBeInstanceOf(Error);
        done();
      }
    );

    expect(mockFns.onImageLoadError).toHaveBeenCalledTimes(0);
    wrapper.setProps({ mainSrc: LOAD_FAILURE_SRC });
  });
});

describe('Key bindings', () => {
  const mockCloseRequest = jest.fn();
  const mockMovePrevRequest = jest.fn();
  const mockMoveNextRequest = jest.fn();

  const wrapper = mount(
    <Lightbox
      {...commonProps}
      onCloseRequest={mockCloseRequest}
      onMovePrevRequest={mockMovePrevRequest}
      onMoveNextRequest={mockMoveNextRequest}
    />
  );

  const simulateKey = keyCode => {
    // Avoid interference by key throttling
    wrapper.instance().lastKeyDownTime = new Date('1970-01-01').getTime();

    wrapper.find('.ril-outer').simulate('keyDown', { keyCode });
  };

  it('Responds to close key binding', () => {
    expect(mockCloseRequest).toHaveBeenCalledTimes(0);
    // Simulate ESC key press
    simulateKey(27);
    expect(mockCloseRequest).toHaveBeenCalledTimes(1);
  });

  it('Doesn\'t respond to "move to next" key binding when no next image available', () => {
    // Simulate right arrow key press
    simulateKey(39);
    expect(mockMoveNextRequest).toHaveBeenCalledTimes(0);
  });

  it('Responds to "move to next" key binding when next image available', () => {
    wrapper.setProps({ nextSrc: '/my/next/src' });

    // Simulate right arrow key press
    simulateKey(39);
    expect(mockMoveNextRequest).toHaveBeenCalledTimes(1);
  });

  it('Doesn\'t respond to "move to prev" key binding when no prev image available', () => {
    // Simulate left arrow key press
    simulateKey(37);
    expect(mockMovePrevRequest).toHaveBeenCalledTimes(0);
  });

  it('Responds to "move to prev" key binding', () => {
    wrapper.setProps({ prevSrc: '/my/prev/src' });

    // Simulate left arrow key press
    simulateKey(37);
    expect(mockMovePrevRequest).toHaveBeenCalledTimes(1);
  });
});

describe('Snapshot Testing', () => {
  it('Lightbox renders properly"', () => {
    const wrapper = mount(
      <Lightbox
        {...commonProps}
        reactModalProps={{ appElement: global.document.createElement('div') }}
      />
    );
    expect(wrapper).toMatchSnapshot();
  });
});

describe('Error Testing', () => {
  it('Should render the default error message', () => {
    const wrapper = mount(<Lightbox {...commonProps} />);
    wrapper.setState({
      loadErrorStatus: { mainSrc: true },
    });
    wrapper.update();
    expect(wrapper.find('div.errorContainer')).toHaveText(
      'This image failed to load'
    );
  });
  it('Should render the specified error message', () => {
    const wrapper = mount(<Lightbox {...commonProps} />);
    const imageLoadErrorMessage =
Specified Error Message
;
    wrapper.setState({
      loadErrorStatus: { mainSrc: true },
    });
    wrapper.setProps({
      imageLoadErrorMessage,
    });
    wrapper.update();
    expect(wrapper.find('div.errorContainer')).toContainReact(
      imageLoadErrorMessage
    );
  });
});
<|endoftext|>


<commit_before>/**
 * @fileoverview RTCMediaHandler
 */

/* RTCMediaHandler
 * @class PeerConnection helper Class.
 * @param {SIP.RTCSession} session
 * @param {Object} [contraints]
 */
(function(SIP){

var RTCMediaHandler = function(session, constraints) {
  this.constraints = constraints || {};

  this.logger = session.ua.getLogger('sip.rtcsession.rtcmediahandler', session.id);
  this.session = session;
  this.localMedia = null;
  this.peerConnection = null;

  this.init(constraints);
};

RTCMediaHandler.prototype = {

  createOffer: function(onSuccess, onFailure) {
    var
      self = this,
      sent = false;

    this.onIceCompleted = function() {
      if (!sent) {
        sent = true;
        onSuccess(self.peerConnection.localDescription.sdp);
      }
    };

    this.peerConnection.createOffer(
      function(sessionDescription){
        self.setLocalDescription(
          sessionDescription,
          onFailure
        );
      },
      function(e) {
        self.logger.error('unable to create offer');
        self.logger.error(e);
        onFailure();
      }
    );

    if (this.peerConnection.iceGatheringState === 'complete' && this.peerConnection.iceConnectionState === 'connected') {
      window.setTimeout(function(){
        self.onIceCompleted();
      },0);
    }
  },

  createAnswer: function(onSuccess, onFailure) {
    var
      self = this,
      sent = false;

    this.onIceCompleted = function() {
      if (!sent) {
        sent = true;
        onSuccess(self.peerConnection.localDescription.sdp);
      }
    };

    this.peerConnection.createAnswer(
      function(sessionDescription){
        self.setLocalDescription(
          sessionDescription,
          onFailure
        );
      },
      function(e) {
        self.logger.error('unable to create answer');
        self.logger.error(e);
        onFailure();
      },
      this.constraints.RTCConstraints
    );

    if (this.peerConnection.iceGatheringState === 'complete' && this.peerConnection.iceConnectionState === 'connected') {
      window.setTimeout(function(){
        self.onIceCompleted();
      },0);
    }
  },

  setLocalDescription: function(sessionDescription, onFailure) {
    var self = this;

    this.peerConnection.setLocalDescription(
      sessionDescription,
      function(){},
      function(e) {
        self.logger.error('unable to set local description');
        self.logger.error(e);
        onFailure();
      }
    );
  },

  addStream: function(stream, onSuccess, onFailure, constraints) {
    try {
      this.peerConnection.addStream(stream, constraints);
    } catch(e) {
      this.logger.error('error adding stream');
      this.logger.error(e);
      onFailure();
      return;
    }

    onSuccess();
  },

  /**
  * peerConnection creation.
  * @param {Function} onSuccess Fired when there are no more ICE candidates
  */
  init: function(options) {
    options = options || {};

    var idx, length, server,
      self = this,
      servers = [],
      constraints = options.RTCConstraints || {},
      stun_servers = options.stun_servers || null,
      turn_servers = options.turn_servers || null,
      config = this.session.ua.configuration;

    if (!stun_servers) {
      stun_servers = config.stun_servers;
    }

    if(!turn_servers) {
      turn_servers = config.turn_servers;
    }

    servers.push({'url': stun_servers});

    length = turn_servers.length;
    for (idx = 0; idx < length; idx++) {
      server = turn_servers[idx];
      servers.push({
        'url': server.urls,
        'username': server.username,
        'credential': server.password
      });
    }

    this.peerConnection = new SIP.WebRTC.RTCPeerConnection({'iceServers': servers}, constraints);

    this.peerConnection.onaddstream = function(e) {
      self.logger.log('stream added: '+ e.stream.id);
    };

    this.peerConnection.onremovestream = function(e) {
      self.logger.log('stream removed: '+ e.stream.id);
    };

    this.peerConnection.onicecandidate = function(e) {
      if (e.candidate) {
        self.logger.log('ICE candidate received: '+ e.candidate.candidate);
      } else if (self.onIceCompleted !== undefined) {
        self.onIceCompleted();
      }
    };

    this.peerConnection.oniceconnectionstatechange = function(e) {
      self.logger.log('ICE connection state changed to "'+ this.iceConnectionState +'"');
      if (this.iceConnectionState === 'disconnected') {
        self.session.terminate({
          cause: SIP.C.causes.RTP_TIMEOUT,
          status_code: 200,
          reason_phrase: SIP.C.causes.RTP_TIMEOUT
        });
      } else if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') {
        self.onIceCompleted();
      }
    };

    this.peerConnection.onstatechange = function() {
      self.logger.log('PeerConnection state changed to "'+ this.readyState +'"');
    };
  },

  close: function() {
    this.logger.log('closing PeerConnection');
    if(this.peerConnection) {
      this.peerConnection.close();

      if(this.localMedia) {
        this.localMedia.stop();
      }
    }
  },

  /**
  * @param {Object} mediaConstraints
  * @param {Function} onSuccess
  * @param {Function} onFailure
  */
  getUserMedia: function(onSuccess, onFailure, mediaConstraints) {
    var self = this;

    this.logger.log('requesting access to local media');

    SIP.WebRTC.getUserMedia(mediaConstraints,
      function(stream) {
        self.logger.log('got local media stream');
        self.localMedia = stream;
        onSuccess(stream);
      },
      function(e) {
        self.logger.error('unable to get user media');
        self.logger.error(e);
        onFailure();
      }
    );
  },

  /**
  * Message reception.
  * @param {String} type
  * @param {String} sdp
  * @param {Function} onSuccess
  * @param {Function} onFailure
  */
  onMessage: function(type, body, onSuccess, onFailure) {
    this.peerConnection.setRemoteDescription(
      new SIP.WebRTC.RTCSessionDescription({type: type, sdp:body}),
      onSuccess,
      onFailure
    );
  }
};

// Return since it will be assigned to a variable.
return RTCMediaHandler;
}(SIP));
<commit_msg>[47] commented out disconnect ICE state change to work with gatewayed/Bria video calls<commit_after>/**
 * @fileoverview RTCMediaHandler
 */

/* RTCMediaHandler
 * @class PeerConnection helper Class.
 * @param {SIP.RTCSession} session
 * @param {Object} [contraints]
 */
(function(SIP){

var RTCMediaHandler = function(session, constraints) {
  this.constraints = constraints || {};

  this.logger = session.ua.getLogger('sip.rtcsession.rtcmediahandler', session.id);
  this.session = session;
  this.localMedia = null;
  this.peerConnection = null;

  this.init(constraints);
};

RTCMediaHandler.prototype = {

  createOffer: function(onSuccess, onFailure) {
    var
      self = this,
      sent = false;

    this.onIceCompleted = function() {
      if (!sent) {
        sent = true;
        onSuccess(self.peerConnection.localDescription.sdp);
      }
    };

    this.peerConnection.createOffer(
      function(sessionDescription){
        self.setLocalDescription(
          sessionDescription,
          onFailure
        );
      },
      function(e) {
        self.logger.error('unable to create offer');
        self.logger.error(e);
        onFailure();
      }
    );

    if (this.peerConnection.iceGatheringState === 'complete' && this.peerConnection.iceConnectionState === 'connected') {
      window.setTimeout(function(){
        self.onIceCompleted();
      },0);
    }
  },

  createAnswer: function(onSuccess, onFailure) {
    var
      self = this,
      sent = false;

    this.onIceCompleted = function() {
      if (!sent) {
        sent = true;
        onSuccess(self.peerConnection.localDescription.sdp);
      }
    };

    this.peerConnection.createAnswer(
      function(sessionDescription){
        self.setLocalDescription(
          sessionDescription,
          onFailure
        );
      },
      function(e) {
        self.logger.error('unable to create answer');
        self.logger.error(e);
        onFailure();
      },
      this.constraints.RTCConstraints
    );

    if (this.peerConnection.iceGatheringState === 'complete' && this.peerConnection.iceConnectionState === 'connected') {
      window.setTimeout(function(){
        self.onIceCompleted();
      },0);
    }
  },

  setLocalDescription: function(sessionDescription, onFailure) {
    var self = this;

    this.peerConnection.setLocalDescription(
      sessionDescription,
      function(){},
      function(e) {
        self.logger.error('unable to set local description');
        self.logger.error(e);
        onFailure();
      }
    );
  },

  addStream: function(stream, onSuccess, onFailure, constraints) {
    try {
      this.peerConnection.addStream(stream, constraints);
    } catch(e) {
      this.logger.error('error adding stream');
      this.logger.error(e);
      onFailure();
      return;
    }

    onSuccess();
  },

  /**
  * peerConnection creation.
  * @param {Function} onSuccess Fired when there are no more ICE candidates
  */
  init: function(options) {
    options = options || {};

    var idx, length, server,
      self = this,
      servers = [],
      constraints = options.RTCConstraints || {},
      stun_servers = options.stun_servers || null,
      turn_servers = options.turn_servers || null,
      config = this.session.ua.configuration;

    if (!stun_servers) {
      stun_servers = config.stun_servers;
    }

    if(!turn_servers) {
      turn_servers = config.turn_servers;
    }

    servers.push({'url': stun_servers});

    length = turn_servers.length;
    for (idx = 0; idx < length; idx++) {
      server = turn_servers[idx];
      servers.push({
        'url': server.urls,
        'username': server.username,
        'credential': server.password
      });
    }

    this.peerConnection = new SIP.WebRTC.RTCPeerConnection({'iceServers': servers}, constraints);

    this.peerConnection.onaddstream = function(e) {
      self.logger.log('stream added: '+ e.stream.id);
    };

    this.peerConnection.onremovestream = function(e) {
      self.logger.log('stream removed: '+ e.stream.id);
    };

    this.peerConnection.onicecandidate = function(e) {
      if (e.candidate) {
        self.logger.log('ICE candidate received: '+ e.candidate.candidate);
      } else if (self.onIceCompleted !== undefined) {
        self.onIceCompleted();
      }
    };

    this.peerConnection.oniceconnectionstatechange = function(e) {
      self.logger.log('ICE connection state changed to "'+ this.iceConnectionState +'"');
      //Bria state changes are always connected -> disconnected -> connected, so session gets termi
     /* if (this.iceConnectionState === 'disconnected') {
        self.session.terminate({
          cause: SIP.C.causes.RTP_TIMEOUT,
          status_code: 200,
          reason_phrase: SIP.C.causes.RTP_TIMEOUT
        });
      } else*/ if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') {
        self.onIceCompleted();
      }
    };

    this.peerConnection.onstatechange = function() {
      self.logger.log('PeerConnection state changed to "'+ this.readyState +'"');
    };
  },

  close: function() {
    this.logger.log('closing PeerConnection');
    if(this.peerConnection) {
      this.peerConnection.close();

      if(this.localMedia) {
        this.localMedia.stop();
      }
    }
  },

  /**
  * @param {Object} mediaConstraints
  * @param {Function} onSuccess
  * @param {Function} onFailure
  */
  getUserMedia: function(onSuccess, onFailure, mediaConstraints) {
    var self = this;

    this.logger.log('requesting access to local media');

    SIP.WebRTC.getUserMedia(mediaConstraints,
      function(stream) {
        self.logger.log('got local media stream');
        self.localMedia = stream;
        onSuccess(stream);
      },
      function(e) {
        self.logger.error('unable to get user media');
        self.logger.error(e);
        onFailure();
      }
    );
  },

  /**
  * Message reception.
  * @param {String} type
  * @param {String} sdp
  * @param {Function} onSuccess
  * @param {Function} onFailure
  */
  onMessage: function(type, body, onSuccess, onFailure) {
    this.peerConnection.setRemoteDescription(
      new SIP.WebRTC.RTCSessionDescription({type: type, sdp:body}),
      onSuccess,
      onFailure
    );
  }
};

// Return since it will be assigned to a variable.
return RTCMediaHandler;
}(SIP));
<|endoftext|>


<commit_before>const { IndexedCramFile, CraiIndex } = cjsRequire('@gmod/cram/src')
const { Buffer } = cjsRequire('buffer')

define( [
            'dojo/_base/declare',
            'JBrowse/Store/SeqFeature',
            'JBrowse/Store/DeferredStatsMixin',
            'JBrowse/Store/DeferredFeaturesMixin',
            'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
            'JBrowse/Model/XHRBlob',
            'JBrowse/Model/SimpleFeature',
        ],
        function(
            declare,
            SeqFeatureStore,
            DeferredStatsMixin,
            DeferredFeaturesMixin,
            GlobalStatsEstimationMixin,
            XHRBlob,
            SimpleFeature,
        ) {

// wrapper class to make old JBrowse *Blob data access classes work with
// the new-style filehandle API expected by the cram code

class BlobWrapper {
    constructor(oldStyleBlob) {
        this.blob = oldStyleBlob
    }

    read(buffer, offset = 0, length, position) {
        return new Promise((resolve,reject) => {
            this.blob.read(
                position,
                length,
                dataArrayBuffer => {
                    const data = Buffer.from(dataArrayBuffer)
                    data.copy(buffer, offset)
                    resolve()
                },
                reject,
            )
        })
    }

    readFile() {
        return new Promise((resolve, reject) => {
            this.blob.fetch( dataArrayBuffer => {
                resolve(Buffer.from(dataArrayBuffer))
            }, reject)
        })
    }

    stat() {
        return new Promise((resolve, reject) => {
            this.blob.stat(resolve, reject)
        })
    }
}

return declare( [ SeqFeatureStore, DeferredStatsMixin, DeferredFeaturesMixin, GlobalStatsEstimationMixin ],

/**
 * @lends JBrowse.Store.SeqFeature.CRAM
 */
{
    /**
     * Data backend for reading feature data directly from a
     * web-accessible CRAM file.
     *
     * @constructs
     */
    constructor: function( args ) {
        const cramArgs = {}

        let dataBlob
        if (args.cram)
            dataBlob = args.cram
        else if (args.urlTemplate)
            dataBlob = new XHRBlob(this.resolveUrl(args.urlTemplate || 'data.cram'))
        else throw new Error('must provide either `cram` or `urlTemplate`')
        cramArgs.cramFilehandle = new BlobWrapper(dataBlob)

        if (args.crai)
            cramArgs.index = new CraiIndex({ filehandle: new BlobWrapper(args.crai)})
        else if (args.craiUrlTemplate)
            cramArgs.index = new CraiIndex({filehandle: new BlobWrapper(new XHRBlob(this.resolveUrl(args.craiUrlTemplate)))})
        else if (args.urlTemplate)
            cramArgs.index = new CraiIndex({filehandle: new BlobWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate+'.crai')))})
        else throw new Error('no index provided, must provide a CRAM index')
        // TODO: need to add .csi index support

        cramArgs.seqFetch = this._seqFetch.bind(this)
        cramArgs.checkSequenceMD5 = false

        this.cram = new IndexedCramFile(cramArgs)

        this.source = ( dataBlob.url  ? dataBlob.url.match( /\/([^/\#\?]+)($|[\#\?])/ )[1] :
                        dataBlob.blob ? dataBlob.blob.name : undefined ) || undefined;

        // pre-download the index before running the statistics estimation so that the stats
        // estimation doesn't time out
        this.cram.hasDataForReferenceSequence(0)
            .then(() => this.cram.cram.getSamHeader())
            .then(samHeader => {
                this._setSamHeader(samHeader)
            })
            .then(() => {
                this._deferred.features.resolve({success:true});
            })
            .then(() => this._estimateGlobalStats())
            .then(stats => {
                this.globalStats = stats;
                this._deferred.stats.resolve({success:true});
            })
            .catch(err => {
                this._deferred.features.reject(err)
                this._deferred.stats.reject(err)
            })

        this.storeTimeout = args.storeTimeout || 3000;
    },

    // process the parsed SAM header from the cram file
    _setSamHeader(samHeader) {
        this._samHeader = {}

        // use the @SQ lines in the header to figure out the
        // mapping between ref seq ID numbers and names
        const refSeqIdToName = []
        const refSeqNameToId = {}
        const sqLines = samHeader.filter(l => l.tag === 'SQ')
        sqLines.forEach((sqLine, seqId) => {
            sqLine.data.forEach(item => {
                if (item.tag === 'SN') {
                    // this is the seq name
                    const seqName = item.value
                    refSeqNameToId[seqName] = seqId
                    refSeqIdToName[seqId] = seqName
                }
            })
        })
        if (refSeqIdToName.length) {
            this._samHeader.refSeqIdToName = refSeqIdToName
            this._samHeader.refSeqNameToId = refSeqNameToId
        }
    },

    _refNameToId(refName) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqNameToId)
            return this._samHeader.refSeqNameToId[refName]
        else
            return this.browser.getRefSeqNumber(refName)
    },

    _refIdToName(refId) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqIdToName) {
            return this._samHeader.refSeqIdToName[refId]
        } else {
            let ref = this.browser.getRefSeqById(refId)
            return ref ? ref.name : undefined
        }
    },

    _getRefSeqStore() {
        return new Promise((resolve,reject) => {
            this.browser.getStore('refseqs',resolve,reject)
        })
    },

    // used by the CRAM backend to fetch a region of the underlying reference
    // sequence.  needed for some of its calculations
    async _seqFetch(seqId, start, end) {
        start -= 1 // convert from 1-based closed to interbase

        const refSeqStore = await this._getRefSeqStore()
        if (!refSeqStore) return undefined
        const refName = this._refIdToName(seqId)
        if (!refName) return undefined

        const seqChunks = await new Promise((resolve,reject) => {
            let features = []
            refSeqStore.getFeatures(
                {ref: refName, start: start-1, end},
                f => features.push(f),
                () => resolve(features),
                reject
            )
        })

        const trimmed = []
        seqChunks
        .sort((a,b) => a.get('start') - b.get('start'))
        .forEach( (chunk,i) => {
            let chunkStart = chunk.get('start')
            let chunkEnd = chunk.get('end')
            let trimStart = Math.max(start - chunkStart, 0)
            let trimEnd = Math.min(end - chunkStart, chunkEnd-chunkStart)
            let trimLength = trimEnd - trimStart
            let chunkSeq = chunk.get('seq') || chunk.get('residues')
            trimmed.push(chunkSeq.substr(trimStart,trimLength))
        })

        const sequence = trimmed.join('')
        if (sequence.length !== (end-start))
            throw new Error('sequence fetch sanity check failed')
        return sequence
    },

    /**
     * Interrogate whether a store has data for a given reference
     * sequence.  Calls the given callback with either true or false.
     *
     * Implemented as a binary interrogation because some stores are
     * smart enough to regularize reference sequence names, while
     * others are not.
     */
    hasRefSeq: function( seqName, callback, errorCallback ) {
        seqName = this.browser.regularizeReferenceName( seqName );
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) callback(false)

        this._deferred.stats
        .then(() => this.cram.hasDataForReferenceSequence(refSeqNumber))
        .then(callback, errorCallback)
    },

    // called by getFeatures from the DeferredFeaturesMixin
    _getFeatures: function( query, featCallback, endCallback, errorCallback ) {
        //this.bam.fetch( query.ref ? query.ref : this.refSeq.name, query.start, query.end, featCallback, endCallback, errorCallback );
        const seqName = query.ref || this.refSeq.name
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) {
            endCallback()
            return
        }

        this.cram.getRecordsForRange(refSeqNumber, query.start + 1, query.end)
            .then(records => {
                for (let i = 0; i < records.length; i+= 1) {
                    featCallback(this._cramRecordToFeature(records[i]))
                }

                endCallback()
            })
            .catch(errorCallback)
    },

    _cramRecordToFeature(record) {
        const data = {
            name: record.readName,
            start: record.alignmentStart-1,
            end: record.alignmentStart+record.lengthOnRef-1,
            cram_read_features: record.readFeatures,
            type: 'match',
            MQ: record.mappingQuality,
            flags: `0x${record.flags.toString(16)}`,
            cramFlags: `0x${record.cramFlags.toString(16)}`,
            strand: record.isReverseComplemented() ? -1 : 1,
            qual: (record.qualityScores || []).map(q => q+33).join(' '),
            seq: record.readBases,
            seq_id: this._refIdToName(record.sequenceId),

            qc_failed: record.isFailedQc(),
            secondary_alignment: record.isSecondary(),
            supplementary_alignment: record.isSupplementary(),
            multi_segment_template: record.isPaired(),
            multi_segment_all_correctly_aligned: record.isProperlyPaired(),
            multi_segment_next_segment_unmapped: record.isMateUnmapped(),
            multi_segment_first: record.isRead1(),
            multi_segment_last: record.isRead2(),
            multi_segment_next_segment_reversed: record.isMateReverseComplemented(),
            unmapped: record.isSegmentUnmapped(),
            next_seq_id: record.mate ? this._refIdToName(record.mate.sequenceId) : undefined,
            next_segment_position: record.mate
                ? ( this._refIdToName(record.mate.sequenceId)+':'+record.mate.alignmentStart) : undefined,
        }
        Object.assign(data,record.tags || {})
        return new SimpleFeature({
            data,
            id: record.uniqueId
        })
    },

    saveStore: function() {
        return {
            urlTemplate: this.config.bam.url,
            baiUrlTemplate: this.config.bai.url
        };
    }

});
});
<commit_msg>map CramSizeLimitError to JBrowse Errors.DataOverflow<commit_after>const { IndexedCramFile, CraiIndex } = cjsRequire('@gmod/cram/src')
const { CramSizeLimitError } = cjsRequire('@gmod/cram/src/errors')

const { Buffer } = cjsRequire('buffer')

define( [
            'dojo/_base/declare',
            'JBrowse/Errors',
            'JBrowse/Store/SeqFeature',
            'JBrowse/Store/DeferredStatsMixin',
            'JBrowse/Store/DeferredFeaturesMixin',
            'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
            'JBrowse/Model/XHRBlob',
            'JBrowse/Model/SimpleFeature',
        ],
        function(
            declare,
            Errors,
            SeqFeatureStore,
            DeferredStatsMixin,
            DeferredFeaturesMixin,
            GlobalStatsEstimationMixin,
            XHRBlob,
            SimpleFeature,
        ) {

// wrapper class to make old JBrowse *Blob data access classes work with
// the new-style filehandle API expected by the cram code

class BlobWrapper {
    constructor(oldStyleBlob) {
        this.blob = oldStyleBlob
    }

    read(buffer, offset = 0, length, position) {
        return new Promise((resolve,reject) => {
            this.blob.read(
                position,
                length,
                dataArrayBuffer => {
                    const data = Buffer.from(dataArrayBuffer)
                    data.copy(buffer, offset)
                    resolve()
                },
                reject,
            )
        })
    }

    readFile() {
        return new Promise((resolve, reject) => {
            this.blob.fetch( dataArrayBuffer => {
                resolve(Buffer.from(dataArrayBuffer))
            }, reject)
        })
    }

    stat() {
        return new Promise((resolve, reject) => {
            this.blob.stat(resolve, reject)
        })
    }
}

return declare( [ SeqFeatureStore, DeferredStatsMixin, DeferredFeaturesMixin, GlobalStatsEstimationMixin ],

/**
 * @lends JBrowse.Store.SeqFeature.CRAM
 */
{
    /**
     * Data backend for reading feature data directly from a
     * web-accessible CRAM file.
     *
     * @constructs
     */
    constructor: function( args ) {
        const cramArgs = {}

        let dataBlob
        if (args.cram)
            dataBlob = args.cram
        else if (args.urlTemplate)
            dataBlob = new XHRBlob(this.resolveUrl(args.urlTemplate || 'data.cram'))
        else throw new Error('must provide either `cram` or `urlTemplate`')
        cramArgs.cramFilehandle = new BlobWrapper(dataBlob)

        if (args.crai)
            cramArgs.index = new CraiIndex({ filehandle: new BlobWrapper(args.crai)})
        else if (args.craiUrlTemplate)
            cramArgs.index = new CraiIndex({filehandle: new BlobWrapper(new XHRBlob(this.resolveUrl(args.craiUrlTemplate)))})
        else if (args.urlTemplate)
            cramArgs.index = new CraiIndex({filehandle: new BlobWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate+'.crai')))})
        else throw new Error('no index provided, must provide a CRAM index')
        // TODO: need to add .csi index support

        cramArgs.seqFetch = this._seqFetch.bind(this)
        cramArgs.checkSequenceMD5 = false

        this.cram = new IndexedCramFile(cramArgs)

        this.source = ( dataBlob.url  ? dataBlob.url.match( /\/([^/\#\?]+)($|[\#\?])/ )[1] :
                        dataBlob.blob ? dataBlob.blob.name : undefined ) || undefined;

        // pre-download the index before running the statistics estimation so that the stats
        // estimation doesn't time out
        this.cram.hasDataForReferenceSequence(0)
            .then(() => this.cram.cram.getSamHeader())
            .then(samHeader => {
                this._setSamHeader(samHeader)
            })
            .then(() => {
                this._deferred.features.resolve({success:true});
            })
            .then(() => this._estimateGlobalStats())
            .then(stats => {
                this.globalStats = stats;
                this._deferred.stats.resolve({success:true});
            })
            .catch(err => {
                this._deferred.features.reject(err)
                this._deferred.stats.reject(err)
            })

        this.storeTimeout = args.storeTimeout || 3000;
    },

    // process the parsed SAM header from the cram file
    _setSamHeader(samHeader) {
        this._samHeader = {}

        // use the @SQ lines in the header to figure out the
        // mapping between ref seq ID numbers and names
        const refSeqIdToName = []
        const refSeqNameToId = {}
        const sqLines = samHeader.filter(l => l.tag === 'SQ')
        sqLines.forEach((sqLine, seqId) => {
            sqLine.data.forEach(item => {
                if (item.tag === 'SN') {
                    // this is the seq name
                    const seqName = item.value
                    refSeqNameToId[seqName] = seqId
                    refSeqIdToName[seqId] = seqName
                }
            })
        })
        if (refSeqIdToName.length) {
            this._samHeader.refSeqIdToName = refSeqIdToName
            this._samHeader.refSeqNameToId = refSeqNameToId
        }
    },

    _refNameToId(refName) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqNameToId)
            return this._samHeader.refSeqNameToId[refName]
        else
            return this.browser.getRefSeqNumber(refName)
    },

    _refIdToName(refId) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqIdToName) {
            return this._samHeader.refSeqIdToName[refId]
        } else {
            let ref = this.browser.getRefSeqById(refId)
            return ref ? ref.name : undefined
        }
    },

    _getRefSeqStore() {
        return new Promise((resolve,reject) => {
            this.browser.getStore('refseqs',resolve,reject)
        })
    },

    // used by the CRAM backend to fetch a region of the underlying reference
    // sequence.  needed for some of its calculations
    async _seqFetch(seqId, start, end) {
        start -= 1 // convert from 1-based closed to interbase

        const refSeqStore = await this._getRefSeqStore()
        if (!refSeqStore) return undefined
        const refName = this._refIdToName(seqId)
        if (!refName) return undefined

        const seqChunks = await new Promise((resolve,reject) => {
            let features = []
            refSeqStore.getFeatures(
                {ref: refName, start: start-1, end},
                f => features.push(f),
                () => resolve(features),
                reject
            )
        })

        const trimmed = []
        seqChunks
        .sort((a,b) => a.get('start') - b.get('start'))
        .forEach( (chunk,i) => {
            let chunkStart = chunk.get('start')
            let chunkEnd = chunk.get('end')
            let trimStart = Math.max(start - chunkStart, 0)
            let trimEnd = Math.min(end - chunkStart, chunkEnd-chunkStart)
            let trimLength = trimEnd - trimStart
            let chunkSeq = chunk.get('seq') || chunk.get('residues')
            trimmed.push(chunkSeq.substr(trimStart,trimLength))
        })

        const sequence = trimmed.join('')
        if (sequence.length !== (end-start))
            throw new Error('sequence fetch sanity check failed')
        return sequence
    },

    /**
     * Interrogate whether a store has data for a given reference
     * sequence.  Calls the given callback with either true or false.
     *
     * Implemented as a binary interrogation because some stores are
     * smart enough to regularize reference sequence names, while
     * others are not.
     */
    hasRefSeq: function( seqName, callback, errorCallback ) {
        seqName = this.browser.regularizeReferenceName( seqName );
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) callback(false)

        this._deferred.stats
        .then(() => this.cram.hasDataForReferenceSequence(refSeqNumber))
        .then(callback, errorCallback)
    },

    // called by getFeatures from the DeferredFeaturesMixin
    _getFeatures: function( query, featCallback, endCallback, errorCallback ) {
        //this.bam.fetch( query.ref ? query.ref : this.refSeq.name, query.start, query.end, featCallback, endCallback, errorCallback );
        const seqName = query.ref || this.refSeq.name
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) {
            endCallback()
            return
        }

        this.cram.getRecordsForRange(refSeqNumber, query.start + 1, query.end)
            .then(records => {
                for (let i = 0; i < records.length; i+= 1) {
                    featCallback(this._cramRecordToFeature(records[i]))
                }

                endCallback()
            })
            .catch(err => {
                // map the CramSizeLimitError to JBrowse Errors.DataOverflow
                if (err instanceof CramSizeLimitError) {
                    err = new Errors.DataOverflow(err.msg)
                }

                errorCallback(err)
            })
    },

    _cramRecordToFeature(record) {
        const data = {
            name: record.readName,
            start: record.alignmentStart-1,
            end: record.alignmentStart+record.lengthOnRef-1,
            cram_read_features: record.readFeatures,
            type: 'match',
            MQ: record.mappingQuality,
            flags: `0x${record.flags.toString(16)}`,
            cramFlags: `0x${record.cramFlags.toString(16)}`,
            strand: record.isReverseComplemented() ? -1 : 1,
            qual: (record.qualityScores || []).map(q => q+33).join(' '),
            seq: record.readBases,
            seq_id: this._refIdToName(record.sequenceId),

            qc_failed: record.isFailedQc(),
            secondary_alignment: record.isSecondary(),
            supplementary_alignment: record.isSupplementary(),
            multi_segment_template: record.isPaired(),
            multi_segment_all_correctly_aligned: record.isProperlyPaired(),
            multi_segment_next_segment_unmapped: record.isMateUnmapped(),
            multi_segment_first: record.isRead1(),
            multi_segment_last: record.isRead2(),
            multi_segment_next_segment_reversed: record.isMateReverseComplemented(),
            unmapped: record.isSegmentUnmapped(),
            next_seq_id: record.mate ? this._refIdToName(record.mate.sequenceId) : undefined,
            next_segment_position: record.mate
                ? ( this._refIdToName(record.mate.sequenceId)+':'+record.mate.alignmentStart) : undefined,
        }
        Object.assign(data,record.tags || {})
        return new SimpleFeature({
            data,
            id: record.uniqueId
        })
    },

    saveStore: function() {
        return {
            urlTemplate: this.config.bam.url,
            baiUrlTemplate: this.config.bai.url
        };
    }

});
});
<|endoftext|>


<commit_before>define([
           'dojo/_base/declare',
           'dojo/_base/array',
           'dojo/_base/lang',
           'JBrowse/Util/FastPromise',
           'JBrowse/View/FeatureGlyph',
           './_FeatureLabelMixin'
       ],
       function(
           declare,
           array,
           lang,
           FastPromise,
           FeatureGlyph,
           FeatureLabelMixin
       ) {

return declare([ FeatureGlyph, FeatureLabelMixin], {

    constructor: function() {
        this._embeddedImagePromises = {};
    },

    _defaultConfig: function() {
        return this._mergeConfigs(
            this.inherited(arguments),
            {
                style: {
                    maxDescriptionLength: 70,

                    color: 'goldenrod',
                    mouseovercolor: 'rgba(0,0,0,0.3)',
                    borderColor: null,
                    borderWidth: 0.5,
                    height: 11,
                    marginBottom: 2,

                    strandArrow: true,

                    label: 'name, id',
                    textFont: 'normal 12px Univers,Helvetica,Arial,sans-serif',
                    textColor:  'black',
                    text2Color: 'blue',
                    text2Font: 'normal 12px Univers,Helvetica,Arial,sans-serif',

                    description: 'note, description'
                }
            });
    },

    _getFeatureHeight: function( viewArgs, feature ) {
        var h = this.getStyle( feature, 'height');

        if( viewArgs.displayMode == 'compact' )
            h = Math.round( 0.45 * h );

        if( this.getStyle( feature, 'strandArrow' ) ) {
            var strand = feature.get('strand');
            if( strand == 1 )
                h = Math.max( this._embeddedImages.plusArrow.height, h );
            else if( strand == -1 )
                h = Math.max( this._embeddedImages.minusArrow.height, h );
        }

        return h;
    },

    _getFeatureRectangle: function( viewArgs, feature ) {
        var block = viewArgs.block;
        var fRect = {
            l: block.bpToX( feature.get('start') ),
            h: this._getFeatureHeight(viewArgs, feature),
            viewInfo: viewArgs,
            f: feature,
            glyph: this
        };

        fRect.w = block.bpToX( feature.get('end') ) - fRect.l;

        // save the original rect in `rect` as the dimensions
        // we'll use for the rectangle itself
        fRect.rect = { l: fRect.l, h: fRect.h, w: Math.max( fRect.w, 2 ), t: 0 };
        fRect.w = fRect.rect.w; // in case it was increased
        if( viewArgs.displayMode != 'compact' )
            fRect.h += this.getStyle( feature, 'marginBottom' ) || 0
;
        // if we are showing strand arrowheads, expand the frect a little
        if( this.getStyle( feature, 'strandArrow') ) {
            var strand = fRect.strandArrow = feature.get('strand');

            if( strand == -1 ) {
                var i = this._embeddedImages.minusArrow;
                fRect.w += i.width;
                fRect.l -= i.width;
            }
            else {
                var i = this._embeddedImages.plusArrow;
                fRect.w += i.width;
            }
        }

        // no labels or descriptions if displayMode is collapsed, so stop here
        if( viewArgs.displayMode == "collapsed")
            return fRect;

        this._expandRectangleWithLabels( viewArgs, feature, fRect );
        this._addMasksToRect( viewArgs, feature, fRect );

        return fRect;
    },

    layoutFeature: function( viewArgs, layout, feature ) {
        var rect = this.inherited( arguments );
        if( ! rect ) return rect;

        // need to set the top of the inner rect
        rect.rect.t = rect.t;

        return rect;
    },

    // given an under-construction feature layout rectangle, expand it
    // to accomodate a label and/or a description
    _expandRectangleWithLabels: function( viewArgs, feature, fRect ) {
        // maybe get the feature's name, and update the layout box
        // accordingly
        if( viewArgs.showLabels ) {
            var label = this.makeFeatureLabel( feature, fRect );
            if( label ) {
                fRect.h += label.h;
                fRect.w = Math.max( label.w, fRect.w );
                fRect.label = label;
                label.yOffset = fRect.rect.h + label.h;
            }
        }

        // maybe get the feature's description if available, and
        // update the layout box accordingly
        if( viewArgs.showDescriptions ) {
            var description = this.makeFeatureDescriptionLabel( feature, fRect );
            if( description ) {
                fRect.description = description;
                fRect.h += description.h;
                fRect.w = Math.max( description.w, fRect.w );
                description.yOffset = fRect.h-(this.getStyle( feature, 'marginBottom' ) || 0);
            }
        }
    },

    _embeddedImages: {
         plusArrow: {
             data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAATUlEQVQIW2NkwATGQKFYIG4A4g8gacb///+7AWlBmNq+vj6V4uLiJiD/FRBXA/F8xu7u7kcVFRWyMEVATQz//v0Dcf9CxaYRZxIxbgIARiAhmifVe8UAAAAASUVORK5CYII=",
             width: 9,
             height: 5
         },

         minusArrow: {
             data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAASklEQVQIW2NkQAABILMBiBcD8VkkcQZGIAeEE4G4FYjFent764qKiu4gKXoPUjAJiLOggsxMTEwMjIwgYQjo6Oh4TLRJME043QQA+W8UD/sdk9IAAAAASUVORK5CYII=",
             width: 9,
             height: 5
         }
    },

    /**
     * Returns a promise for an Image object for the image with the
     * given name.  Image data comes from a data URL embedded in this
     * source code.
     */
    getEmbeddedImage: function( name ) {
        return (this._embeddedImagePromises[name] || function() {
                    var p = new FastPromise();
                    var imgRec = this._embeddedImages[ name ];
                    if( ! imgRec ) {
                        p.resolve( null );
                    }
                    else {
                        var i = new Image();
                        var thisB = this;
                        i.onload = function() {
                            p.resolve( this );
                        };
                        i.src = imgRec.data;
                    }
                    return this._embeddedImagePromises[name] = p;
                }.call(this));
    },

    renderFeature: function( context, fRect ) {
        if( this.track.displayMode != 'collapsed' )
            context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w-Math.floor(fRect.l)+fRect.l), fRect.h );

        this.renderBox( context, fRect.viewInfo, fRect.f, fRect.t, fRect.rect.h, fRect.f );
        this.renderLabel( context, fRect );
        this.renderDescription( context, fRect );
        this.renderArrowhead( context, fRect );
    },

    // top and height are in px
    renderBox: function( context, viewInfo, feature, top, overallHeight, parentFeature, style ) {
        var left  = viewInfo.block.bpToX( feature.get('start') );
        var width = viewInfo.block.bpToX( feature.get('end') ) - left;
        //left = Math.round( left );
        //width = Math.round( width );

        style = style || lang.hitch( this, 'getStyle' );

        var height = this._getFeatureHeight( viewInfo, feature );
        if( ! height )
            return;
        if( height != overallHeight )
            top += Math.round( (overallHeight - height)/2 );

        // background
        var bgcolor = style( feature, 'color' );
        if( bgcolor ) {
            context.fillStyle = bgcolor;
            context.fillRect( left, top, Math.max(1,width), height );
        }
        else {
            context.clearRect( left, top, Math.max(1,width), height );
        }

        // foreground border
        var borderColor, lineWidth;
        if( (borderColor = style( feature, 'borderColor' )) && ( lineWidth = style( feature, 'borderWidth')) ) {
            if( width > 3 ) {
                context.lineWidth = lineWidth;
                context.strokeStyle = borderColor;

                // need to stroke a smaller rectangle to remain within
                // the bounds of the feature's overall height and
                // width, because of the way stroking is done in
                // canvas.  thus the +0.5 and -1 business.
                context.strokeRect( left+lineWidth/2, top+lineWidth/2, width-lineWidth, height-lineWidth );
            }
            else {
                context.globalAlpha = lineWidth*2/width;
                context.fillStyle = borderColor;
                context.fillRect( left, top, Math.max(1,width), height );
                context.globalAlpha = 1;
            }
        }
    },

    // feature label is handled by updateStaticElements
    renderLabel: function( context, fRect ) {
    },

    // feature description is handled by updateStaticElements
    renderDescription: function( context, fRect ) {
    },

    // strand arrowhead is sometimes drawn normally, sometimes *also* as a static element
    renderArrowhead: function( context, fRect ) {
        if( fRect.strandArrow ) {
            if( fRect.strandArrow == 1 && fRect.rect.l+fRect.rect.w <= context.canvas.width ) {
                this.getEmbeddedImage( 'plusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, fRect.rect.l + fRect.rect.w, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
            else if( fRect.strandArrow == -1 && fRect.rect.l >= 0 ) {
                this.getEmbeddedImage( 'minusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, fRect.rect.l-9, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
        }
    },

    updateStaticElements( context, fRect, viewArgs ) {
        let vMin = viewArgs.minVisible
        let vMax = viewArgs.maxVisible
        let block = fRect.viewInfo.block

        let bpToPx = viewArgs.bpToPx
        let feature = fRect.f

        let fMin = feature.get('start')
        let fMax = feature.get('end')

        if( fRect.strandArrow ) {
            if( fRect.strandArrow == 1 && fMax >= vMax && fMin <= vMax ) {
                this.getEmbeddedImage( 'plusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, bpToPx(vMax) - bpToPx(vMin) - 9, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
            else if( fRect.strandArrow == -1 && fMin <= vMin && fMax >= vMin ) {
                this.getEmbeddedImage( 'minusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, 0, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
        }

        // if the feature is within the view
        if (!(fMin > vMax || fMax < vMin)) {
            let fRectLeft = fRect.l+bpToPx(block.startBase-vMin+1)

            let clamp = (val,min,max) => Math.min(Math.max(val,min),max)
            let renderText = fLabelRecord => {
                let maxLabelLeft = fRectLeft+fRect.w-fLabelRecord.w
                let labelTop = fRect.t+(fLabelRecord.yOffset||0)
                let labelLeft = fRectLeft+(fLabelRecord.xOffset||0)
                labelLeft = clamp(labelLeft,0,maxLabelLeft)

                context.font = fLabelRecord.font
                context.fillStyle = fLabelRecord.fill
                context.textBaseline = fLabelRecord.baseline

                let clearTop;
                if (fLabelRecord.baseline === 'bottom') {
                    clearTop = labelTop-fLabelRecord.h
                } else if (fLabelRecord.baseline === 'top') {
                    clearTop = labelTop
                } else if (fLabelRecord.baseline === 'middle') {
                    clearTop = labelTop-fLabelRecord.h/2
                }
                if (clearTop) context.clearRect(labelLeft,clearTop,fLabelRecord.w,fLabelRecord.h)

                context.fillText(
                    fLabelRecord.text,
                    labelLeft,
                    labelTop
                )
            }

            if (fRect.label) {
                renderText(fRect.label)
            }
            if (fRect.description) {
                renderText(fRect.description)
            }
        }
    }

});
});
<commit_msg>fix feature labels repeating themselves across the view at some zoom levels<commit_after>define([
           'dojo/_base/declare',
           'dojo/_base/array',
           'dojo/_base/lang',
           'JBrowse/Util/FastPromise',
           'JBrowse/View/FeatureGlyph',
           './_FeatureLabelMixin'
       ],
       function(
           declare,
           array,
           lang,
           FastPromise,
           FeatureGlyph,
           FeatureLabelMixin
       ) {

return declare([ FeatureGlyph, FeatureLabelMixin], {

    constructor: function() {
        this._embeddedImagePromises = {};
    },

    _defaultConfig: function() {
        return this._mergeConfigs(
            this.inherited(arguments),
            {
                style: {
                    maxDescriptionLength: 70,

                    color: 'goldenrod',
                    mouseovercolor: 'rgba(0,0,0,0.3)',
                    borderColor: null,
                    borderWidth: 0.5,
                    height: 11,
                    marginBottom: 2,

                    strandArrow: true,

                    label: 'name, id',
                    textFont: 'normal 12px Univers,Helvetica,Arial,sans-serif',
                    textColor:  'black',
                    text2Color: 'blue',
                    text2Font: 'normal 12px Univers,Helvetica,Arial,sans-serif',

                    description: 'note, description'
                }
            });
    },

    _getFeatureHeight: function( viewArgs, feature ) {
        var h = this.getStyle( feature, 'height');

        if( viewArgs.displayMode == 'compact' )
            h = Math.round( 0.45 * h );

        if( this.getStyle( feature, 'strandArrow' ) ) {
            var strand = feature.get('strand');
            if( strand == 1 )
                h = Math.max( this._embeddedImages.plusArrow.height, h );
            else if( strand == -1 )
                h = Math.max( this._embeddedImages.minusArrow.height, h );
        }

        return h;
    },

    _getFeatureRectangle: function( viewArgs, feature ) {
        var block = viewArgs.block;
        var fRect = {
            l: block.bpToX( feature.get('start') ),
            h: this._getFeatureHeight(viewArgs, feature),
            viewInfo: viewArgs,
            f: feature,
            glyph: this
        };

        fRect.w = block.bpToX( feature.get('end') ) - fRect.l;

        // save the original rect in `rect` as the dimensions
        // we'll use for the rectangle itself
        fRect.rect = { l: fRect.l, h: fRect.h, w: Math.max( fRect.w, 2 ), t: 0 };
        fRect.w = fRect.rect.w; // in case it was increased
        if( viewArgs.displayMode != 'compact' )
            fRect.h += this.getStyle( feature, 'marginBottom' ) || 0
;
        // if we are showing strand arrowheads, expand the frect a little
        if( this.getStyle( feature, 'strandArrow') ) {
            var strand = fRect.strandArrow = feature.get('strand');

            if( strand == -1 ) {
                var i = this._embeddedImages.minusArrow;
                fRect.w += i.width;
                fRect.l -= i.width;
            }
            else {
                var i = this._embeddedImages.plusArrow;
                fRect.w += i.width;
            }
        }

        // no labels or descriptions if displayMode is collapsed, so stop here
        if( viewArgs.displayMode == "collapsed")
            return fRect;

        this._expandRectangleWithLabels( viewArgs, feature, fRect );
        this._addMasksToRect( viewArgs, feature, fRect );

        return fRect;
    },

    layoutFeature: function( viewArgs, layout, feature ) {
        var rect = this.inherited( arguments );
        if( ! rect ) return rect;

        // need to set the top of the inner rect
        rect.rect.t = rect.t;

        return rect;
    },

    // given an under-construction feature layout rectangle, expand it
    // to accomodate a label and/or a description
    _expandRectangleWithLabels: function( viewArgs, feature, fRect ) {
        // maybe get the feature's name, and update the layout box
        // accordingly
        if( viewArgs.showLabels ) {
            var label = this.makeFeatureLabel( feature, fRect );
            if( label ) {
                fRect.h += label.h;
                fRect.w = Math.max( label.w, fRect.w );
                fRect.label = label;
                label.yOffset = fRect.rect.h + label.h;
            }
        }

        // maybe get the feature's description if available, and
        // update the layout box accordingly
        if( viewArgs.showDescriptions ) {
            var description = this.makeFeatureDescriptionLabel( feature, fRect );
            if( description ) {
                fRect.description = description;
                fRect.h += description.h;
                fRect.w = Math.max( description.w, fRect.w );
                description.yOffset = fRect.h-(this.getStyle( feature, 'marginBottom' ) || 0);
            }
        }
    },

    _embeddedImages: {
         plusArrow: {
             data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAATUlEQVQIW2NkwATGQKFYIG4A4g8gacb///+7AWlBmNq+vj6V4uLiJiD/FRBXA/F8xu7u7kcVFRWyMEVATQz//v0Dcf9CxaYRZxIxbgIARiAhmifVe8UAAAAASUVORK5CYII=",
             width: 9,
             height: 5
         },

         minusArrow: {
             data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAASklEQVQIW2NkQAABILMBiBcD8VkkcQZGIAeEE4G4FYjFent764qKiu4gKXoPUjAJiLOggsxMTEwMjIwgYQjo6Oh4TLRJME043QQA+W8UD/sdk9IAAAAASUVORK5CYII=",
             width: 9,
             height: 5
         }
    },

    /**
     * Returns a promise for an Image object for the image with the
     * given name.  Image data comes from a data URL embedded in this
     * source code.
     */
    getEmbeddedImage: function( name ) {
        return (this._embeddedImagePromises[name] || function() {
                    var p = new FastPromise();
                    var imgRec = this._embeddedImages[ name ];
                    if( ! imgRec ) {
                        p.resolve( null );
                    }
                    else {
                        var i = new Image();
                        var thisB = this;
                        i.onload = function() {
                            p.resolve( this );
                        };
                        i.src = imgRec.data;
                    }
                    return this._embeddedImagePromises[name] = p;
                }.call(this));
    },

    renderFeature: function( context, fRect ) {
        if( this.track.displayMode != 'collapsed' )
            context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w-Math.floor(fRect.l)+fRect.l), fRect.h );

        this.renderBox( context, fRect.viewInfo, fRect.f, fRect.t, fRect.rect.h, fRect.f );
        this.renderLabel( context, fRect );
        this.renderDescription( context, fRect );
        this.renderArrowhead( context, fRect );
    },

    // top and height are in px
    renderBox: function( context, viewInfo, feature, top, overallHeight, parentFeature, style ) {
        var left  = viewInfo.block.bpToX( feature.get('start') );
        var width = viewInfo.block.bpToX( feature.get('end') ) - left;
        //left = Math.round( left );
        //width = Math.round( width );

        style = style || lang.hitch( this, 'getStyle' );

        var height = this._getFeatureHeight( viewInfo, feature );
        if( ! height )
            return;
        if( height != overallHeight )
            top += Math.round( (overallHeight - height)/2 );

        // background
        var bgcolor = style( feature, 'color' );
        if( bgcolor ) {
            context.fillStyle = bgcolor;
            context.fillRect( left, top, Math.max(1,width), height );
        }
        else {
            context.clearRect( left, top, Math.max(1,width), height );
        }

        // foreground border
        var borderColor, lineWidth;
        if( (borderColor = style( feature, 'borderColor' )) && ( lineWidth = style( feature, 'borderWidth')) ) {
            if( width > 3 ) {
                context.lineWidth = lineWidth;
                context.strokeStyle = borderColor;

                // need to stroke a smaller rectangle to remain within
                // the bounds of the feature's overall height and
                // width, because of the way stroking is done in
                // canvas.  thus the +0.5 and -1 business.
                context.strokeRect( left+lineWidth/2, top+lineWidth/2, width-lineWidth, height-lineWidth );
            }
            else {
                context.globalAlpha = lineWidth*2/width;
                context.fillStyle = borderColor;
                context.fillRect( left, top, Math.max(1,width), height );
                context.globalAlpha = 1;
            }
        }
    },

    // feature label is handled by updateStaticElements
    renderLabel: function( context, fRect ) {
    },

    // feature description is handled by updateStaticElements
    renderDescription: function( context, fRect ) {
    },

    // strand arrowhead is sometimes drawn normally, sometimes *also* as a static element
    renderArrowhead: function( context, fRect ) {
        if( fRect.strandArrow ) {
            if( fRect.strandArrow == 1 && fRect.rect.l+fRect.rect.w <= context.canvas.width ) {
                this.getEmbeddedImage( 'plusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, fRect.rect.l + fRect.rect.w, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
            else if( fRect.strandArrow == -1 && fRect.rect.l >= 0 ) {
                this.getEmbeddedImage( 'minusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, fRect.rect.l-9, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
        }
    },

    updateStaticElements( context, fRect, viewArgs ) {
        const vMin = viewArgs.minVisible
        const vMax = viewArgs.maxVisible
        const block = fRect.viewInfo.block

        const bpToPx = viewArgs.bpToPx
        const feature = fRect.f

        const fMin = feature.get('start')
        const fMax = feature.get('end')
        const bMin = block.startBase
        const bMax = block.endBase

        if( fRect.strandArrow ) {
            if( fRect.strandArrow == 1 && fMax >= vMax && fMin <= vMax ) {
                this.getEmbeddedImage( 'plusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, bpToPx(vMax) - bpToPx(vMin) - 9, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
            else if( fRect.strandArrow == -1 && fMin <= vMin && fMax >= vMin ) {
                this.getEmbeddedImage( 'minusArrow' )
                    .then( function( img ) {
                               context.imageSmoothingEnabled = false;
                               context.drawImage( img, 0, fRect.t + (fRect.rect.h-img.height)/2 );
                           });
            }
        }

        // if the feature is within the view and within this block
        if (fMin < vMax && fMax > vMin &&  fMin < bMax && fMax > bMin) {
            let fRectLeft = fRect.l+bpToPx(block.startBase-vMin+1)

            let clamp = (val,min,max) => Math.min(Math.max(val,min),max)
            let renderText = fLabelRecord => {
                let maxLabelLeft = fRectLeft+fRect.w-fLabelRecord.w
                let labelTop = fRect.t+(fLabelRecord.yOffset||0)
                let labelLeft = fRectLeft+(fLabelRecord.xOffset||0)
                labelLeft = clamp(labelLeft,0,maxLabelLeft)

                context.font = fLabelRecord.font
                context.fillStyle = fLabelRecord.fill
                context.textBaseline = fLabelRecord.baseline

                let clearTop;
                if (fLabelRecord.baseline === 'bottom') {
                    clearTop = labelTop-fLabelRecord.h
                } else if (fLabelRecord.baseline === 'top') {
                    clearTop = labelTop
                } else if (fLabelRecord.baseline === 'middle') {
                    clearTop = labelTop-fLabelRecord.h/2
                }
                if (clearTop) context.clearRect(labelLeft,clearTop,fLabelRecord.w,fLabelRecord.h)

                context.fillText(
                    fLabelRecord.text,
                    labelLeft,
                    labelTop
                )
            }

            if (fRect.label) {
                renderText(fRect.label)
            }
            if (fRect.description) {
                renderText(fRect.description)
            }
        }
    }

});
});
<|endoftext|>


<commit_before>(function() {
  /*
   * ViolaJones utility.
   * @static
   * @constructor
   */
  tracking.ViolaJones = {};

  /**
   * Holds the minimum area of intersection that defines when a rectangle is
   * from the same group. Often when a face is matched multiple rectangles are
   * classified as possible rectangles to represent the face, when they
   * intersects they are grouped as one face.
   * @type {number}
   * @default 0.5
   * @static
   */
  tracking.ViolaJones.REGIONS_OVERLAP = 0.5;

  /**
   * Holds the block size.
   * @type {number}
   * @default 20
   * @static
   */
  tracking.ViolaJones.BLOCK_SIZE = 20;

  /**
   * Holds the block jump size.
   * @type {number}
   * @default 2
   * @static
   */
  tracking.ViolaJones.BLOCK_JUMP = 2;

  /**
   * Holds the block scale factor.
   * @type {number}
   * @default 1.25
   * @static
   */
  tracking.ViolaJones.BLOCK_SCALE = 1.25;

  /**
   * Detects through the HAAR cascade data rectangles matches.
   * @param {array} The grayscale pixels in a linear [p1,p2,...] array.
   * @param {number} width The image width.
   * @param {number} height The image height.
   * @param {number} data The HAAR cascade data.
   * @return {array} Found rectangles.
   */
  tracking.ViolaJones.detect = function(pixels, width, height, data) {
    var integralImages = tracking.Matrix.computeIntergralImage(pixels, width, height);
    var blockSize = this.BLOCK_SIZE;
    var blockSizeInverse = 1.0 / this.BLOCK_SIZE;
    var maxBlockSize = Math.min(width, height);
    var position = 0;
    var payload = [];

    for (; blockSize <= maxBlockSize; blockSize = (blockSize * this.BLOCK_SCALE + 0.5) | 0) {
      var inverseArea = 1.0 / (blockSize * blockSize);
      var scale = blockSize * blockSizeInverse;

      var xmax = (height - blockSize);
      var ymax = (width - blockSize);
      var jump = (this.BLOCK_JUMP * scale + 0.5) | 0;

      for (var i = 0; i < xmax; i += jump) {
        for (var j = 0; j < ymax; j += jump) {
          if (this.evalStages_(data, integralImages, i, j, width, blockSize, scale, inverseArea)) {
            payload[position++] = {
              size: blockSize,
              x: j,
              y: i
            };
          }
        }
      }
    }
    return this.mergeRectangles_(payload);
  };

  /**
   * Evaluates if the block size on i,j position is a valid HAAR cascade
   * stage.
   * @param {number} data The HAAR cascade data.
   * @param {Array.<Array.<number>>} integralImages Array containing in the
   *     first position the integral image and in the second position the integral
   *     image squared.
   * @param {number} i Vertical position of the pixel to be evaluated.
   * @param {number} j Horizontal position of the pixel to be evaluated.
   * @param {number} width The image width.
   * @param {number} blockSize The block size.
   * @param {number} scale The scale factor of the block size and its original
   *     size.
   * @param {number} inverseArea The inverse area of the block size.
   * @return {boolean} Whether the region passes all the stage tests.
   * @private
   */
  tracking.ViolaJones.evalStages_ = function(data, integralImages, i, j, width, blockSize, scale, inverseArea) {
    var integralImage = integralImages[0];
    var integralImageSquare = integralImages[1];
    var wb1 = i * width + j;
    var wb2 = i * width + (j + blockSize);
    var wb3 = (i + blockSize) * width + j;
    var wb4 = (i + blockSize) * width + (j + blockSize);

    var total = integralImage[wb1] - integralImage[wb2] - integralImage[wb3] + integralImage[wb4];
    var totalSquare = integralImageSquare[wb1] - integralImageSquare[wb2] - integralImageSquare[wb3] + integralImageSquare[wb4];

    var mean = total * inverseArea;
    var variance = totalSquare * inverseArea - mean * mean;

    var standardDeviation = 1;
    if (variance > 0) {
      standardDeviation = Math.sqrt(variance);
    }

    var length = data.length;

    for (var w = 0; w < length; ) {
      var stageSum = 0;
      var stageThreshold = data[w++];
      var nodeLength = data[w++];

      while (nodeLength--) {
        var rectsSum = 0;
        var rectsLength = data[w++];

        for (var r = 0; r < rectsLength; r++) {
          var rectLeft = j + (data[w++] * scale + 0.5) | 0;
          var rectTop = i + (data[w++] * scale + 0.5) | 0;
          var rectWidth = (data[w++] * scale + 0.5) | 0;
          var rectHeight = (data[w++] * scale + 0.5) | 0;
          var rectWeight = data[w++];
          var recRight = rectLeft + rectWidth;
          var recBottom = rectTop + rectHeight;
          var w1 = rectTop * width + rectLeft;
          var w2 = rectTop * width + recRight;
          var w3 = recBottom * width + rectLeft;
          var w4 = recBottom * width + recRight;
          rectsSum += (integralImage[w1] - integralImage[w2] - integralImage[w3] + integralImage[w4]) * rectWeight;
        }

        var nodeThreshold = data[w++];
        var nodeLeft = data[w++];
        var nodeRight = data[w++];

        if (rectsSum * inverseArea < nodeThreshold * standardDeviation) {
          stageSum += nodeLeft;
        } else {
          stageSum += nodeRight;
        }
      }

      if (stageSum <= stageThreshold) {
        return false;
      }
    }
    return true;
  };

  /**
   * Postprocess the detected sub-windows in order to combine overlapping
   * detections into a single detection.
   * @param {array} rects
   * @return {array}
   * @private
   */
  tracking.ViolaJones.mergeRectangles_ = function(rects) {
    var disjointSet = new tracking.DisjointSet(rects.length);

    for (var i = 0; i < rects.length; i++) {
      var r1 = rects[i];
      for (var j = 0; j < rects.length; j++) {
        var r2 = rects[j];
        if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.size, r1.y + r1.size, r2.x, r2.y, r2.x + r2.size, r2.y + r2.size)) {
          var x1 = Math.max(r1.x, r2.x);
          var y1 = Math.max(r1.y, r2.y);
          var x2 = Math.min(r1.x + r1.size, r2.x + r2.size);
          var y2 = Math.min(r1.y + r1.size, r2.y + r2.size);
          var overlap = (x1 - x2) * (y1 - y2);
          var area1 = (r1.size * r1.size);
          var area2 = (r2.size * r2.size);

          if ((overlap / (area1 * (area1 / area2)) >= this.REGIONS_OVERLAP) &&
            (overlap / (area2 * (area1 / area2)) >= this.REGIONS_OVERLAP)) {
            disjointSet.union(i, j);
          }
        }
      }
    }

    var map = {};
    for (var k = 0; k < disjointSet.length; k++) {
      var rep = disjointSet.find(k);
      if (!map[rep]) {
        map[rep] = {
          count: 1,
          size: rects[k].size,
          x: rects[k].x,
          y: rects[k].y
        };
        continue;
      }
      map[rep].count++;
      map[rep].size += rects[k].size;
      map[rep].x += rects[k].x;
      map[rep].y += rects[k].y;
    }

    var result = [];
    Object.keys(map).forEach(function(key) {
      var rect = map[key];
      result.push({
        size: rect.size / rect.count,
        x: rect.x / rect.count,
        y: rect.y / rect.count
      });
    });

    return result;
  };

}());
<commit_msg>Fail a stage when the sum is lower than threshold<commit_after>(function() {
  /*
   * ViolaJones utility.
   * @static
   * @constructor
   */
  tracking.ViolaJones = {};

  /**
   * Holds the minimum area of intersection that defines when a rectangle is
   * from the same group. Often when a face is matched multiple rectangles are
   * classified as possible rectangles to represent the face, when they
   * intersects they are grouped as one face.
   * @type {number}
   * @default 0.5
   * @static
   */
  tracking.ViolaJones.REGIONS_OVERLAP = 0.5;

  /**
   * Holds the block size.
   * @type {number}
   * @default 20
   * @static
   */
  tracking.ViolaJones.BLOCK_SIZE = 20;

  /**
   * Holds the block jump size.
   * @type {number}
   * @default 2
   * @static
   */
  tracking.ViolaJones.BLOCK_JUMP = 2;

  /**
   * Holds the block scale factor.
   * @type {number}
   * @default 1.25
   * @static
   */
  tracking.ViolaJones.BLOCK_SCALE = 1.25;

  /**
   * Detects through the HAAR cascade data rectangles matches.
   * @param {array} The grayscale pixels in a linear [p1,p2,...] array.
   * @param {number} width The image width.
   * @param {number} height The image height.
   * @param {number} data The HAAR cascade data.
   * @return {array} Found rectangles.
   */
  tracking.ViolaJones.detect = function(pixels, width, height, data) {
    var integralImages = tracking.Matrix.computeIntergralImage(pixels, width, height);
    var blockSize = this.BLOCK_SIZE;
    var blockSizeInverse = 1.0 / this.BLOCK_SIZE;
    var maxBlockSize = Math.min(width, height);
    var position = 0;
    var payload = [];

    for (; blockSize <= maxBlockSize; blockSize = (blockSize * this.BLOCK_SCALE + 0.5) | 0) {
      var inverseArea = 1.0 / (blockSize * blockSize);
      var scale = blockSize * blockSizeInverse;

      var xmax = (height - blockSize);
      var ymax = (width - blockSize);
      var jump = (this.BLOCK_JUMP * scale + 0.5) | 0;

      for (var i = 0; i < xmax; i += jump) {
        for (var j = 0; j < ymax; j += jump) {
          if (this.evalStages_(data, integralImages, i, j, width, blockSize, scale, inverseArea)) {
            payload[position++] = {
              size: blockSize,
              x: j,
              y: i
            };
          }
        }
      }
    }
    return this.mergeRectangles_(payload);
  };

  /**
   * Evaluates if the block size on i,j position is a valid HAAR cascade
   * stage.
   * @param {number} data The HAAR cascade data.
   * @param {Array.<Array.<number>>} integralImages Array containing in the
   *     first position the integral image and in the second position the integral
   *     image squared.
   * @param {number} i Vertical position of the pixel to be evaluated.
   * @param {number} j Horizontal position of the pixel to be evaluated.
   * @param {number} width The image width.
   * @param {number} blockSize The block size.
   * @param {number} scale The scale factor of the block size and its original
   *     size.
   * @param {number} inverseArea The inverse area of the block size.
   * @return {boolean} Whether the region passes all the stage tests.
   * @private
   */
  tracking.ViolaJones.evalStages_ = function(data, integralImages, i, j, width, blockSize, scale, inverseArea) {
    var integralImage = integralImages[0];
    var integralImageSquare = integralImages[1];
    var wb1 = i * width + j;
    var wb2 = i * width + (j + blockSize);
    var wb3 = (i + blockSize) * width + j;
    var wb4 = (i + blockSize) * width + (j + blockSize);

    var total = integralImage[wb1] - integralImage[wb2] - integralImage[wb3] + integralImage[wb4];
    var totalSquare = integralImageSquare[wb1] - integralImageSquare[wb2] - integralImageSquare[wb3] + integralImageSquare[wb4];

    var mean = total * inverseArea;
    var variance = totalSquare * inverseArea - mean * mean;

    var standardDeviation = 1;
    if (variance > 0) {
      standardDeviation = Math.sqrt(variance);
    }

    var length = data.length;

    for (var w = 0; w < length; ) {
      var stageSum = 0;
      var stageThreshold = data[w++];
      var nodeLength = data[w++];

      while (nodeLength--) {
        var rectsSum = 0;
        var rectsLength = data[w++];

        for (var r = 0; r < rectsLength; r++) {
          var rectLeft = j + (data[w++] * scale + 0.5) | 0;
          var rectTop = i + (data[w++] * scale + 0.5) | 0;
          var rectWidth = (data[w++] * scale + 0.5) | 0;
          var rectHeight = (data[w++] * scale + 0.5) | 0;
          var rectWeight = data[w++];
          var recRight = rectLeft + rectWidth;
          var recBottom = rectTop + rectHeight;
          var w1 = rectTop * width + rectLeft;
          var w2 = rectTop * width + recRight;
          var w3 = recBottom * width + rectLeft;
          var w4 = recBottom * width + recRight;
          rectsSum += (integralImage[w1] - integralImage[w2] - integralImage[w3] + integralImage[w4]) * rectWeight;
        }

        var nodeThreshold = data[w++];
        var nodeLeft = data[w++];
        var nodeRight = data[w++];

        if (rectsSum * inverseArea < nodeThreshold * standardDeviation) {
          stageSum += nodeLeft;
        } else {
          stageSum += nodeRight;
        }
      }

      if (stageSum < stageThreshold) {
        return false;
      }
    }
    return true;
  };

  /**
   * Postprocess the detected sub-windows in order to combine overlapping
   * detections into a single detection.
   * @param {array} rects
   * @return {array}
   * @private
   */
  tracking.ViolaJones.mergeRectangles_ = function(rects) {
    var disjointSet = new tracking.DisjointSet(rects.length);

    for (var i = 0; i < rects.length; i++) {
      var r1 = rects[i];
      for (var j = 0; j < rects.length; j++) {
        var r2 = rects[j];
        if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.size, r1.y + r1.size, r2.x, r2.y, r2.x + r2.size, r2.y + r2.size)) {
          var x1 = Math.max(r1.x, r2.x);
          var y1 = Math.max(r1.y, r2.y);
          var x2 = Math.min(r1.x + r1.size, r2.x + r2.size);
          var y2 = Math.min(r1.y + r1.size, r2.y + r2.size);
          var overlap = (x1 - x2) * (y1 - y2);
          var area1 = (r1.size * r1.size);
          var area2 = (r2.size * r2.size);

          if ((overlap / (area1 * (area1 / area2)) >= this.REGIONS_OVERLAP) &&
            (overlap / (area2 * (area1 / area2)) >= this.REGIONS_OVERLAP)) {
            disjointSet.union(i, j);
          }
        }
      }
    }

    var map = {};
    for (var k = 0; k < disjointSet.length; k++) {
      var rep = disjointSet.find(k);
      if (!map[rep]) {
        map[rep] = {
          count: 1,
          size: rects[k].size,
          x: rects[k].x,
          y: rects[k].y
        };
        continue;
      }
      map[rep].count++;
      map[rep].size += rects[k].size;
      map[rep].x += rects[k].x;
      map[rep].y += rects[k].y;
    }

    var result = [];
    Object.keys(map).forEach(function(key) {
      var rect = map[key];
      result.push({
        size: rect.size / rect.count,
        x: rect.x / rect.count,
        y: rect.y / rect.count
      });
    });

    return result;
  };

}());
<|endoftext|>


<commit_before>var config   = require("./conf/mongo-driver"),
    Playlist = require('../../api/Playlist'),
    Media    = require('../../api/Media'),
    mubsub   = require("mubsub"),
    moment   = require("moment");

function mongo_driver() {
    var self = this;

    this.newPlaylistCallback    = undefined;
    this.updatePlaylistCallback = undefined;
    this.removePlaylistCallback = undefined;

    console.log("mbc-mosto: [INFO] Creating mongodb playlists driver");

    mongo_driver.prototype.start = function(config) {
        var db = require('./db').db(config && config.db);
        var client = mubsub(db);

        var channel = client.channel('messages', { size: 10000000, max: 5000 });

        self.scheds = db.collection('scheds');
        self.lists = db.collection('lists');
        self.readPlaylists();

        channel.subscribe({channel: 'schedbackend', method: 'create'}, function(msg) {
            self.createPlaylist(msg.model, self.newPlaylistCallback);
        });
        channel.subscribe({channel: 'schedbackend', method: 'update'}, function(msg) {
            self.createPlaylist(msg.model, self.updatePlaylistCallback);
        });
        channel.subscribe({channel: 'schedbackend', method: 'delete'}, function(msg) {
            self.deletePlaylistCallback(msg.model._id);
        });
    };
    mongo_driver.prototype.registerNewPlaylistListener = function(newPlaylistCallback) {
        self.newPlaylistCallback = newPlaylistCallback;
    };
    mongo_driver.prototype.registerUpdatePlaylistListener = function(updatePlaylistCallback) {
        self.updatePlaylistCallback = updatePlaylistCallback;
    };
    mongo_driver.prototype.registerRemovePlaylistListener = function(removePlaylistCallback) {
        self.removePlaylistCallback = removePlaylistCallback;
    };
    mongo_driver.prototype.validTimes = function() {
        var now = moment(new Date());
        var until = moment(new Date());
        until.add(config.load_time * 60 * 1000);
        return {
            from: now,
            to: until
        }
    };

    mongo_driver.prototype.readPlaylists =  function() {
        // read playlists from the database

        /*
         * This should get the database's 'scheds' and 'lists' collections
         * and turn them into a mosto.api.Playlist
         */
        //console.log("mbc-mosto: [INFO] Start reading playlists from " + config.playlists.to_read);
        var boundaries = self.validTimes();
        var now = boundaries.from;
        var until = boundaries.to;
        self.scheds.findEach({
            start: { $lte: until.unix()},
            end: { $gte: now.unix() }}, function(err, sched) {
                if( err ) {
                    console.log(err);
                } else if( sched ) {
                    console.log("Processing sched:", sched);
                    self.createPlaylist(sched, self.newPlaylistCallback);
                } else {
                    console.log('Done');
                }
            });
    };

    mongo_driver.prototype.createPlaylist = function(sched, callback) {
        self.lists.findById(sched.list, function(err, list) {
            console.log("Processing list:", list);
            var startDate = new Date(sched.start * 1000);
            var endDate   = new Date(sched.end * 1000);
            var name = (sched._id.toHexString && sched._id.toHexString()) || sched._id;

            var medias = [];
            list.models.forEach(function(block) {
                // TODO: don't know what goes in type
                var type = "default";
                var file = block.file;
                var length = block.durationraw;
                var fps = block.fps;
                medias.push(new Media(type, file, length, parseFloat(fps)));
            });
            callback(new Playlist(name, startDate, medias, endDate));
        });
    };
}

exports = module.exports = function() {
    var driver = new mongo_driver();
    return driver;
};
<commit_msg>method to check that a given sched is within valid timeframe<commit_after>var config   = require("./conf/mongo-driver"),
    Playlist = require('../../api/Playlist'),
    Media    = require('../../api/Media'),
    mubsub   = require("mubsub"),
    moment   = require("moment");

function mongo_driver() {
    var self = this;

    this.newPlaylistCallback    = undefined;
    this.updatePlaylistCallback = undefined;
    this.removePlaylistCallback = undefined;

    console.log("mbc-mosto: [INFO] Creating mongodb playlists driver");

    mongo_driver.prototype.start = function(config) {
        var db = require('./db').db(config && config.db);
        var client = mubsub(db);

        var channel = client.channel('messages', { size: 10000000, max: 5000 });

        self.scheds = db.collection('scheds');
        self.lists = db.collection('lists');
        self.readPlaylists();

        channel.subscribe({channel: 'schedbackend', method: 'create'}, function(msg) {
            self.createPlaylist(msg.model, self.newPlaylistCallback);
        });
        channel.subscribe({channel: 'schedbackend', method: 'update'}, function(msg) {
            self.createPlaylist(msg.model, self.updatePlaylistCallback);
        });
        channel.subscribe({channel: 'schedbackend', method: 'delete'}, function(msg) {
            self.deletePlaylistCallback(msg.model._id);
        });
    };
    mongo_driver.prototype.registerNewPlaylistListener = function(newPlaylistCallback) {
        self.newPlaylistCallback = newPlaylistCallback;
    };
    mongo_driver.prototype.registerUpdatePlaylistListener = function(updatePlaylistCallback) {
        self.updatePlaylistCallback = updatePlaylistCallback;
    };
    mongo_driver.prototype.registerRemovePlaylistListener = function(removePlaylistCallback) {
        self.removePlaylistCallback = removePlaylistCallback;
    };
    mongo_driver.prototype.validTimes = function() {
        var now = moment(new Date());
        var until = moment(new Date());
        until.add(config.load_time * 60 * 1000);
        return {
            from: now,
            to: until
        }
    };

    mongo_driver.prototype.inTime = function(sched) {
        var boundaries = self.validTimes();
        return (sched.start <= boundaries.to.unix() &&
                sched.end >= boundaries.from.unix());
    };

    mongo_driver.prototype.readPlaylists =  function() {
        // read playlists from the database

        /*
         * This should get the database's 'scheds' and 'lists' collections
         * and turn them into a mosto.api.Playlist
         */
        //console.log("mbc-mosto: [INFO] Start reading playlists from " + config.playlists.to_read);
        var boundaries = self.validTimes();
        var now = boundaries.from;
        var until = boundaries.to;
        self.scheds.findEach({
            start: { $lte: until.unix()},
            end: { $gte: now.unix() }}, function(err, sched) {
                if( err ) {
                    console.log(err);
                } else if( sched ) {
                    console.log("Processing sched:", sched);
                    self.createPlaylist(sched, self.newPlaylistCallback);
                } else {
                    console.log('Done');
                }
            });
    };

    mongo_driver.prototype.createPlaylist = function(sched, callback) {
        self.lists.findById(sched.list, function(err, list) {
            console.log("Processing list:", list);
            var startDate = new Date(sched.start * 1000);
            var endDate   = new Date(sched.end * 1000);
            var name = (sched._id.toHexString && sched._id.toHexString()) || sched._id;

            var medias = [];
            list.models.forEach(function(block) {
                // TODO: don't know what goes in type
                var type = "default";
                var file = block.file;
                var length = block.durationraw;
                var fps = block.fps;
                medias.push(new Media(type, file, length, parseFloat(fps)));
            });
            callback(new Playlist(name, startDate, medias, endDate));
        });
    };
}

exports = module.exports = function() {
    var driver = new mongo_driver();
    return driver;
};
<|endoftext|>